436 lines
12 KiB
Lua
436 lines
12 KiB
Lua
--[[
|
|
|
|
TechAge
|
|
=======
|
|
|
|
Copyright (C) 2017-2022 Joachim Stolberg
|
|
|
|
AGPL v3
|
|
See LICENSE.txt for more information
|
|
|
|
Logic Block 2
|
|
|
|
]]--
|
|
|
|
-- for lazy programmers
|
|
local M = minetest.get_meta
|
|
local S = techage.S
|
|
local logic = techage.logic
|
|
local NUM_RULES = 4
|
|
|
|
local HELP = S("Send an 'on'/'off' command if the\nexpression becomes true.\n") ..
|
|
S("\nRule:\n<output> = on/off if <input-expression> is true\n") ..
|
|
S("\n<output> is the block number to which the\ncommand should be sent.\n") ..
|
|
S("\n<input-expression> is a boolean expression\nwhere input numbers are evaluated.\n") ..
|
|
S("\nExamples:\n1234 == on\n1234 == off\n1234 == on and 2345 == off\n2345 ~= 3456\n") ..
|
|
S("\nValid operators:\nand or on off me == ~= ( )\n") ..
|
|
S("'~=' means: not equal\n") ..
|
|
S("'me' has to be used for the own block number.\n") ..
|
|
S("\nAll rules are checked with each received\ncommand.") ..
|
|
S("\nThe internal processing time for all\ncommands is 100 ms.")
|
|
|
|
local ValidSymbols = {
|
|
["me"] = true,
|
|
["and"] = true,
|
|
["or"] = true,
|
|
["on"] = true,
|
|
["off"] = true,
|
|
["=="] = true,
|
|
["~="] = true,
|
|
["("] = true,
|
|
[")"] = true,
|
|
}
|
|
|
|
local Dropdown = {
|
|
[""] = 1,
|
|
["on"] = 2,
|
|
["off"] = 3
|
|
}
|
|
|
|
local function check_expr(pos, expr)
|
|
local nvm = techage.get_nvm(pos)
|
|
local origin = expr
|
|
-- Add blanks for the syntax check
|
|
expr = expr:gsub("==", " == ")
|
|
expr = expr:gsub("~=", " ~= ")
|
|
expr = expr:gsub("%(", " ( ")
|
|
expr = expr:gsub("%)", " ) ")
|
|
|
|
-- First syntax check
|
|
local old_sym = "or" -- valid default value
|
|
for sym in expr:gmatch("[^%s]+") do
|
|
if not ValidSymbols[sym] and string.find(sym, '^[0-9]+$') == nil then
|
|
return "Unexpected symbol '"..sym.."'"
|
|
end
|
|
if string.find(sym, '^[0-9]+$') and sym == nvm.own_num then
|
|
return "Invalid node number '"..sym.."'"
|
|
end
|
|
-- function call check
|
|
if sym == "(" and (old_sym ~= "and" and old_sym ~= "or") then
|
|
return "Syntax error at '" .. sym .. "'"
|
|
end
|
|
old_sym = sym
|
|
end
|
|
-- Second syntax check
|
|
local code, _ = loadstring("return " .. expr)
|
|
if not code then
|
|
return "Syntax error in '" .. origin .. "'"
|
|
end
|
|
end
|
|
|
|
local function check_num(pos, num, player_name)
|
|
local nvm = techage.get_nvm(pos)
|
|
|
|
if num ~= "me" and (num == nvm.own_num or
|
|
not techage.check_numbers(num, player_name)) then
|
|
return "Invalid node number '"..num.."'"
|
|
end
|
|
end
|
|
|
|
local function debug(mem, text)
|
|
mem.debug = mem.debug or {}
|
|
if #mem.debug > 20 then
|
|
table.remove(mem.debug, 1)
|
|
end
|
|
local s = string.format("%.3f", techage.SystemTime) .. " s: " .. text
|
|
table.insert(mem.debug, s)
|
|
end
|
|
|
|
local function send(pos, num, val)
|
|
local nvm = techage.get_nvm(pos)
|
|
local mem = techage.get_mem(pos)
|
|
debug(mem, "(outp) " .. num .. " = " .. val)
|
|
|
|
if num == "me" then
|
|
nvm.outp_tbl = nvm.outp_tbl or {}
|
|
nvm.outp_tbl.me = val
|
|
-- set the input directly
|
|
nvm.inp_tbl = nvm.inp_tbl or {}
|
|
nvm.inp_tbl.me = val
|
|
else
|
|
nvm.outp_tbl = nvm.outp_tbl or {}
|
|
nvm.outp_tbl[num] = val
|
|
nvm.own_num = nvm.own_num or M(pos):get_string("node_number")
|
|
techage.send_single(nvm.own_num, num, val)
|
|
end
|
|
end
|
|
|
|
local function get_inputs(pos)
|
|
local nvm = techage.get_nvm(pos)
|
|
local mem = techage.get_mem(pos)
|
|
-- old data is needed for formspec 'input' values
|
|
nvm.old_inp_tbl = table.copy(nvm.inp_tbl or {})
|
|
for _, num in ipairs(mem.outp_num or {}) do
|
|
nvm.old_inp_tbl[num] = nvm.outp_tbl[num] or "off"
|
|
end
|
|
return nvm.old_inp_tbl
|
|
end
|
|
|
|
local function check_syntax(pos, line, owner, outp, expr)
|
|
local err = check_num(pos, outp, owner)
|
|
if not err then
|
|
err = check_expr(pos, expr)
|
|
if not err then
|
|
return true, "ok"
|
|
end
|
|
end
|
|
return false, "Error(" .. line .. "): " .. err
|
|
end
|
|
|
|
local function compile(nvm, str)
|
|
if str then
|
|
local code, _ = loadstring(str)
|
|
if code then
|
|
nvm.error = "ok"
|
|
return code
|
|
else
|
|
nvm.error = "Unknown compile error"
|
|
end
|
|
end
|
|
end
|
|
|
|
local function data(nvm)
|
|
local inp = {}
|
|
local outp = {}
|
|
for num, val in pairs(nvm.old_inp_tbl or {}) do
|
|
if num == nvm.own_num then num = "me" end
|
|
inp[#inp+1] = num .. " = " .. tostring(val)
|
|
end
|
|
for num, val in pairs(nvm.outp_tbl or {}) do
|
|
if num == nvm.own_num then num = "me" end
|
|
outp[#outp+1] = num .. " = " .. tostring(val)
|
|
end
|
|
return table.concat(inp, ", "), table.concat(outp, ", ")
|
|
end
|
|
|
|
local function get_code(pos, nvm, mem)
|
|
local meta = M(pos)
|
|
local tbl = {"local inputs = get_inputs(pos) or {}"}
|
|
local owner = M(pos):get_string("owner")
|
|
nvm.own_num = nvm.own_num or M(pos):get_string("node_number")
|
|
mem.outp_num = {}
|
|
|
|
for i = 1,NUM_RULES do
|
|
local outp = meta:get_string("outp" .. i)
|
|
local val = meta:get_string("val" .. i)
|
|
local expr = meta:get_string("expr" .. i)
|
|
|
|
if outp ~= "" and val ~= "" and expr ~= "" then
|
|
local res, err = check_syntax(pos, i, owner, outp, expr)
|
|
if res then
|
|
expr = string.gsub(expr, '([0-9]+)', 'inputs["%1"]')
|
|
expr = string.gsub(expr, 'me', 'inputs["me"]')
|
|
expr = string.gsub(expr, 'on', '"on"')
|
|
expr = string.gsub(expr, 'off', '"off"')
|
|
tbl[#tbl + 1] = "if "..expr.." then send(pos, '"..outp.."', '"..val.."') end"
|
|
table.insert(mem.outp_num, outp)
|
|
else
|
|
nvm.error = err
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
local str = table.concat(tbl, "\n")
|
|
local code = compile(nvm, str)
|
|
if code then
|
|
local env = {}
|
|
env.send = send
|
|
env.pos = pos
|
|
env.get_inputs = get_inputs
|
|
setfenv(code, env)
|
|
|
|
return code
|
|
end
|
|
end
|
|
|
|
local function execute(pos)
|
|
local nvm = techage.get_nvm(pos)
|
|
local mem = techage.get_mem(pos)
|
|
mem.code = mem.code or get_code(pos, nvm, mem)
|
|
if mem.code then
|
|
local res, _ = pcall(mem.code)
|
|
if not res then
|
|
nvm.error = "Unknown runtime error"
|
|
mem.code = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
local function rules(meta)
|
|
local tbl = {}
|
|
|
|
tbl[#tbl + 1] = "label[-0.2,0;<outp>]"
|
|
tbl[#tbl + 1] = "label[1.4,0;=]"
|
|
tbl[#tbl + 1] = "label[1.8,0;<cmnd>]"
|
|
tbl[#tbl + 1] = "label[3.5,0;if]"
|
|
tbl[#tbl + 1] = "label[4.2,0;<inp expression> is true]"
|
|
|
|
|
|
for i = 1,NUM_RULES do
|
|
local y1 = (i * 0.9) - 0.1
|
|
local y2 = (i * 0.9) - 0.2
|
|
local y3 = (i * 0.9) - 0.3
|
|
local outp = meta:get_string("outp" .. i)
|
|
local val = meta:get_string("val" .. i)
|
|
local expr = meta:get_string("expr" .. i)
|
|
val = Dropdown[val] or 1
|
|
|
|
tbl[#tbl + 1] = "field[0," .. y1 .. ";1.6,1;outp" .. i ..";;" .. outp .. "]"
|
|
tbl[#tbl + 1] = "label[1.4," .. y2 .. ";=]"
|
|
tbl[#tbl + 1] = "dropdown[1.8," .. y3 .. ";1.6,1;val" .. i ..";,on,off;" .. val .. "]"
|
|
tbl[#tbl + 1] = "label[3.5," .. y2 .. ";if]"
|
|
tbl[#tbl + 1] = "field[4.2," .. y1 .. ";5.6,1;expr" .. i ..";;" .. expr .. "]"
|
|
end
|
|
return table.concat(tbl, "")
|
|
end
|
|
|
|
local function formspec(pos, meta)
|
|
local nvm = techage.get_nvm(pos)
|
|
local err = nvm.error or "ok"
|
|
err = minetest.formspec_escape(err)
|
|
nvm.io_tbl = nvm.io_tbl or {}
|
|
local inputs, outputs = data(nvm)
|
|
local bt = nvm.blocking_time or 1
|
|
return "size[10,8.2]" ..
|
|
"tabheader[0,0;tab;"..S("Rules") .. "," .. S("Help") .. "," .. S("Debug") .. ";1;;true]" ..
|
|
"container[0.4,0.1]" ..
|
|
rules(meta) ..
|
|
"container_end[]" ..
|
|
|
|
"label[0.2,4.4;" .. S("Blocking Time") .. "]"..
|
|
"field[4.6,4.5;2,1;bt;;" .. bt .. "]"..
|
|
"label[6.3,4.4;s]"..
|
|
|
|
"label[0,5.3;" .. S("Inputs") .. ":]" ..
|
|
"label[2,5.3;" .. inputs .."]" ..
|
|
"label[0,5.9;" .. S("Outputs") .. ":]" ..
|
|
"label[2,5.9;" .. outputs .."]" ..
|
|
"label[0,6.5;" .. S("Syntax") .. ":]" ..
|
|
"label[2,6.5;" .. err .. "]" ..
|
|
"button[1.5,7.5;3,1;update;" .. S("Update") .. "]" ..
|
|
"button[5.6,7.5;3,1;store;" .. S("Store") .. "]"
|
|
end
|
|
|
|
local function formspec_help()
|
|
return "size[10,8.2]" ..
|
|
"tabheader[0,0;tab;"..S("Rules") .. "," .. S("Help") .. "," .. S("Debug") .. ";2;;true]" ..
|
|
"textarea[0.3,0.3;9.9,8.5;;;"..minetest.formspec_escape(HELP).."]"
|
|
end
|
|
|
|
local function formspec_debug(mem)
|
|
mem.debug = mem.debug or {}
|
|
local s = table.concat(mem.debug, "\n")
|
|
return "size[10,8.2]" ..
|
|
"tabheader[0,0;tab;"..S("Rules") .. "," .. S("Help") .. "," .. S("Debug") .. ";3;;true]" ..
|
|
"textarea[0.3,0.3;9.9,8.5;;;"..minetest.formspec_escape(s).."]" ..
|
|
"button[1.5,7.5;3,1;update2;" .. S("Update") .. "]" ..
|
|
"button[5.6,7.5;3,1;clear;" .. S("Clear") .. "]"
|
|
end
|
|
|
|
minetest.register_node("techage:ta3_logic2", {
|
|
description = S("TA3 Logic Block"),
|
|
tiles = {
|
|
-- up, down, right, left, back, front
|
|
"techage_filling_ta3.png^techage_frame_ta3_top.png",
|
|
"techage_filling_ta3.png^techage_frame_ta3_top.png",
|
|
"techage_filling_ta3.png^techage_frame_ta3.png^techage_appl_logic.png",
|
|
},
|
|
|
|
after_place_node = function(pos, placer)
|
|
local meta = M(pos)
|
|
local nvm = techage.get_nvm(pos)
|
|
logic.after_place_node(pos, placer, "techage:ta3_logic2", S("TA3 Logic Block"))
|
|
logic.infotext(meta, S("TA3 Logic Block"))
|
|
meta:set_string("formspec", formspec(pos, meta))
|
|
meta:set_string("owner", placer:get_player_name())
|
|
end,
|
|
|
|
on_receive_fields = function(pos, formname, fields, player)
|
|
if minetest.is_protected(pos, player:get_player_name()) then
|
|
return
|
|
end
|
|
|
|
local meta = M(pos)
|
|
|
|
if fields.store then
|
|
for i = 1,NUM_RULES do
|
|
meta:set_string("outp" .. i, fields["outp" .. i] or "")
|
|
meta:set_string("val" .. i, fields["val" .. i] or "")
|
|
meta:set_string("expr" .. i, fields["expr" .. i] or "")
|
|
end
|
|
local nvm = techage.get_nvm(pos)
|
|
nvm.blocking_time = tonumber(fields.bt) or 0.1
|
|
nvm.inp_tbl = {me = "off"}
|
|
nvm.outp_tbl = {}
|
|
elseif fields.update2 then
|
|
local mem = techage.get_mem(pos)
|
|
meta:set_string("formspec", formspec_debug(mem))
|
|
elseif fields.clear then
|
|
local mem = techage.get_mem(pos)
|
|
mem.debug = {}
|
|
meta:set_string("formspec", formspec_debug(mem))
|
|
end
|
|
|
|
if fields.tab == "2" then
|
|
meta:set_string("formspec", formspec_help())
|
|
elseif fields.tab == "3" then
|
|
local mem = techage.get_mem(pos)
|
|
meta:set_string("formspec", formspec_debug(mem))
|
|
else
|
|
local nvm = techage.get_nvm(pos)
|
|
local mem = techage.get_mem(pos)
|
|
mem.code = nil
|
|
get_code(pos, nvm, mem)
|
|
meta:set_string("formspec", formspec(pos, meta))
|
|
end
|
|
end,
|
|
|
|
on_timer = function(pos)
|
|
execute(pos)
|
|
return false
|
|
end,
|
|
|
|
on_rightclick = function(pos, node, clicker)
|
|
if minetest.is_protected(pos, clicker:get_player_name()) then
|
|
return
|
|
end
|
|
|
|
local meta = M(pos)
|
|
local nvm = techage.get_nvm(pos)
|
|
meta:set_string("formspec", formspec(pos, meta))
|
|
end,
|
|
|
|
after_dig_node = function(pos, oldnode, oldmetadata)
|
|
techage.remove_node(pos, oldnode, oldmetadata)
|
|
techage.del_mem(pos)
|
|
end,
|
|
|
|
paramtype2 = "facedir",
|
|
groups = {choppy=2, cracky=2, crumbly=2},
|
|
is_ground_content = false,
|
|
sounds = default.node_sound_wood_defaults(),
|
|
})
|
|
|
|
|
|
minetest.register_craft({
|
|
output = "techage:ta3_logic2",
|
|
recipe = {
|
|
{"", "group:wood", ""},
|
|
{"techage:vacuum_tube", "default:copper_ingot", "techage:vacuum_tube"},
|
|
{"", "group:wood", ""},
|
|
},
|
|
})
|
|
|
|
techage.register_node({"techage:ta3_logic2"}, {
|
|
on_recv_message = function(pos, src, topic, payload)
|
|
local nvm = techage.get_nvm(pos)
|
|
local mem = techage.get_mem(pos)
|
|
nvm.own_num = nvm.own_num or M(pos):get_string("node_number")
|
|
nvm.blocking_time = nvm.blocking_time or M(pos):get_float("blocking_time")
|
|
nvm.inp_tbl = nvm.inp_tbl or {}
|
|
|
|
if src ~= nvm.own_num then
|
|
if topic == "on" then
|
|
debug(mem, "(inp) " .. src .. " = on")
|
|
nvm.inp_tbl[src] = "on"
|
|
elseif topic == "off" then
|
|
debug(mem, "(inp) " .. src .. " = off")
|
|
nvm.inp_tbl[src] = "off"
|
|
else
|
|
debug(mem, "(inp) invalid command")
|
|
return "unsupported"
|
|
end
|
|
|
|
local t = math.max((mem.ttl or 0) - techage.SystemTime, 0.1)
|
|
minetest.get_node_timer(pos):start(t)
|
|
mem.ttl = techage.SystemTime + (nvm.blocking_time or 0)
|
|
end
|
|
end,
|
|
on_beduino_receive_cmnd = function(pos, src, topic, payload)
|
|
local nvm = techage.get_nvm(pos)
|
|
local mem = techage.get_mem(pos)
|
|
nvm.own_num = nvm.own_num or M(pos):get_string("node_number")
|
|
nvm.blocking_time = nvm.blocking_time or M(pos):get_float("blocking_time")
|
|
nvm.inp_tbl = nvm.inp_tbl or {}
|
|
|
|
if src ~= nvm.own_num then
|
|
if topic == 1 and payload[1] == 1 then
|
|
debug(mem, "(inp) " .. src .. " = on")
|
|
nvm.inp_tbl[src] = "on"
|
|
return 0
|
|
elseif topic == 1 and payload[1] == 0 then
|
|
debug(mem, "(inp) " .. src .. " = off")
|
|
nvm.inp_tbl[src] = "off"
|
|
return 0
|
|
else
|
|
debug(mem, "(inp) invalid command")
|
|
return 2
|
|
end
|
|
local t = math.max((mem.ttl or 0) - techage.SystemTime, 0.1)
|
|
minetest.get_node_timer(pos):start(t)
|
|
mem.ttl = techage.SystemTime + (nvm.blocking_time or 0)
|
|
end
|
|
end,
|
|
})
|