--[[ SmartLine ========= Copyright (C) 2017-2020 Joachim Stolberg AGPL v3 See LICENSE.txt for more information controller.lua: REPLACED BY SMARTLINE CONTROLLER2 !!! ]]-- local NUM_RULES = 10 local mail_exists = minetest.get_modpath("mail") and mail ~= nil local sHELP = [[SmartLine Controller Help Control other nodes by means of rules, according to: IF and/or THEN These rules allow to execute actions based on conditions. Examples for conditions are: - the Player Detector detects a player - a button is pressed - a node state is fault, blocked, standby,... - a timer is expired Actions are: - switch on/off tubelib nodes, like lamps, door blocks, machines - send mail/chat messages to the owner - output a text message to the display - set timer variables - set/reset flag variables Variables and timers: - 8 flags (set/reset) can be used to store conditions for later use. - Action flags (one flag for each rule, set when action is executed) The flag can be used as condition for subsequent rules. - 8 timers (resolution in seconds) can be use for delayed actions. The controller executes all rules once per second. Independent how long the input condition stays 'true', the corresponding action will be triggered only once. The condition has to become false and then true again, to re-trigger/execute the action again. The 'label' has no function. It is only used to give rules a name. Edit command examples: - 'x 1 8' exchange rows 1 with row 8 - 'c 1 2' copy row 1 to 2 - 'd 3' delete row 3 The state view shows the current state of all rules. The colors show, if conditions are true or false and if actions were already executed or not. It has a 'update' button to update the view. ]] local sOUTPUT = "Press 'help' for edit commands" -- -- Helper functions -- local function create_kv_list(elem) local a = {} for i,v in ipairs(elem) do a[v] = i end return a end local function output(label, prefix, postfix, kvTbl) local tbl = {label..": "} for k,v in pairs(kvTbl) do tbl[#tbl+1] = prefix..k..postfix.."="..v..", " end return table.concat(tbl) end -- -- Conditions -- -- tables with all data from condition/action registrations local kvRegisteredCond = {} local kvRegisteredActn = {} -- list of keys for conditions/actions local aCondTypes = {} local aActnTypes = {} -- list of titles for conditions/actions local aCondTitles = {} local aActnTitles = {} -- table with runtime functions local CondRunTimeHandlers = {} local ActnRunTimeHandlers = {} local function eval_cond(data, environ) return CondRunTimeHandlers[data.__idx__](data, environ) and 1 or 0 end local function exec_action(data, environ, number) ActnRunTimeHandlers[data.__idx__](data, environ, number) end -- -- API functions for condition/action registrations -- function smartline.register_condition(key, tData) table.insert(CondRunTimeHandlers, tData.on_execute) table.insert(aCondTypes, key) table.insert(aCondTitles, tData.title) tData.__idx__ = #aCondTypes if kvRegisteredCond[key] ~= nil then print("[SmartLine] Condition registration error "..key) return end kvRegisteredCond[key] = tData for _,item in ipairs(tData.formspec) do if item.type == "textlist" then item.tChoices = string.split(item.choices, ",") item.num_choices = #item.tChoices end end end function smartline.register_action(key, tData) table.insert(ActnRunTimeHandlers, tData.on_execute) table.insert(aActnTypes, key) table.insert(aActnTitles, tData.title) tData.__idx__ = #aActnTypes if kvRegisteredActn[key] ~= nil then print("[SmartLine] Action registration error "..key) return end kvRegisteredActn[key] = tData for _,item in ipairs(tData.formspec) do if item.type == "textlist" then item.tChoices = string.split(item.choices, ",") item.num_choices = #item.tChoices end end end -- -- Formspec -- -- Determine the selected submenu and return the corresponding -- formspec definition. -- postfix: row/culumn info like "11" or "a2" -- type: "cond" or "actn" -- fs_data: formspec data local function get_active_subm_definition(postfix, type, fs_data) local idx = 1 local fs_definition = {} if type == "cond" then idx = fs_data["subm"..postfix.."_cond"] or 1 local key = aCondTypes[idx] fs_definition = kvRegisteredCond[key] elseif type == "actn" then idx = fs_data["subm"..postfix.."_actn"] or 1 local key = aActnTypes[idx] fs_definition = kvRegisteredActn[key] end return idx, fs_definition end -- Extract runtime relevant data from the given submenu -- postfix: row/culum info like "11" or "a2" -- fs_definition: submenu formspec definition -- fs_data: formspec data local function get_subm_data(postfix, fs_definition, fs_data) local data = {} for idx,elem in ipairs(fs_definition.formspec) do if elem.type == "field" then data[elem.name] = fs_data["subm"..postfix.."_"..elem.name] or "?" elseif elem.type == "textlist" then local num = tonumber(fs_data["subm"..postfix.."_"..elem.name]) or 1 num = math.min(num, elem.num_choices) data[elem.name] = num data[elem.name.."_text"] = elem.tChoices[num] end end -- type of the condition/action data.__idx__ = fs_definition.__idx__ return data end -- Copy field/formspec data to the table fs_data -- fs_definition: submenu formspec definituion -- fields: formspec input -- fs_data: formspec data local function field2fs_data(fs_definition, fields, fs_data) for idx,elem in ipairs(fs_definition.formspec) do local key = "subm"..fields._postfix_.."_"..elem.name if elem.type == "field" then if fields[elem.name] then fs_data[key] = fields[elem.name] end elseif elem.type == "textlist" then local evt = minetest.explode_textlist_event(fields[elem.name]) if evt.type == "CHG" then fs_data[key] = evt.index end end if fs_data[key] == nil then fs_data[key] = elem.default end end return fs_data end local function add_controls_to_table(tbl, postfix, fs_data, fs_definition) local val = "" local offs = 2.4 for idx,elem in ipairs(fs_definition.formspec) do if elem.label then tbl[#tbl+1] = "label[0,"..offs..";"..elem.label.."]" offs = offs + 0.5 end if elem.type == "field" then val = fs_data["subm"..postfix.."_"..elem.name] or elem.default tbl[#tbl+1] = "field[0.3,"..(offs+0.2)..";8.2,1;"..elem.name..";;"..val.."]" offs = offs + 0.9 elseif elem.type == "textlist" then val = fs_data["subm"..postfix.."_"..elem.name] or elem.default tbl[#tbl+1] = "textlist[0.0,"..(offs)..";8,1.4;"..elem.name..";"..elem.choices..";"..val.."]" offs = offs + 1.8 end end return tbl end local function runtime_data(postfix, type, fs_data) local _,fs_definition = get_active_subm_definition(postfix, type, fs_data) return get_subm_data(postfix, fs_definition, fs_data) end local function decrement_timers(timers) if timers ~= nil then for idx,_ in pairs(timers) do timers[idx] = tonumber(timers[idx]) if timers[idx] >= 0 then timers[idx] = timers[idx] - 1 end end end end local function toggle_flag(meta, environ) environ.toggle = (meta:get_int("runtime") or 0) % 4 >= 2 end -- -- Condition formspec -- local function formspec_cond(_postfix_, fs_data) local tbl = {"size[8.2,9]".. default.gui_bg.. default.gui_bg_img.. default.gui_slots.. "field[0,0;0,0;_type_;;cond]".. "field[0,0;0,0;_postfix_;;".._postfix_.."]"} local sConditions = table.concat(aCondTitles, ",") local cond_idx, fs_definition = get_active_subm_definition(_postfix_, "cond", fs_data) tbl[#tbl+1] = "label[0,0.1;Condition type:]" tbl[#tbl+1] = "textlist[0,0.6;8,1.4;cond;"..sConditions..";"..cond_idx.."]" tbl = add_controls_to_table(tbl, _postfix_, fs_data, fs_definition) tbl[#tbl+1] = "button[4,8.4;2,1;_cancel_;cancel]" tbl[#tbl+1] = "button[6,8.4;2,1;_exit_;ok]" return table.concat(tbl) end -- evaluate the row condition local function eval_formspec_cond(meta, fs_data, fields, readonly) if readonly then return fs_data end -- determine condition type local cond = minetest.explode_textlist_event(fields.cond) if cond.type == "CHG" then fs_data["subm"..fields._postfix_.."_cond"] = cond.index end -- prepare data local _, fs_definition = get_active_subm_definition(fields._postfix_, "cond", fs_data) fs_data = field2fs_data(fs_definition, fields, fs_data) local data = get_subm_data(fields._postfix_, fs_definition, fs_data) -- update button for main menu fs_data["cond"..fields._postfix_] = fs_definition.button_label(data) if fields._exit_ == nil then -- update formspec if exit is not pressed meta:set_string("formspec", formspec_cond(fields._postfix_, fs_data)) end return fs_data end -- -- Action formspec -- local function formspec_actn(_postfix_, fs_data) local tbl = {"size[8.2,9]".. default.gui_bg.. default.gui_bg_img.. default.gui_slots.. "field[0,0;0,0;_type_;;actn]".. "field[0,0;0,0;_postfix_;;".._postfix_.."]"} local sActions = table.concat(aActnTitles, ",") local actn_idx, fs_definition = get_active_subm_definition(_postfix_, "actn", fs_data) tbl[#tbl+1] = "label[0,0.1;Action type:]" tbl[#tbl+1] = "textlist[0,0.6;8,1.4;actn;"..sActions..";"..actn_idx.."]" tbl = add_controls_to_table(tbl, _postfix_, fs_data, fs_definition) tbl[#tbl+1] = "button[4,8.4;2,1;_cancel_;cancel]" tbl[#tbl+1] = "button[6,8.4;2,1;_exit_;ok]" return table.concat(tbl) end -- evaluate the row action local function eval_formspec_actn(meta, fs_data, fields, readonly) if readonly then return fs_data end -- determine action type local actn = minetest.explode_textlist_event(fields.actn) if actn.type == "CHG" then fs_data["subm"..fields._postfix_.."_actn"] = actn.index end -- prepare data local _, fs_definition = get_active_subm_definition(fields._postfix_, "actn", fs_data) fs_data = field2fs_data(fs_definition, fields, fs_data) local data = get_subm_data(fields._postfix_, fs_definition, fs_data) -- update button for main menu fs_data["actn"..fields._postfix_] = fs_definition.button_label(data) if fields._exit_ == nil then -- update formspec if exit is not pressed meta:set_string("formspec", formspec_actn(fields._postfix_, fs_data)) end if fields._cancel_ then fields._exit_ = true end return fs_data end -- -- Label text formspec -- local function formspec_label(_postfix_, fs_data) local label = fs_data["label".._postfix_] or "" return "size[6,4]".. default.gui_bg.. default.gui_bg_img.. default.gui_slots.. "field[0,0;0,0;_type_;;label]".. "field[0,0;0,0;_postfix_;;".._postfix_.."]".. "label[0.2,0.3;Label:]".. "field[0.3,1.5;5,1;label;;"..label.."]".. "button[4.5,3;1.5,1;_exit_;ok]" end -- evaluate the row label local function eval_formspec_label(meta, fs_data, fields, readonly) if readonly then return fs_data end fs_data["subml"..fields._postfix_.."_label"] = fields.label if fields._exit_ == nil then meta:set_string("formspec", formspec_label(fields._postfix_, fs_data)) end -- set the button label of the main menu based on the given input in the submenu fs_data["label"..fields._postfix_] = fs_data["subml"..fields._postfix_.."_label"] return fs_data end -- -- Operand formspec -- local function formspec_oprnd(_postfix_, fs_data) local oprnd = fs_data["submo".._postfix_.."_oprnd"] or "" return "size[6,4]".. default.gui_bg.. default.gui_bg_img.. default.gui_slots.. "field[0,0;0,0;_type_;;oprnd]".. "field[0,0;0,0;_postfix_;;".._postfix_.."]".. "label[0.2,0.3;Operand:]".. "textlist[0,0.8;5.6,1.4;oprnd;or,and;"..oprnd.."]".. "button[4.5,3;1.5,1;_exit_;ok]" end -- evaluate the row operand local function eval_formspec_oprnd(meta, fs_data, fields, readonly) if readonly then return fs_data end local oprnd = minetest.explode_textlist_event(fields.oprnd) if oprnd.type == "CHG" then fs_data["submo"..fields._postfix_.."_oprnd"] = oprnd.index end if fields._exit_ == nil then meta:set_string("formspec", formspec_oprnd(fields._postfix_, fs_data)) end -- set the button label of the main menu based on the given input in the submenu fs_data["oprnd"..fields._postfix_] = fs_data["submo"..fields._postfix_.."_oprnd"] == 1 and "or" or "and" return fs_data end local function formspec_main(state, fs_data, output) local tbl = {"size[15,9;true]".. default.gui_bg.. default.gui_bg_img.. default.gui_slots.. "field[0,0;0,0;_type_;;main]".. "label[0.8,0;label:]label[3.8,0;IF cond 1:]label[7,0;and/or]label[8.3,0;cond 2:]label[11.7,0;THEN action:]"} for idx = 1,NUM_RULES do local ypos = idx * 0.75 - 0.4 tbl[#tbl+1] = "label[0,"..(0.2+ypos)..";"..idx.."]" tbl[#tbl+1] = "button[0.4,"..ypos..";3,1;label"..idx..";"..(fs_data["label"..idx] or "...").."]" tbl[#tbl+1] = "button[3.5,"..ypos..";3.4,1;cond1"..idx..";"..(fs_data["cond1"..idx] or "...").."]" tbl[#tbl+1] = "button[7,".. ypos..";1,1;oprnd".. idx..";"..(fs_data["oprnd"..idx] or "or").."]" tbl[#tbl+1] = "button[8,".. ypos..";3.4,1;cond2"..idx..";"..(fs_data["cond2"..idx] or "...").."]" tbl[#tbl+1] = "button[11.5,".. ypos..";3.4,1;actna"..idx..";"..(fs_data["actna"..idx] or "...").."]" end tbl[#tbl+1] = "image_button[14,8.1;1,1;".. tubelib.state_button(state) ..";button;]" tbl[#tbl+1] = "button[10.6,8.2;1.5,1;state;state]" tbl[#tbl+1] = "button[12.2,8.2;1.5,1;help;help]" tbl[#tbl+1] = "label[0.2,8.4;"..output.."]" tbl[#tbl+1] = "field[6.5,8.5;3,1;cmnd;;]" tbl[#tbl+1] = "button[9.2,8.2;1,1;ok;OK]" return table.concat(tbl) end local function eval_formspec_main(meta, fs_data, fields, readonly) meta:set_string("fs_old", minetest.serialize(fs_data)) for idx = 1,NUM_RULES do -- eval standard inputs if not readonly then fs_data["oprnd"..idx] = fields["oprnd"..idx] or fs_data["oprnd"..idx] end -- eval submenu button events if fields["label"..idx] then meta:set_string("formspec", formspec_label(idx, fs_data)) elseif fields["cond1"..idx] then meta:set_string("formspec", formspec_cond("1"..idx, fs_data)) elseif fields["cond2"..idx] then meta:set_string("formspec", formspec_cond("2"..idx, fs_data)) elseif fields["oprnd"..idx] then meta:set_string("formspec", formspec_oprnd(idx, fs_data)) elseif fields["actna"..idx] then meta:set_string("formspec", formspec_actn("a"..idx, fs_data)) end end return fs_data end local function formspec_help(offs) return "size[15,9]".. default.gui_bg.. default.gui_bg_img.. default.gui_slots.. "field[0,0;0,0;_type_;;help]".. "label[0,"..(-offs/50)..";"..sHELP.."]".. --"label[0.2,0;test]".. "scrollbar[13.5,1;0.5,7;vertical;sb_help;"..offs.."]".. "button[13.3,8.2;1.5,1;close;close]" end local function background(xpos, ypos, val) if val == true then return "box["..(xpos-0.1)..",".. ypos..";3.3,0.4;#008000]" elseif val == false then return "box["..(xpos-0.1)..",".. ypos..";3.3,0.4;#800000]" else return "box["..(xpos-0.1)..",".. ypos..";3.3,0.4;#202020]" end end local function formspec_state(meta, fs_data) local number = meta:get_string("number") local state = meta:get_int("state") local tbl = {"size[15,9;true]".. default.gui_bg.. default.gui_bg_img.. default.gui_slots.. "field[0,0;0,0;_type_;;state]".. "label[0.8,0;label:]label[3.8,0;IF cond 1:]label[7,0;and/or]label[8.3,0;cond 2:]label[11.7,0;THEN action:]"} if state == tubelib.RUNNING and number then local environ = tubelib.get_data(number, "environ") local act_gate = tubelib.get_data(number, "act_gate") local conds = tubelib.get_data(number, "conds") if environ and act_gate and conds then for idx = 1,NUM_RULES do local ypos = idx * 0.6 + 0.2 local s1 = fs_data["cond1"..idx] or " ... " local s2 = fs_data["cond2"..idx] or " ... " local sa = fs_data["actna"..idx] or " ... " if conds[idx] == nil then tbl[#tbl+1] = background(3.7, ypos, nil) tbl[#tbl+1] = background(8, ypos, nil) else tbl[#tbl+1] = background(3.7, ypos, conds[idx] == 1 or conds[idx] == 3) tbl[#tbl+1] = background(8, ypos, conds[idx] == 2 or conds[idx] == 3) end tbl[#tbl+1] = background(11.5, ypos, act_gate[idx]) tbl[#tbl+1] = "label[0,".. ypos..";"..idx.."]" tbl[#tbl+1] = "label[0.5,"..ypos..";"..(fs_data["label"..idx] or " ... ").."]" tbl[#tbl+1] = "label[3.7,".. ypos..";"..s1.."]" tbl[#tbl+1] = "label[7.2,".. ypos..";"..(fs_data["oprnd"..idx] or "or").."]" tbl[#tbl+1] = "label[8,".. ypos..";"..s2.."]" tbl[#tbl+1] = "label[11.5,".. ypos..";"..sa.."]" if act_gate[idx] == true then act_gate[idx] = false end end tbl[#tbl+1] = "label[10,7; Seconds: "..(meta:get_int("runtime") or 1).."]" tbl[#tbl+1] = "label[0,7;"..output("Inputs", "i(", ")", environ.inputs).."]" tbl[#tbl+1] = "label[0,7.6;"..output("Timers", "t", "", environ.timers).."]" tbl[#tbl+1] = "label[0,8.2;"..output("Flags", "f", "", environ.flags).."]" tbl[#tbl+1] = "label[0,8.8;Hint:]" tbl[#tbl+1] = "box[1.3,8.8;6,0.4;#008000]" tbl[#tbl+1] = "label[1.4,8.8;condition true / action executed]" tbl[#tbl+1] = "box[7.9,8.8;6,0.4;#800000]" tbl[#tbl+1] = "label[8,8.8;condition false / action out-of-date]" end end tbl[#tbl+1] = "button[13.3,6.9;1.7,1;update;update]" tbl[#tbl+1] = "button[13.3,7.8;1.7,1;close;close]" return table.concat(tbl) end local function execute(meta, number, debug) local rt_rules = tubelib.get_data(number, "rt_rules") local environ = tubelib.get_data(number, "environ") local act_gate = tubelib.get_data(number, "act_gate") local conds = tubelib.get_data(number, "conds") if rt_rules and environ and act_gate and conds then environ.actions = {} decrement_timers(environ.timers) toggle_flag(meta, environ) for i,item in ipairs(rt_rules) do local c1 = eval_cond(item.cond1, environ) local c2 = eval_cond(item.cond2, environ) conds[i] = c1 + c2*2 if c1 + c2 >= item.cond_cnt then if act_gate[i] == nil then -- execute action exec_action(item.actn, environ, number) environ.actions[i] = true act_gate[i] = true end else act_gate[i] = nil end end end end local function check_rules(pos, elapsed) if tubelib.data_not_corrupted(pos) then --local t = minetest.get_us_time() local meta = minetest.get_meta(pos) meta:set_int("runtime", (meta:get_int("runtime") or 1) + 1) local number = meta:get_string("number") local state = meta:get_int("state") if state == tubelib.RUNNING and number then execute(meta, number, debug) end --print("time", minetest.get_us_time() - t) return true end return false end local function switch_state(pos, state, fs_data) local meta = minetest.get_meta(pos) local number = meta:get_string("number") meta:set_int("state", state) meta:set_string("formspec", formspec_main(state, fs_data, sOUTPUT)) if state == tubelib.RUNNING then meta:set_string("infotext", "SmartLine Controller "..number..": running") minetest.get_node_timer(pos):start(1) else meta:set_string("infotext", "SmartLine Controller "..number..": stopped") minetest.get_node_timer(pos):stop() end end local function start_controller(pos, number, fs_data) -- delete old data tubelib.set_data(number, "environ", { timers = {}, flags = {}, inputs = {} }) tubelib.set_data(number, "conds", {}) tubelib.set_data(number, "act_gate", {}) switch_state(pos, tubelib.RUNNING, fs_data) end function smartline.stop_controller(pos, fs_data) switch_state(pos, tubelib.STOPPED, fs_data) end local function formspec2runtime_rule(number, owner, fs_data) local rt_rules = {} local num2inp = {} for idx = 1,NUM_RULES do -- valid rule? if fs_data["subm1"..idx.."_cond"] and fs_data["subm2"..idx.."_cond"] and fs_data["subma"..idx.."_actn"] then -- add to list of runtine rules local rule = { cond_cnt = fs_data["oprnd"..idx] == "and" and 2 or 1, cond1 = runtime_data("1"..idx, "cond", fs_data), cond2 = runtime_data("2"..idx, "cond", fs_data), actn = runtime_data("a"..idx, "actn", fs_data), } rule.actn.owner = owner table.insert(rt_rules, rule) end end tubelib.set_data(number, "rt_rules", rt_rules) end local function get_keys(fs_data) -- collect all keys and remove row information local keys = {} for k,v in pairs(fs_data) do local key = string.sub(k,1,5).."*"..string.sub(k, 7) if type(v) == 'number' then keys[key] = 1 -- default value else keys[key] = "..." -- default value end end return keys end local function exchange_rules(fs_data, pos1, pos2) -- exchange elem by elem for k,v in pairs(get_keys(fs_data)) do local k1 = string.gsub(k, "*", pos1) local k2 = string.gsub(k, "*", pos2) local temp = fs_data[k1] or v fs_data[k1] = fs_data[k2] or v fs_data[k2] = temp end return fs_data end local function copy_rule(fs_data, pos1, pos2) -- copy elem by elem for k,v in pairs(get_keys(fs_data)) do local k1 = string.gsub(k, "*", pos1) local k2 = string.gsub(k, "*", pos2) fs_data[k2] = fs_data[k1] or v end return fs_data end local function delete_rule(fs_data, pos) for k,v in pairs(get_keys(fs_data)) do local k1 = string.gsub(k, "*", pos) fs_data[k1] = nil end return fs_data end local function edit_command(fs_data, text) local cmnd, pos1, pos2 = text:match('^(%S)%s(%d+)%s(%d+)$') if pos2 == nil then cmnd, pos1 = text:match('^(%S)%s(%d+)$') end if cmnd and pos1 and pos2 then pos1 = math.max(1, math.min(pos1, NUM_RULES)) pos2 = math.max(1, math.min(pos2, NUM_RULES)) if cmnd == "x" then exchange_rules(fs_data, pos1, pos2) return "rows "..pos1.." and "..pos2.." exchanged" end if cmnd == "c" then copy_rule(fs_data, pos1, pos2) return "row "..pos1.." copied to "..pos2 end elseif cmnd == "d" and pos1 then pos1 = math.max(1, math.min(pos1, NUM_RULES)) delete_rule(fs_data, pos1) return "row "..pos1.." deleted" end return "Invalid command '"..text.."'" end local function on_receive_fields(pos, formname, fields, player) local meta = minetest.get_meta(pos) local owner = meta:get_string("owner") local state = meta:get_int("state") if not player or not player:is_player() then return end local fs_data = minetest.deserialize(meta:get_string("fs_data")) or {} local output = "" local readonly = player:get_player_name() ~= owner -- FIRST: test if command entered? if fields.ok then if not readonly then output = edit_command(fs_data, fields.cmnd) smartline.stop_controller(pos, fs_data) meta:set_string("formspec", formspec_main(tubelib.STOPPED, fs_data, output)) meta:set_string("fs_data", minetest.serialize(fs_data)) end -- SECOND: eval none edit events (events based in __type__)? elseif fields.help then meta:set_string("formspec", formspec_help(1)) elseif fields.state then meta:set_string("formspec", formspec_state(meta, fs_data)) elseif fields.update then meta:set_string("formspec", formspec_state(meta, fs_data)) elseif fields._cancel_ then fs_data = minetest.deserialize(meta:get_string("fs_old")) meta:set_string("formspec", formspec_main(state, fs_data, sOUTPUT)) elseif fields.close then meta:set_string("formspec", formspec_main(state, fs_data, sOUTPUT)) elseif fields.sb_help then local evt = minetest.explode_scrollbar_event(fields.sb_help) if evt.type == "CHG" then meta:set_string("formspec", formspec_help(evt.value)) end elseif fields.button then if not readonly then local number = meta:get_string("number") local state = meta:get_int("state") if state == tubelib.RUNNING then smartline.stop_controller(pos, fs_data) meta:set_string("formspec", formspec_main(tubelib.STOPPED, fs_data, sOUTPUT)) else formspec2runtime_rule(number, owner, fs_data) start_controller(pos, number, fs_data) meta:set_string("formspec", formspec_main(tubelib.RUNNING, fs_data, sOUTPUT)) end end -- THIRD: evaluate edit events from sub-menus elseif fields._type_ == "main" then fs_data = eval_formspec_main(meta, fs_data, fields, readonly) meta:set_string("fs_data", minetest.serialize(fs_data)) elseif fields._type_ == "label" then fs_data = eval_formspec_label(meta, fs_data, fields, readonly) meta:set_string("fs_data", minetest.serialize(fs_data)) elseif fields._type_ == "cond" then fs_data = eval_formspec_cond(meta, fs_data, fields, readonly) meta:set_string("fs_data", minetest.serialize(fs_data)) elseif fields._type_ == "oprnd" then fs_data = eval_formspec_oprnd(meta, fs_data, fields, readonly) meta:set_string("fs_data", minetest.serialize(fs_data)) elseif fields._type_ == "actn" then fs_data = eval_formspec_actn(meta, fs_data, fields, readonly) meta:set_string("fs_data", minetest.serialize(fs_data)) elseif fields._type_ == "help" then meta:set_string("formspec", formspec_main(state, fs_data, sOUTPUT)) elseif fields._type_ == "state" then meta:set_string("formspec", formspec_main(state, fs_data, sOUTPUT)) end -- FOURTH: back to main menu if fields._exit_ then meta:set_string("formspec", formspec_main(state, fs_data, sOUTPUT)) -- smartline.stop_controller(pos, fs_data) -- meta:set_string("fs_data", minetest.serialize(fs_data)) -- end end end minetest.register_node("smartline:controller", { description = "SmartLine Controller", inventory_image = "smartline_controller_inventory.png", wield_image = "smartline_controller_inventory.png", stack_max = 1, tiles = { -- up, down, right, left, back, front "smartline.png", "smartline.png", "smartline.png", "smartline.png", "smartline.png", "smartline.png^smartline_controller.png", }, drawtype = "nodebox", node_box = { type = "fixed", fixed = { { -6/32, -6/32, 14/32, 6/32, 6/32, 16/32}, }, }, after_place_node = function(pos, placer) local meta = minetest.get_meta(pos) local number = tubelib.add_node(pos, "smartline:controller") local fs_data = {} meta:set_string("fs_data", minetest.serialize(fs_data)) meta:set_string("owner", placer:get_player_name()) meta:set_string("number", number) meta:set_int("state", tubelib.STOPPED) meta:set_int("debug", 0) meta:set_string("formspec", formspec_main(tubelib.STOPPED, fs_data, sOUTPUT)) meta:set_string("infotext", "SmartLine Controller "..number..": stopped") end, on_receive_fields = on_receive_fields, on_dig = function(pos, node, puncher, pointed_thing) if minetest.is_protected(pos, puncher:get_player_name()) then return end minetest.node_dig(pos, node, puncher, pointed_thing) tubelib.remove_node(pos) end, on_timer = check_rules, paramtype = "light", sunlight_propagates = true, paramtype2 = "facedir", groups = {choppy=1, cracky=1, crumbly=1, not_in_creative_inventory=1}, is_ground_content = false, sounds = default.node_sound_stone_defaults(), drop = "smartline:controller2", }) minetest.register_craft({ output = "smartline:controller", recipe = { {"", "default:mese_crystal", ""}, {"dye:blue", "default:copper_ingot", "tubelib:wlanchip"}, {"", "default:mese_crystal", ""}, }, }) local function set_input(meta, payload, val) if payload then local number = meta:get_string("number") local environ = tubelib.get_data(number, "environ") or {} if environ.inputs then environ.inputs[payload] = val tubelib.set_data(number, "environ", environ) end end end tubelib.register_node("smartline:controller", {}, { on_recv_message = function(pos, topic, payload) local meta = minetest.get_meta(pos) if topic == "on" then set_input(meta, payload, topic) elseif topic == "off" then set_input(meta, payload, topic) elseif topic == "state" then local state = meta:get_int("state") return tubelib.statestring(state) else return "unsupported" end end, on_node_load = function(pos) local meta = minetest.get_meta(pos) if meta:get_int("state") == tubelib.RUNNING then minetest.get_node_timer(pos):start(1) end end, }) -- List of Controller actions and conditions is dependent on loaded mods. -- Therefore, the order of actions and conditions has to be re-assembled each time. -- last order from last run is stored as meta data local storage = minetest.get_mod_storage() local function old_to_new(newTypes, oldTypes) local res = {} if #oldTypes == 0 then return nil end local new = create_kv_list(newTypes) for idx,key in ipairs(oldTypes) do res[idx] = new[key] or 1 end return res end local function update_node_database(meta) local aOldCondTypes = minetest.deserialize(meta:get_string("aCondTypes")) or {} local aOldActnTypes = minetest.deserialize(meta:get_string("aActnTypes")) or {} local tOld2NewCond = old_to_new(aCondTypes, aOldCondTypes) local tOld2NewActn = old_to_new(aActnTypes, aOldActnTypes) meta:set_string("aCondTypes", minetest.serialize(aCondTypes)) meta:set_string("aActnTypes", minetest.serialize(aActnTypes)) return tOld2NewCond, tOld2NewActn end local function maintain_dataset(number) local flags = tubelib.get_data(number, "flags") if flags ~= nil then local timers = tubelib.get_data(number, "timers") or {} local inputs = tubelib.get_data(number, "inputs") or {} tubelib.set_data(number, "environ", { flags = flags, timers = timers, inputs = inputs, vars = {} }) tubelib.set_data(number, "inputs", nil) tubelib.set_data(number, "timers", nil) tubelib.set_data(number, "flags", nil) end end -- -- Update formspec data for the case that rules order has changed or new rules were added -- function smartline.update_fs_data(meta, fs_data) local tOld2NewCond, tOld2NewActn = update_node_database(meta) if tOld2NewCond and tOld2NewActn then -- map from old to new indexes for idx = 1,NUM_RULES do fs_data["subm1"..idx.."_cond"] = tOld2NewCond[fs_data["subm1"..idx.."_cond"]] fs_data["subm2"..idx.."_cond"] = tOld2NewCond[fs_data["subm2"..idx.."_cond"]] fs_data["subma"..idx.."_actn"] = tOld2NewActn[fs_data["subma"..idx.."_actn"]] end end return fs_data end minetest.register_lbm({ label = "[SmartLine] Controller update", name = "smartline:update", nodenames = {"smartline:controller"}, run_at_every_load = true, action = function(pos, node) local meta = minetest.get_meta(pos) local fs_data = minetest.deserialize(meta:get_string("fs_data")) fs_data = smartline.update_fs_data(meta, fs_data) meta:set_string("fs_data", minetest.serialize(fs_data)) local number = meta:get_string("number") local owner = meta:get_string("owner") formspec2runtime_rule(number, owner, fs_data) maintain_dataset(number) end })