-- ______ -- | -- | -- | __ ___ _ __ _ _ -- | | | | | |\ | | |_| | | | | |_ |_| -- |___| |______ |__| | \| | | \ |__| |_ |_ |_ |\ -- | -- | -- -- Reference -- ports = get_real_port_states(pos): gets if inputs are powered from outside -- newport = merge_port_states(state1, state2): just does result = state1 or state2 for every port -- set_port(pos, rule, state): activates/deactivates the mesecons according to the port states -- set_port_states(pos, ports): Applies new port states to a Luacontroller at pos -- run(pos): runs the code in the controller at pos -- reset_meta(pos, code, errmsg): performs a software-reset, installs new code and prints error messages -- resetn(pos): performs a hardware reset, turns off all ports -- -- The Sandbox -- The whole code of the controller runs in a sandbox, -- a very restricted environment. -- Actually the only way to damage the server is to -- use too much memory from the sandbox. -- You can add more functions to the environment -- (see where local env is defined) -- Something nice to play is is appending minetest.env to it. local BASENAME = "mesecons_luacontroller:luacontroller" local rules = { a = {x = -1, y = 0, z = 0, name="A"}, b = {x = 0, y = 0, z = 1, name="B"}, c = {x = 1, y = 0, z = 0, name="C"}, d = {x = 0, y = 0, z = -1, name="D"}, } ------------------ -- Action stuff -- ------------------ -- These helpers are required to set the port states of the luacontroller local function update_real_port_states(pos, rule_name, new_state) local meta = minetest.get_meta(pos) if rule_name == nil then meta:set_int("real_portstates", 1) return end local n = meta:get_int("real_portstates") - 1 local L = {} for i = 1, 4 do L[i] = n % 2 n = math.floor(n / 2) end -- (0,-1) (-1,0) (1,0) (0,1) local pos_to_side = { 4, 1, nil, 3, 2 } if rule_name.x == nil then for _, rname in ipairs(rule_name) do local port = pos_to_side[rname.x + (2 * rname.z) + 3] L[port] = (newstate == "on") and 1 or 0 end else local port = pos_to_side[rule_name.x + (2 * rule_name.z) + 3] L[port] = (new_state == "on") and 1 or 0 end meta:set_int("real_portstates", 1 + 1 * L[1] + 2 * L[2] + 4 * L[3] + 8 * L[4]) end local port_names = {"a", "b", "c", "d"} local function get_real_port_states(pos) -- Determine if ports are powered (by itself or from outside) local meta = minetest.get_meta(pos) local L = {} local n = meta:get_int("real_portstates") - 1 for _, name in ipairs(port_names) do L[name] = ((n % 2) == 1) n = math.floor(n / 2) end return L end local function merge_port_states(ports, vports) return { a = ports.a or vports.a, b = ports.b or vports.b, c = ports.c or vports.c, d = ports.d or vports.d, } end local function generate_name(ports) local d = ports.d and 1 or 0 local c = ports.c and 1 or 0 local b = ports.b and 1 or 0 local a = ports.a and 1 or 0 return BASENAME..d..c..b..a end local function set_port(pos, rule, state) if state then mesecon.receptor_on(pos, {rule}) else mesecon.receptor_off(pos, {rule}) end end local function clean_port_states(ports) ports.a = ports.a and true or false ports.b = ports.b and true or false ports.c = ports.c and true or false ports.d = ports.d and true or false end local function set_port_states(pos, ports) local node = minetest.get_node(pos) local name = node.name clean_port_states(ports) local vports = minetest.registered_nodes[name].virtual_portstates local new_name = generate_name(ports) if name ~= new_name and vports then -- Problem: -- We need to place the new node first so that when turning -- off some port, it won't stay on because the rules indicate -- there is an onstate output port there. -- When turning the output off then, it will however cause feedback -- so that the luacontroller will receive an "off" event by turning -- its output off. -- Solution / Workaround: -- Remember which output was turned off and ignore next "off" event. local meta = minetest.get_meta(pos) local ign = minetest.deserialize(meta:get_string("ignore_offevents")) or {} if ports.a and not vports.a and not mesecon.is_powered(pos, rules.a) then ign.A = true end if ports.b and not vports.b and not mesecon.is_powered(pos, rules.b) then ign.B = true end if ports.c and not vports.c and not mesecon.is_powered(pos, rules.c) then ign.C = true end if ports.d and not vports.d and not mesecon.is_powered(pos, rules.d) then ign.D = true end meta:set_string("ignore_offevents", minetest.serialize(ign)) minetest.swap_node(pos, {name = new_name, param2 = node.param2}) if ports.a ~= vports.a then set_port(pos, rules.a, ports.a) end if ports.b ~= vports.b then set_port(pos, rules.b, ports.b) end if ports.c ~= vports.c then set_port(pos, rules.c, ports.c) end if ports.d ~= vports.d then set_port(pos, rules.d, ports.d) end end end ----------------- -- Overheating -- ----------------- local function burn_controller(pos) local node = minetest.get_node(pos) node.name = BASENAME.."_burnt" minetest.swap_node(pos, node) minetest.get_meta(pos):set_string("lc_memory", ""); -- Wait for pending operations minetest.after(0.2, mesecon.receptor_off, pos, mesecon.rules.flat) end local function overheat(pos, meta) if mesecon.do_overheat(pos) then -- If too hot burn_controller(pos) return true end end ------------------------ -- Ignored off events -- ------------------------ local function ignore_event(event, meta) if event.type ~= "off" then return false end local ignore_offevents = minetest.deserialize(meta:get_string("ignore_offevents")) or {} if ignore_offevents[event.pin.name] then ignore_offevents[event.pin.name] = nil meta:set_string("ignore_offevents", minetest.serialize(ignore_offevents)) return true end end ------------------------- -- Parsing and running -- ------------------------- local function safe_print(param) print(dump(param)) end local function safe_date() return(os.date("*t",os.time())) end -- string.rep(str, n) with a high value for n can be used to DoS -- the server. Therefore, limit max. length of generated string. local function safe_string_rep(str, n) if #str * n > mesecon.setting("luacontroller_string_rep_max", 64000) then debug.sethook() -- Clear hook error("string.rep: string length overflow", 2) end return string.rep(str, n) end -- string.find with a pattern can be used to DoS the server. -- Therefore, limit string.find to patternless matching. local function safe_string_find(...) if (select(4, ...)) ~= true then debug.sethook() -- Clear hook error("string.find: 'plain' (fourth parameter) must always be true in a Luacontroller") end return string.find(...) end local function remove_functions(x) local tp = type(x) if tp == "function" then return nil end -- Make sure to not serialize the same table multiple times, otherwise -- writing mem.test = mem in the Luacontroller will lead to infinite recursion local seen = {} local function rfuncs(x) if seen[x] then return end seen[x] = true if type(x) ~= "table" then return end for key, value in pairs(x) do if type(key) == "function" or type(value) == "function" then x[key] = nil else if type(key) == "table" then rfuncs(key) end if type(value) == "table" then rfuncs(value) end end end end rfuncs(x) return x end local function get_interrupt(pos) -- iid = interrupt id local function interrupt(time, iid) if type(time) ~= "number" then return end local luac_id = minetest.get_meta(pos):get_int("luac_id") mesecon.queue:add_action(pos, "lc_interrupt", {luac_id, iid}, time, iid, 1) end return interrupt end local function get_digiline_send(pos) if not digiline then return end return function(channel, msg) -- Make sure channel is string, number or boolean if (type(channel) ~= "string" and type(channel) ~= "number" and type(channel) ~= "boolean") then return false end -- It is technically possible to send functions over the wire since -- the high performance impact of stripping those from the data has -- been decided to not be worth the added realism. -- Make sure serialized version of the data is not insanely long to -- prevent DoS-like attacks local msg_ser = minetest.serialize(msg) if #msg_ser > mesecon.setting("luacontroller_digiline_maxlen", 50000) then return false end minetest.after(0, function() digiline:receptor_send(pos, digiline.rules.default, channel, msg) end) return true end end local safe_globals = { "assert", "error", "ipairs", "next", "pairs", "select", "tonumber", "tostring", "type", "unpack", "_VERSION" } local function create_environment(pos, mem, event) -- Gather variables for the environment local vports = minetest.registered_nodes[minetest.get_node(pos).name].virtual_portstates local vports_copy = {} for k, v in pairs(vports) do vports_copy[k] = v end local rports = get_real_port_states(pos) -- Create new library tables on each call to prevent one Luacontroller -- from breaking a library and messing up other Luacontrollers. local env = { pin = merge_port_states(vports, rports), port = vports_copy, event = event, mem = mem, heat = minetest.get_meta(pos):get_int("heat"), heat_max = mesecon.setting("overheat_max", 20), print = safe_print, interrupt = get_interrupt(pos), digiline_send = get_digiline_send(pos), string = { byte = string.byte, char = string.char, format = string.format, len = string.len, lower = string.lower, upper = string.upper, rep = safe_string_rep, reverse = string.reverse, sub = string.sub, find = safe_string_find, }, math = { abs = math.abs, acos = math.acos, asin = math.asin, atan = math.atan, atan2 = math.atan2, ceil = math.ceil, cos = math.cos, cosh = math.cosh, deg = math.deg, exp = math.exp, floor = math.floor, fmod = math.fmod, frexp = math.frexp, huge = math.huge, ldexp = math.ldexp, log = math.log, log10 = math.log10, max = math.max, min = math.min, modf = math.modf, pi = math.pi, pow = math.pow, rad = math.rad, random = math.random, sin = math.sin, sinh = math.sinh, sqrt = math.sqrt, tan = math.tan, tanh = math.tanh, }, table = { concat = table.concat, insert = table.insert, maxn = table.maxn, remove = table.remove, sort = table.sort, }, os = { clock = os.clock, difftime = os.difftime, time = os.time, datetable = safe_date, }, } env._G = env for _, name in pairs(safe_globals) do env[name] = _G[name] end return env end local function timeout() debug.sethook() -- Clear hook error("Code timed out!", 2) end local function create_sandbox(code, env) if code:byte(1) == 27 then return nil, "Binary code prohibited." end local f, msg = loadstring(code) if not f then return nil, msg end setfenv(f, env) -- Turn off JIT optimization for user code so that count -- events are generated when adding debug hooks if rawget(_G, "jit") then jit.off(f, true) end return function(...) -- Use instruction counter to stop execution -- after luacontroller_maxevents local maxevents = mesecon.setting("luacontroller_maxevents", 10000) debug.sethook(timeout, "", maxevents) local ok, ret = pcall(f, ...) debug.sethook() -- Clear hook if not ok then error(ret, 0) end return ret end end local function load_memory(meta) return minetest.deserialize(meta:get_string("lc_memory")) or {} end local function save_memory(pos, meta, mem) local memstring = minetest.serialize(remove_functions(mem)) local memsize_max = mesecon.setting("luacontroller_memsize", 100000) if (#memstring <= memsize_max) then meta:set_string("lc_memory", memstring) else print("Error: Luacontroller memory overflow. "..memsize_max.." bytes available, " ..#memstring.." required. Controller overheats.") burn_controller(pos) end end local function run(pos, event) local meta = minetest.get_meta(pos) if overheat(pos) then return end if ignore_event(event, meta) then return end -- Load code & mem from meta local mem = load_memory(meta) local code = meta:get_string("code") -- Create environment local env = create_environment(pos, mem, event) -- Create the sandbox and execute code local f, msg = create_sandbox(code, env) if not f then return msg end local success, msg = pcall(f) if not success then return msg end if type(env.port) ~= "table" then return "Ports set are invalid." end -- Actually set the ports set_port_states(pos, env.port) -- Save memory. This may burn the luacontroller if a memory overflow occurs. save_memory(pos, meta, env.mem) end mesecon.queue:add_function("lc_interrupt", function (pos, luac_id, iid) -- There is no luacontroller anymore / it has been reprogrammed / replaced / burnt if (minetest.get_meta(pos):get_int("luac_id") ~= luac_id) then return end if (minetest.registered_nodes[minetest.get_node(pos).name].is_burnt) then return end run(pos, {type="interrupt", iid = iid}) end) local function reset_meta(pos, code, errmsg) local meta = minetest.get_meta(pos) meta:set_string("code", code) code = minetest.formspec_escape(code or "") errmsg = minetest.formspec_escape(errmsg or "") meta:set_string("formspec", "size[10,8]".. "background[-0.2,-0.25;10.4,8.75;jeija_luac_background.png]".. "textarea[0.2,0.6;10.2,5;code;;"..code.."]".. "image_button[3.75,6;2.5,1;jeija_luac_runbutton.png;program;]".. "image_button_exit[9.72,-0.25;0.425,0.4;jeija_close_window.png;exit;]".. "label[0.1,5;"..errmsg.."]") meta:set_int("heat", 0) meta:set_int("luac_id", math.random(1, 65535)) end local function reset(pos) set_port_states(pos, {a=false, b=false, c=false, d=false}) end ----------------------- -- Node Registration -- ----------------------- local output_rules = {} local input_rules = {} local node_box = { type = "fixed", fixed = { {-8/16, -8/16, -8/16, 8/16, -7/16, 8/16}, -- Bottom slab {-5/16, -7/16, -5/16, 5/16, -6/16, 5/16}, -- Circuit board {-3/16, -6/16, -3/16, 3/16, -5/16, 3/16}, -- IC } } local selection_box = { type = "fixed", fixed = { -8/16, -8/16, -8/16, 8/16, -5/16, 8/16 }, } local digiline = { receptor = {}, effector = { action = function(pos, node, channel, msg) run(pos, {type = "digiline", channel = channel, msg = msg}) end } } local function on_receive_fields(pos, form_name, fields, sender) if not fields.program then return end local name = sender:get_player_name() if minetest.is_protected(pos, name) and not minetest.check_player_privs(name, {protection_bypass=true}) then minetest.record_protection_violation(pos, name) return end reset(pos) reset_meta(pos, fields.code) local err = run(pos, {type="program"}) if err then print(err) reset_meta(pos, fields.code, err) end end for a = 0, 1 do -- 0 = off 1 = on for b = 0, 1 do for c = 0, 1 do for d = 0, 1 do local cid = tostring(d)..tostring(c)..tostring(b)..tostring(a) local node_name = BASENAME..cid local top = "jeija_luacontroller_top.png" if a == 1 then top = top.."^jeija_luacontroller_LED_A.png" end if b == 1 then top = top.."^jeija_luacontroller_LED_B.png" end if c == 1 then top = top.."^jeija_luacontroller_LED_C.png" end if d == 1 then top = top.."^jeija_luacontroller_LED_D.png" end local groups if a + b + c + d ~= 0 then groups = {dig_immediate=2, not_in_creative_inventory=1, overheat = 1} else groups = {dig_immediate=2, overheat = 1} end output_rules[cid] = {} input_rules[cid] = {} if a == 1 then table.insert(output_rules[cid], rules.a) end if b == 1 then table.insert(output_rules[cid], rules.b) end if c == 1 then table.insert(output_rules[cid], rules.c) end if d == 1 then table.insert(output_rules[cid], rules.d) end if a == 0 then table.insert( input_rules[cid], rules.a) end if b == 0 then table.insert( input_rules[cid], rules.b) end if c == 0 then table.insert( input_rules[cid], rules.c) end if d == 0 then table.insert( input_rules[cid], rules.d) end local mesecons = { effector = { rules = input_rules[cid], action_change = function (pos, _, rule_name, new_state) update_real_port_states(pos, rule_name, new_state) run(pos, {type=new_state, pin=rule_name}) end, }, receptor = { state = mesecon.state.on, rules = output_rules[cid] } } minetest.register_node(node_name, { description = "Luacontroller", drawtype = "nodebox", tiles = { top, "jeija_microcontroller_bottom.png", "jeija_microcontroller_sides.png", "jeija_microcontroller_sides.png", "jeija_microcontroller_sides.png", "jeija_microcontroller_sides.png" }, inventory_image = top, paramtype = "light", groups = groups, drop = BASENAME.."0000", sunlight_propagates = true, selection_box = selection_box, node_box = node_box, on_construct = reset_meta, on_receive_fields = on_receive_fields, sounds = default.node_sound_stone_defaults(), mesecons = mesecons, digiline = digiline, -- Virtual portstates are the ports that -- the node shows as powered up (light up). virtual_portstates = { a = a == 1, b = b == 1, c = c == 1, d = d == 1, }, after_dig_node = function (pos, node) mesecon.receptor_off(pos, output_rules) end, is_luacontroller = true, }) end end end end ------------------------------ -- Overheated Luacontroller -- ------------------------------ minetest.register_node(BASENAME .. "_burnt", { drawtype = "nodebox", tiles = { "jeija_luacontroller_burnt_top.png", "jeija_microcontroller_bottom.png", "jeija_microcontroller_sides.png", "jeija_microcontroller_sides.png", "jeija_microcontroller_sides.png", "jeija_microcontroller_sides.png" }, inventory_image = "jeija_luacontroller_burnt_top.png", is_burnt = true, paramtype = "light", groups = {dig_immediate=2, not_in_creative_inventory=1}, drop = BASENAME.."0000", sunlight_propagates = true, selection_box = selection_box, node_box = node_box, on_construct = reset_meta, on_receive_fields = on_receive_fields, sounds = default.node_sound_stone_defaults(), virtual_portstates = {a = false, b = false, c = false, d = false}, mesecons = { effector = { rules = mesecon.rules.flat, action_change = function(pos, _, rule_name, new_state) update_real_port_states(pos, rule_name, new_state) end, }, }, }) ------------------------ -- Craft Registration -- ------------------------ minetest.register_craft({ output = BASENAME.."0000 2", recipe = { {'mesecons_materials:silicon', 'mesecons_materials:silicon', 'group:mesecon_conductor_craftable'}, {'mesecons_materials:silicon', 'mesecons_materials:silicon', 'group:mesecon_conductor_craftable'}, {'group:mesecon_conductor_craftable', 'group:mesecon_conductor_craftable', ''}, } })