123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726 |
- -- ______
- -- |
- -- |
- -- | __ ___ _ __ _ _
- -- | | | | | |\ | | |_| | | | | |_ |_|
- -- |___| |______ |__| | \| | | \ |__| |_ |_ |_ |\
- -- |
- -- |
- --
- -- 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.
- -- However, as this does not prevent you from using e.g. loops,
- -- we need to check for these prohibited commands first.
- -- 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
- error("string.rep: string length overflow", 2)
- end
- return string.rep(str, n)
- end
- local function remove_functions(x)
- local tp = type(x)
- if tp == "table" then
- for key, value in pairs(x) do
- local key_t, val_t = type(key), type(value)
- if key_t == "function" or val_t == "function" then
- x[key] = nil
- else
- if key_t == "table" then
- remove_functions(key)
- end
- if val_t == "table" then
- remove_functions(value)
- end
- end
- end
- elseif tp == "function" then
- return nil
- end
- 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)
- minetest.after(0, function()
- digiline:receptor_send(pos, digiline.rules.default, channel, msg)
- end)
- 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,
- },
- 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()
- error("Code timed out!", 2)
- end
- local function strip_squarebrackets(code, pos)
- if code:byte(pos) ~= 91 then return end -- [
- pos = pos + 1
- local start = pos
- while code:byte(pos) == 61 do -- =
- pos = pos + 1
- end
- if code:byte(pos) ~= 91 then return end -- [
- local _, last = code:find('%]'..string.rep('=', pos-start)..'%]', pos + 1)
- last = last or #code
- return code:sub(1, start - 1) .. " " .. code:sub(last + 1, #code)
- end
- local function strip_strings_and_comments(code)
- local i = 1
- local len = #code
- local start, new
- while i <= len do
- local c = code:byte(i)
- if c == 34 or c == 39 then
- -- " or '
- start = i
- i = i + 1
- while i <= len and code:byte(i) ~= c do
- if code:byte(i) == 92 then i = i + 1 end -- \
- i = i + 1
- end
- code = code:sub(1, start - 1) .. " " .. code:sub(i + 1, #code)
- i = start
- len = #code
- elseif c == 91 then
- -- [=...[ ... ]=...]
- new = strip_squarebrackets(code, i)
- if new then
- code = new
- len = #code
- else
- i = i + 1
- end
- elseif c == 45 and code:byte(i + 1) == 45 then
- -- --
- i = i + 2
- new = strip_squarebrackets(code, i)
- if new then
- code = new
- else
- start = code:find("[\r\n]", i) or #code
- code = code:sub(1, i - 1) .. code:sub(start + 1, #code)
- end
- len = #code
- else
- i = i + 1
- end
- end
- return code
- end
- local function code_prohibited(code)
- -- LuaJIT doesn't increment the instruction counter when running
- -- loops, so we have to sanitize inputs if we're using LuaJIT.
- if not rawget(_G, "jit") then
- return false
- end
- local prohibited = {"while", "for", "repeat", "until", "goto"}
- code = " "..strip_strings_and_comments(code).." "
- for _, p in ipairs(prohibited) do
- if string.find(code, "[^%w_]"..p.."[^%w_]") then
- return "Prohibited command: "..p
- end
- end
- 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)
- return function(...)
- -- Normal Lua: Use instruction counter to stop execution
- -- after luacontroller_maxevents.
- -- LuaJIT: Count function calls instead of instructions, allows usage
- -- of function keyword. However, LuaJIT still doesn't trigger
- -- lines events when using infinite loops.
- local maxevents = mesecon.setting("luacontroller_maxevents", 10000)
- if not rawget(_G, "jit") then
- debug.sethook(timeout, "", maxevents)
- else
- local events = 0
- debug.sethook(function ()
- events = events + 1
- if events > maxevents then
- timeout()
- end
- end, "c")
- end
- 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")
- local err = code_prohibited(code)
- if err then return err end
- -- 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)
- if not fields.program then
- 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', ''},
- }
- })
|