-- ______ -- | -- | -- | __ ___ _ __ _ _ -- | | | | | |\ | | |_| | | | | |_ |_| -- |___| |______ |__| | \| | | \ |__| |_ |_ |_ |\ tube -- | -- | -- -- 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_inner(pos, code, event): runs code on the controller at pos and event -- reset_formspec(pos, code, errmsg): installs new code and prints error messages, without resetting LCID -- reset_meta(pos, code, errmsg): performs a software-reset, installs new code and prints error message -- run(pos, event): a wrapper for run_inner which gets code & handles errors via reset_meta -- 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 appending minetest.env to it. local BASENAME = "pipeworks:lua_tube" local rules = { red = {x = -1, y = 0, z = 0, name = "red"}, blue = {x = 1, y = 0, z = 0, name = "blue"}, yellow = {x = 0, y = -1, z = 0, name = "yellow"}, green = {x = 0, y = 1, z = 0, name = "green"}, black = {x = 0, y = 0, z = -1, name = "black"}, white = {x = 0, y = 0, z = 1, name = "white"}, } local digiline_rules_luatube = { {x=0, y=0, z=-1}, {x=1, y=0, z=0}, {x=-1, y=0, z=0}, {x=0, y=0, z=1}, {x=1, y=1, z=0}, {x=1, y=-1, z=0}, {x=-1, y=1, z=0}, {x=-1, y=-1, z=0}, {x=0, y=1, z=1}, {x=0, y=-1, z=1}, {x=0, y=1, z=-1}, {x=0, y=-1, z=-1}, -- vertical connectivity {x=0, y=1, z=0}, {x=0, y=-1, z=0}, } ------------------ -- Action stuff -- ------------------ -- These helpers are required to set the port states of the lua_tube 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, 6 do L[i] = n % 2 n = math.floor(n / 2) end -- (0,0,-1) (0,-1,0) (-1,0,0) (1,0,0) (0,1,0) (0,0,1) local pos_to_side = { 5, 3, 1, nil, 2, 4, 6 } if rule_name.x == nil then for _, rname in ipairs(rule_name) do local port = pos_to_side[rname.x + (2 * rname.y) + (3 * rname.z) + 4] L[port] = (new_state == "on") and 1 or 0 end else local port = pos_to_side[rule_name.x + (2 * rule_name.y) + (3 * rule_name.z) + 4] 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] + 16 * L[5] + 32 * L[6]) end local port_names = {"red", "blue", "yellow", "green", "black", "white"} 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 { red = ports.red or vports.red, blue = ports.blue or vports.blue, yellow = ports.yellow or vports.yellow, green = ports.green or vports.green, black = ports.black or vports.black, white = ports.white or vports.white, } end local function generate_name(ports) local red = ports.red and 1 or 0 local blue = ports.blue and 1 or 0 local yellow = ports.yellow and 1 or 0 local green = ports.green and 1 or 0 local black = ports.black and 1 or 0 local white = ports.white and 1 or 0 return BASENAME..white..black..green..yellow..blue..red 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.red = ports.red and true or false ports.blue = ports.blue and true or false ports.yellow = ports.yellow and true or false ports.green = ports.green and true or false ports.black = ports.black and true or false ports.white = ports.white 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 lua_tube 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"), true) or {} if ports.red and not vports.red and not mesecon.is_powered(pos, rules.red) then ign.red = true end if ports.blue and not vports.blue and not mesecon.is_powered(pos, rules.blue) then ign.blue = true end if ports.yellow and not vports.yellow and not mesecon.is_powered(pos, rules.yellow) then ign.yellow = true end if ports.green and not vports.green and not mesecon.is_powered(pos, rules.green) then ign.green = true end if ports.black and not vports.black and not mesecon.is_powered(pos, rules.black) then ign.black = true end if ports.white and not vports.white and not mesecon.is_powered(pos, rules.white) then ign.white = true end meta:set_string("ignore_offevents", minetest.serialize(ign)) minetest.swap_node(pos, {name = new_name, param2 = node.param2}) if ports.red ~= vports.red then set_port(pos, rules.red, ports.red) end if ports.blue ~= vports.blue then set_port(pos, rules.blue, ports.blue) end if ports.yellow ~= vports.yellow then set_port(pos, rules.yellow, ports.yellow) end if ports.green ~= vports.green then set_port(pos, rules.green, ports.green) end if ports.black ~= vports.black then set_port(pos, rules.black, ports.black) end if ports.white ~= vports.white then set_port(pos, rules.white, ports.white) 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"), true) 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) local string_meta = getmetatable("") local sandbox = string_meta.__index string_meta.__index = string -- Leave string sandbox temporarily print(dump(param)) string_meta.__index = sandbox -- Restore string sandbox 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 lua controlled tube") 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 lua controlled tube will lead to infinite recursion local seen = {} local function rfuncs(x) if x == nil then return end 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 -- The setting affects API so is not intended to be changeable at runtime local get_interrupt if mesecon.setting("luacontroller_lightweight_interrupts", false) then -- use node timer get_interrupt = function(pos, itbl, send_warning) return (function(time, iid) if type(time) ~= "number" then error("Delay must be a number") end if iid ~= nil then send_warning("Interrupt IDs are disabled on this server") end table.insert(itbl, function() minetest.get_node_timer(pos):start(time) end) end) end else -- use global action queue -- itbl: Flat table of functions to run after sandbox cleanup, used to prevent various security hazards get_interrupt = function(pos, itbl, send_warning) -- iid = interrupt id local function interrupt(time, iid) -- NOTE: This runs within string metatable sandbox, so don't *rely* on anything of the form (""):y -- Hence the values get moved out. Should take less time than original, so totally compatible if type(time) ~= "number" then error("Delay must be a number") end table.insert(itbl, function () -- Outside string metatable sandbox, can safely run this now local luac_id = minetest.get_meta(pos):get_int("luac_id") -- Check if IID is dodgy, so you can't use interrupts to store an infinite amount of data. -- Note that this is safe from alter-after-free because this code gets run after the sandbox has ended. -- This runs outside of the timer and *shouldn't* harm perf. unless dodgy data is being sent in the first place iid = remove_functions(iid) local msg_ser = minetest.serialize(iid) if #msg_ser <= mesecon.setting("luacontroller_interruptid_maxlen", 256) then mesecon.queue:add_action(pos, "pipeworks:lc_tube_interrupt", {luac_id, iid}, time, iid, 1) else send_warning("An interrupt ID was too large!") end end) end return interrupt end end -- Given a message object passed to digiline_send, clean it up into a form -- which is safe to transmit over the network and compute its "cost" (a very -- rough estimate of its memory usage). -- -- The cleaning comprises the following: -- 1. Functions (and userdata, though user scripts ought not to get hold of -- those in the first place) are removed, because they break the model of -- Digilines as a network that carries basic data, and they could exfiltrate -- references to mutable objects from one Luacontroller to another, allowing -- inappropriate high-bandwidth, no-wires communication. -- 2. Tables are duplicated because, being mutable, they could otherwise be -- modified after the send is complete in order to change what data arrives -- at the recipient, perhaps in violation of the previous cleaning rule or -- in violation of the message size limit. -- -- The cost indication is only approximate; it’s not a perfect measurement of -- the number of bytes of memory used by the message object. -- -- Parameters: -- msg -- the message to clean -- back_references -- for internal use only; do not provide -- -- Returns: -- 1. The cleaned object. -- 2. The approximate cost of the object. local function clean_and_weigh_digiline_message(msg, back_references) local t = type(msg) if t == "string" then -- Strings are immutable so can be passed by reference, and cost their -- length plus the size of the Lua object header (24 bytes on a 64-bit -- platform) plus one byte for the NUL terminator. return msg, #msg + 25 elseif t == "number" then -- Numbers are passed by value so need not be touched, and cost 8 bytes -- as all numbers in Lua are doubles. return msg, 8 elseif t == "boolean" then -- Booleans are passed by value so need not be touched, and cost 1 -- byte. return msg, 1 elseif t == "table" then -- Tables are duplicated. Check if this table has been seen before -- (self-referential or shared table); if so, reuse the cleaned value -- of the previous occurrence, maintaining table topology and avoiding -- infinite recursion, and charge zero bytes for this as the object has -- already been counted. back_references = back_references or {} local bref = back_references[msg] if bref then return bref, 0 end -- Construct a new table by cleaning all the keys and values and adding -- up their costs, plus 8 bytes as a rough estimate of table overhead. local cost = 8 local ret = {} back_references[msg] = ret for k, v in pairs(msg) do local k_cost, v_cost k, k_cost = clean_and_weigh_digiline_message(k, back_references) v, v_cost = clean_and_weigh_digiline_message(v, back_references) if k ~= nil and v ~= nil then -- Only include an element if its key and value are of legal -- types. ret[k] = v end -- If we only counted the cost of a table element when we actually -- used it, we would be vulnerable to the following attack: -- 1. Construct a huge table (too large to pass the cost limit). -- 2. Insert it somewhere in a table, with a function as a key. -- 3. Insert it somewhere in another table, with a number as a key. -- 4. The first occurrence doesn’t pay the cost because functions -- are stripped and therefore the element is dropped. -- 5. The second occurrence doesn’t pay the cost because it’s in -- back_references. -- By counting the costs regardless of whether the objects will be -- included, we avoid this attack; it may overestimate the cost of -- some messages, but only those that won’t be delivered intact -- anyway because they contain illegal object types. cost = cost + k_cost + v_cost end return ret, cost else return nil, 0 end end -- itbl: Flat table of functions to run after sandbox cleanup, used to prevent various security hazards local function get_digiline_send(pos, itbl, send_warning) if not minetest.global_exists("digilines") then return end local chan_maxlen = mesecon.setting("luacontroller_digiline_channel_maxlen", 256) local maxlen = mesecon.setting("luacontroller_digiline_maxlen", 50000) return function(channel, msg) -- NOTE: This runs within string metatable sandbox, so don't *rely* on anything of the form (""):y -- or via anything that could. -- Make sure channel is string, number or boolean if type(channel) == "string" then if #channel > chan_maxlen then send_warning("Channel string too long.") return false end elseif (type(channel) ~= "string" and type(channel) ~= "number" and type(channel) ~= "boolean") then send_warning("Channel must be string, number or boolean.") return false end local msg_cost msg, msg_cost = clean_and_weigh_digiline_message(msg) if msg == nil or msg_cost > maxlen then send_warning("Message was too complex, or contained invalid data.") return false end table.insert(itbl, function () -- Runs outside of string metatable sandbox local luac_id = minetest.get_meta(pos):get_int("luac_id") mesecon.queue:add_action(pos, "pipeworks:lt_digiline_relay", {channel, luac_id, msg}) end) return true end end local safe_globals = { -- Don't add pcall/xpcall unless willing to deal with the consequences (unless very careful, incredibly likely to allow killing server indirectly) "assert", "error", "ipairs", "next", "pairs", "select", "tonumber", "tostring", "type", "unpack", "_VERSION" } local function create_environment(pos, mem, event, itbl, send_warning) -- Make sure the tube hasn't broken. local vports = minetest.registered_nodes[minetest.get_node(pos).name].virtual_portstates if not vports then return {} end -- Gather variables for the environment 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 = mesecon.get_heat(pos), heat_max = mesecon.setting("overheat_max", 20), print = safe_print, interrupt = get_interrupt(pos, itbl, send_warning), digiline_send = get_digiline_send(pos, itbl, send_warning), 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 local maxevents = mesecon.setting("luacontroller_maxevents", 10000) return function(...) -- NOTE: This runs within string metatable sandbox, so the setting's been moved out for safety -- Use instruction counter to stop execution -- after luacontroller_maxevents 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"), true) 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) meta:mark_as_private("lc_memory") else print("Error: lua_tube memory overflow. "..memsize_max.." bytes available, " ..#memstring.." required. Controller overheats.") burn_controller(pos) end end -- Returns success (boolean), errmsg (string), retval(any, return value of the user supplied code) -- run (as opposed to run_inner) is responsible for setting up meta according to this output local function run_inner(pos, code, event) local meta = minetest.get_meta(pos) -- Note: These return success, presumably to avoid changing LC ID. if overheat(pos) then return true, "", nil end if ignore_event(event, meta) then return true, "", nil end -- Load code & mem from meta local mem = load_memory(meta) local code = meta:get_string("code") -- 'Last warning' label. local warning = "" local function send_warning(str) warning = "Warning: " .. str end -- Create environment local itbl = {} local env = create_environment(pos, mem, event, itbl, send_warning) -- Create the sandbox and execute code local f, msg = create_sandbox(code, env) if not f then return false, msg, nil end -- Start string true sandboxing local onetruestring = getmetatable("") -- If a string sandbox is already up yet inconsistent, something is very wrong assert(onetruestring.__index == string) onetruestring.__index = env.string local success, msg = pcall(f) onetruestring.__index = string -- End string true sandboxing if not success then return false, msg, nil end if type(env.port) ~= "table" then return false, "Ports set are invalid.", nil 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) -- Execute deferred tasks for _, v in ipairs(itbl) do local failure = v() if failure then return false, failure, nil end end return true, warning, msg end local function reset_formspec(meta, code, errmsg) meta:set_string("code", code) meta:mark_as_private("code") code = minetest.formspec_escape(code or "") errmsg = minetest.formspec_escape(tostring(errmsg or "")) meta:set_string("formspec", "size[12,10]" .."background[-0.2,-0.25;12.4,10.75;jeija_luac_background.png]" .."label[0.1,8.3;"..errmsg.."]" .."textarea[0.2,0.2;12.2,9.5;code;;"..code.."]" .."image_button[4.75,8.75;2.5,1;jeija_luac_runbutton.png;program;]" .."image_button_exit[11.72,-0.25;0.425,0.4;jeija_close_window.png;exit;]" ) end local function reset_meta(pos, code, errmsg) local meta = minetest.get_meta(pos) reset_formspec(meta, code, errmsg) meta:set_int("luac_id", math.random(1, 65535)) end -- Wraps run_inner with LC-reset-on-error local function run(pos, event) local meta = minetest.get_meta(pos) local code = meta:get_string("code") local ok, errmsg, retval = run_inner(pos, code, event) if not ok then reset_meta(pos, code, errmsg) else reset_formspec(meta, code, errmsg) end return ok, errmsg, retval end local function reset(pos) set_port_states(pos, {red = false, blue = false, yellow = false, green = false, black = false, white = false}) end local function node_timer(pos) if minetest.registered_nodes[minetest.get_node(pos).name].is_burnt then return false end run(pos, {type="interrupt"}) return false end ----------------------- -- A.Queue callbacks -- ----------------------- mesecon.queue:add_function("pipeworks:lc_tube_interrupt", function (pos, luac_id, iid) -- There is no lua_tube 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) mesecon.queue:add_function("pipeworks:lt_digiline_relay", function (pos, channel, luac_id, msg) if not digiline then return end -- This check is only really necessary because in case of server crash, old actions can be thrown into the future 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 -- The actual work digiline:receptor_send(pos, digiline_rules_luatube, channel, msg) end) ----------------------- -- Node Registration -- ----------------------- local output_rules = {} local input_rules = {} local node_box = { type = "fixed", fixed = { pipeworks.tube_leftstub[1], -- tube segment against -X face pipeworks.tube_rightstub[1], -- tube segment against +X face pipeworks.tube_bottomstub[1], -- tube segment against -Y face pipeworks.tube_topstub[1], -- tube segment against +Y face pipeworks.tube_frontstub[1], -- tube segment against -Z face pipeworks.tube_backstub[1], -- tube segment against +Z face } } local selection_box = { type = "fixed", fixed = pipeworks.tube_selectboxes, } local digiline = { receptor = {}, effector = { action = function(pos, node, channel, msg) msg = clean_and_weigh_digiline_message(msg) run(pos, {type = "digiline", channel = channel, msg = msg}) end }, wire = { rules = pipeworks.digilines_rules }, } local function get_program(pos) local meta = minetest.get_meta(pos) return meta:get_string("code") end local function set_program(pos, code) reset(pos) reset_meta(pos, code) return run(pos, {type="program"}) 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 local ok, err = set_program(pos, fields.code) if not ok then -- it's not an error from the server perspective minetest.log("action", "Lua controller programming error: " .. tostring(err)) end end local function go_back(velocity) local adjlist={{x=0,y=0,z=1},{x=0,y=0,z=-1},{x=0,y=1,z=0},{x=0,y=-1,z=0},{x=1,y=0,z=0},{x=-1,y=0,z=0}} local speed = math.abs(velocity.x + velocity.y + velocity.z) if speed == 0 then speed = 1 end local vel = {x = velocity.x/speed, y = velocity.y/speed, z = velocity.z/speed,speed=speed} if speed >= 4.1 then speed = 4 elseif speed >= 1.1 then speed = speed - 0.1 else speed = 1 end vel.speed = speed return pipeworks.notvel(adjlist, vel) end local tiles_base = { "pipeworks_mese_tube_plain_4.png", "pipeworks_mese_tube_plain_3.png", "pipeworks_mese_tube_plain_2.png", "pipeworks_mese_tube_plain_1.png", "pipeworks_mese_tube_plain_6.png", "pipeworks_mese_tube_plain_5.png"} for red = 0, 1 do -- 0 = off 1 = on for blue = 0, 1 do for yellow = 0, 1 do for green = 0, 1 do for black = 0, 1 do for white = 0, 1 do local cid = tostring(white)..tostring(black)..tostring(green).. tostring(yellow)..tostring(blue)..tostring(red) local node_name = BASENAME..cid local tiles = table.copy(tiles_base) if red == 1 then tiles[1] = tiles[1].."^(pipeworks_lua_tube_port_on.png^[transformR90)" tiles[2] = tiles[2].."^(pipeworks_lua_tube_port_on.png^[transformR90)" tiles[5] = tiles[5].."^(pipeworks_lua_tube_port_on.png^[transformR270)" tiles[6] = tiles[6].."^(pipeworks_lua_tube_port_on.png^[transformR90)" else tiles[1] = tiles[1].."^(pipeworks_lua_tube_port_off.png^[transformR90)" tiles[2] = tiles[2].."^(pipeworks_lua_tube_port_off.png^[transformR90)" tiles[5] = tiles[5].."^(pipeworks_lua_tube_port_off.png^[transformR270)" tiles[6] = tiles[6].."^(pipeworks_lua_tube_port_off.png^[transformR90)" end if blue == 1 then tiles[1] = tiles[1].."^(pipeworks_lua_tube_port_on.png^[transformR270)" tiles[2] = tiles[2].."^(pipeworks_lua_tube_port_on.png^[transformR270)" tiles[5] = tiles[5].."^(pipeworks_lua_tube_port_on.png^[transformR90)" tiles[6] = tiles[6].."^(pipeworks_lua_tube_port_on.png^[transformR270)" else tiles[1] = tiles[1].."^(pipeworks_lua_tube_port_off.png^[transformR270)" tiles[2] = tiles[2].."^(pipeworks_lua_tube_port_off.png^[transformR270)" tiles[5] = tiles[5].."^(pipeworks_lua_tube_port_off.png^[transformR90)" tiles[6] = tiles[6].."^(pipeworks_lua_tube_port_off.png^[transformR270)" end if yellow == 1 then tiles[3] = tiles[3].."^(pipeworks_lua_tube_port_on.png^[transformR180)" tiles[4] = tiles[4].."^(pipeworks_lua_tube_port_on.png^[transformR180)" tiles[5] = tiles[5].."^(pipeworks_lua_tube_port_on.png^[transformR180)" tiles[6] = tiles[6].."^(pipeworks_lua_tube_port_on.png^[transformR180)" else tiles[3] = tiles[3].."^(pipeworks_lua_tube_port_off.png^[transformR180)" tiles[4] = tiles[4].."^(pipeworks_lua_tube_port_off.png^[transformR180)" tiles[5] = tiles[5].."^(pipeworks_lua_tube_port_off.png^[transformR180)" tiles[6] = tiles[6].."^(pipeworks_lua_tube_port_off.png^[transformR180)" end if green == 1 then tiles[3] = tiles[3].."^pipeworks_lua_tube_port_on.png" tiles[4] = tiles[4].."^pipeworks_lua_tube_port_on.png" tiles[5] = tiles[5].."^pipeworks_lua_tube_port_on.png" tiles[6] = tiles[6].."^pipeworks_lua_tube_port_on.png" else tiles[3] = tiles[3].."^pipeworks_lua_tube_port_off.png" tiles[4] = tiles[4].."^pipeworks_lua_tube_port_off.png" tiles[5] = tiles[5].."^pipeworks_lua_tube_port_off.png" tiles[6] = tiles[6].."^pipeworks_lua_tube_port_off.png" end if black == 1 then tiles[1] = tiles[1].."^(pipeworks_lua_tube_port_on.png^[transformR180)" tiles[2] = tiles[2].."^pipeworks_lua_tube_port_on.png" tiles[3] = tiles[3].."^(pipeworks_lua_tube_port_on.png^[transformR90)" tiles[4] = tiles[4].."^(pipeworks_lua_tube_port_on.png^[transformR270)" else tiles[1] = tiles[1].."^(pipeworks_lua_tube_port_off.png^[transformR180)" tiles[2] = tiles[2].."^pipeworks_lua_tube_port_off.png" tiles[3] = tiles[3].."^(pipeworks_lua_tube_port_off.png^[transformR90)" tiles[4] = tiles[4].."^(pipeworks_lua_tube_port_off.png^[transformR270)" end if white == 1 then tiles[1] = tiles[1].."^pipeworks_lua_tube_port_on.png" tiles[2] = tiles[2].."^(pipeworks_lua_tube_port_on.png^[transformR180)" tiles[3] = tiles[3].."^(pipeworks_lua_tube_port_on.png^[transformR270)" tiles[4] = tiles[4].."^(pipeworks_lua_tube_port_on.png^[transformR90)" else tiles[1] = tiles[1].."^pipeworks_lua_tube_port_off.png" tiles[2] = tiles[2].."^(pipeworks_lua_tube_port_off.png^[transformR180)" tiles[3] = tiles[3].."^(pipeworks_lua_tube_port_off.png^[transformR270)" tiles[4] = tiles[4].."^(pipeworks_lua_tube_port_off.png^[transformR90)" end local groups = {snappy = 3, tube = 1, tubedevice = 1, overheat = 1} if red + blue + yellow + green + black + white ~= 0 then groups.not_in_creative_inventory = 1 end output_rules[cid] = {} input_rules[cid] = {} if red == 1 then table.insert(output_rules[cid], rules.red) end if blue == 1 then table.insert(output_rules[cid], rules.blue) end if yellow == 1 then table.insert(output_rules[cid], rules.yellow) end if green == 1 then table.insert(output_rules[cid], rules.green) end if black == 1 then table.insert(output_rules[cid], rules.black) end if white == 1 then table.insert(output_rules[cid], rules.white) end if red == 0 then table.insert( input_rules[cid], rules.red) end if blue == 0 then table.insert( input_rules[cid], rules.blue) end if yellow == 0 then table.insert( input_rules[cid], rules.yellow) end if green == 0 then table.insert( input_rules[cid], rules.green) end if black == 0 then table.insert( input_rules[cid], rules.black) end if white == 0 then table.insert( input_rules[cid], rules.white) 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] }, luacontroller = { get_program = get_program, set_program = set_program, }, } minetest.register_node(node_name, { description = "Lua controlled Tube", drawtype = "nodebox", tiles = tiles, paramtype = "light", is_ground_content = false, groups = groups, drop = BASENAME.."000000", 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_wood_defaults(), mesecons = mesecons, digiline = digiline, -- Virtual portstates are the ports that -- the node shows as powered up (light up). virtual_portstates = { red = red == 1, blue = blue == 1, yellow = yellow == 1, green = green == 1, black = black == 1, white = white == 1, }, after_dig_node = function(pos, node) mesecon.do_cooldown(pos) mesecon.receptor_off(pos, output_rules) pipeworks.after_dig(pos, node) end, is_luacontroller = true, on_timer = node_timer, tubelike = 1, tube = { connect_sides = {front = 1, back = 1, left = 1, right = 1, top = 1, bottom = 1}, priority = 50, can_go = function(pos, node, velocity, stack) local src = {name = nil} -- add color of the incoming tube explicitly; referring to rules, in case they change later for color, rule in pairs(rules) do if (-velocity.x == rule.x and -velocity.y == rule.y and -velocity.z == rule.z) then src.name = rule.name break end end local succ, _, msg = run(pos, { type = "item", pin = src, itemstring = stack:to_string(), item = stack:to_table(), velocity = velocity, }) if not succ or type(msg) ~= "string" then return go_back(velocity) end local r = rules[msg] return r and {r} or go_back(velocity) end, }, after_place_node = pipeworks.after_place, on_blast = function(pos, intensity) if not intensity or intensity > 1 + 3^0.5 then minetest.remove_node(pos) return {string.format("%s_%s", name, dropname)} end minetest.swap_node(pos, {name = "pipeworks:broken_tube_1"}) pipeworks.scan_for_tube_objects(pos) end, }) end end end end end end ------------------------------------ -- Overheated Lua controlled Tube -- ------------------------------------ local tiles_burnt = table.copy(tiles_base) tiles_burnt[1] = tiles_burnt[1].."^(pipeworks_lua_tube_port_burnt.png^[transformR90)" tiles_burnt[2] = tiles_burnt[2].."^(pipeworks_lua_tube_port_burnt.png^[transformR90)" tiles_burnt[5] = tiles_burnt[5].."^(pipeworks_lua_tube_port_burnt.png^[transformR270)" tiles_burnt[6] = tiles_burnt[6].."^(pipeworks_lua_tube_port_burnt.png^[transformR90)" tiles_burnt[1] = tiles_burnt[1].."^(pipeworks_lua_tube_port_burnt.png^[transformR270)" tiles_burnt[2] = tiles_burnt[2].."^(pipeworks_lua_tube_port_burnt.png^[transformR270)" tiles_burnt[5] = tiles_burnt[5].."^(pipeworks_lua_tube_port_burnt.png^[transformR90)" tiles_burnt[6] = tiles_burnt[6].."^(pipeworks_lua_tube_port_burnt.png^[transformR270)" tiles_burnt[3] = tiles_burnt[3].."^(pipeworks_lua_tube_port_burnt.png^[transformR180)" tiles_burnt[4] = tiles_burnt[4].."^(pipeworks_lua_tube_port_burnt.png^[transformR180)" tiles_burnt[5] = tiles_burnt[5].."^(pipeworks_lua_tube_port_burnt.png^[transformR180)" tiles_burnt[6] = tiles_burnt[6].."^(pipeworks_lua_tube_port_burnt.png^[transformR180)" tiles_burnt[3] = tiles_burnt[3].."^pipeworks_lua_tube_port_burnt.png" tiles_burnt[4] = tiles_burnt[4].."^pipeworks_lua_tube_port_burnt.png" tiles_burnt[5] = tiles_burnt[5].."^pipeworks_lua_tube_port_burnt.png" tiles_burnt[6] = tiles_burnt[6].."^pipeworks_lua_tube_port_burnt.png" tiles_burnt[1] = tiles_burnt[1].."^(pipeworks_lua_tube_port_burnt.png^[transformR180)" tiles_burnt[2] = tiles_burnt[2].."^pipeworks_lua_tube_port_burnt.png" tiles_burnt[3] = tiles_burnt[3].."^(pipeworks_lua_tube_port_burnt.png^[transformR90)" tiles_burnt[4] = tiles_burnt[4].."^(pipeworks_lua_tube_port_burnt.png^[transformR270)" tiles_burnt[1] = tiles_burnt[1].."^pipeworks_lua_tube_port_burnt.png" tiles_burnt[2] = tiles_burnt[2].."^(pipeworks_lua_tube_port_burnt.png^[transformR180)" tiles_burnt[3] = tiles_burnt[3].."^(pipeworks_lua_tube_port_burnt.png^[transformR270)" tiles_burnt[4] = tiles_burnt[4].."^(pipeworks_lua_tube_port_burnt.png^[transformR90)" minetest.register_node(BASENAME .. "_burnt", { drawtype = "nodebox", tiles = tiles_burnt, is_burnt = true, paramtype = "light", is_ground_content = false, groups = {snappy = 3, tube = 1, tubedevice = 1, not_in_creative_inventory=1}, drop = BASENAME.."000000", 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_wood_defaults(), virtual_portstates = {red = false, blue = false, yellow = false, green = false, black = false, white = false}, mesecons = { effector = { rules = mesecon.rules.alldirs, action_change = function(pos, _, rule_name, new_state) update_real_port_states(pos, rule_name, new_state) end, }, }, tubelike = 1, tube = { connect_sides = {front = 1, back = 1, left = 1, right = 1, top = 1, bottom = 1}, priority = 50, }, after_place_node = pipeworks.after_place, after_dig_node = pipeworks.after_dig, on_blast = function(pos, intensity) if not intensity or intensity > 1 + 3^0.5 then minetest.remove_node(pos) return {string.format("%s_%s", name, dropname)} end minetest.swap_node(pos, {name = "pipeworks:broken_tube_1"}) pipeworks.scan_for_tube_objects(pos) end, }) ------------------------ -- Craft Registration -- ------------------------ minetest.register_craft({ type = "shapeless", output = BASENAME.."000000", recipe = {"pipeworks:mese_tube_000000", "mesecons_luacontroller:luacontroller0000"}, })