2014-08-07 04:54:17 +02:00
-- ______
-- |
-- |
-- | __ ___ _ __ _ _
-- | | | | | |\ | | |_| | | | | |_ |_|
-- |___| |______ |__| | \| | | \ |__| |_ |_ |_ |\
-- |
-- |
--
2013-01-13 00:18:25 +01:00
-- Reference
2014-08-07 04:54:17 +02:00
-- 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
2017-02-15 19:52:29 +01:00
-- set_port_states(pos, ports): Applies new port states to a Luacontroller at pos
2018-01-22 12:26:21 +01:00
-- 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
2014-08-07 04:54:17 +02:00
-- resetn(pos): performs a hardware reset, turns off all ports
2013-01-13 00:18:25 +01:00
--
-- 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 "
2014-08-07 04:54:17 +02:00
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 " } ,
}
2013-01-13 00:18:25 +01:00
2013-01-13 11:05:04 +01:00
------------------
-- Action stuff --
------------------
2014-08-07 04:54:17 +02:00
-- These helpers are required to set the port states of the luacontroller
2013-01-13 11:05:04 +01:00
2014-08-07 04:54:17 +02:00
local function update_real_port_states ( pos , rule_name , new_state )
2014-01-10 16:38:02 +01:00
local meta = minetest.get_meta ( pos )
2014-08-07 04:54:17 +02:00
if rule_name == nil then
2014-01-10 16:38:02 +01:00
meta : set_int ( " real_portstates " , 1 )
return
end
local n = meta : get_int ( " real_portstates " ) - 1
local L = { }
for i = 1 , 4 do
2014-08-07 04:54:17 +02:00
L [ i ] = n % 2
n = math.floor ( n / 2 )
2014-01-10 16:38:02 +01:00
end
2014-08-07 04:54:17 +02:00
-- (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 ]
2014-01-10 16:38:02 +01:00
L [ port ] = ( newstate == " on " ) and 1 or 0
end
else
2014-08-07 04:54:17 +02:00
local port = pos_to_side [ rule_name.x + ( 2 * rule_name.z ) + 3 ]
L [ port ] = ( new_state == " on " ) and 1 or 0
2014-01-10 16:38:02 +01:00
end
2014-08-07 04:54:17 +02:00
meta : set_int ( " real_portstates " ,
1 +
1 * L [ 1 ] +
2 * L [ 2 ] +
4 * L [ 3 ] +
8 * L [ 4 ] )
2014-01-10 16:38:02 +01:00
end
2014-08-07 04:54:17 +02:00
local port_names = { " a " , " b " , " c " , " d " }
local function get_real_port_states ( pos )
-- Determine if ports are powered (by itself or from outside)
2014-01-10 16:38:02 +01:00
local meta = minetest.get_meta ( pos )
local L = { }
local n = meta : get_int ( " real_portstates " ) - 1
2014-08-07 04:54:17 +02:00
for _ , name in ipairs ( port_names ) do
L [ name ] = ( ( n % 2 ) == 1 )
n = math.floor ( n / 2 )
2014-01-10 16:38:02 +01:00
end
return L
2013-01-13 00:18:25 +01:00
end
2014-08-07 04:54:17 +02:00
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 ,
}
2013-01-13 00:18:25 +01:00
end
2014-08-07 04:54:17 +02:00
local function generate_name ( ports )
2013-04-01 22:24:01 +02:00
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
2013-01-20 17:48:43 +01:00
return BASENAME .. d .. c .. b .. a
2013-01-13 00:18:25 +01:00
end
2014-08-07 04:54:17 +02:00
local function set_port ( pos , rule , state )
2013-01-22 18:26:27 +01:00
if state then
2014-11-22 15:42:22 +01:00
mesecon.receptor_on ( pos , { rule } )
2013-01-22 18:26:27 +01:00
else
2014-11-22 15:42:22 +01:00
mesecon.receptor_off ( pos , { rule } )
2013-01-22 18:26:27 +01:00
end
end
2014-08-07 04:54:17 +02:00
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 )
2013-12-01 02:20:01 +01:00
local node = minetest.get_node ( pos )
local name = node.name
2014-08-07 04:54:17 +02:00
clean_port_states ( ports )
2013-01-13 17:33:16 +01:00
local vports = minetest.registered_nodes [ name ] . virtual_portstates
2014-08-07 04:54:17 +02:00
local new_name = generate_name ( ports )
2013-01-22 18:26:27 +01:00
2014-08-07 04:54:17 +02:00
if name ~= new_name and vports then
2014-11-29 10:56:09 +01:00
-- 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 )
2018-01-22 12:26:21 +01:00
local ign = minetest.deserialize ( meta : get_string ( " ignore_offevents " ) , true ) or { }
2014-11-29 10:56:09 +01:00
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 ) )
2014-08-07 04:54:17 +02:00
minetest.swap_node ( pos , { name = new_name , param2 = node.param2 } )
2013-01-13 00:18:25 +01:00
2014-08-07 04:54:17 +02:00
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
2013-04-01 22:24:01 +02:00
end
2013-02-10 23:08:59 +01:00
end
2013-01-13 11:05:04 +01:00
2014-08-07 04:54:17 +02:00
-----------------
-- Overheating --
-----------------
2015-10-18 11:28:35 +02:00
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 )
2013-01-13 00:18:25 +01:00
end
2014-08-07 04:54:17 +02:00
local function overheat ( pos , meta )
if mesecon.do_overheat ( pos ) then -- If too hot
2015-10-18 11:28:35 +02:00
burn_controller ( pos )
2014-08-07 04:54:17 +02:00
return true
2013-01-13 00:18:25 +01:00
end
2013-01-13 11:05:04 +01:00
end
2014-11-29 10:56:09 +01:00
------------------------
-- Ignored off events --
------------------------
local function ignore_event ( event , meta )
if event.type ~= " off " then return false end
2018-01-22 12:26:21 +01:00
local ignore_offevents = minetest.deserialize ( meta : get_string ( " ignore_offevents " ) , true ) or { }
2014-11-29 10:56:09 +01:00
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
2013-01-13 00:18:25 +01:00
2014-08-07 04:54:17 +02:00
-------------------------
-- Parsing and running --
-------------------------
local function safe_print ( param )
2018-07-23 14:53:32 +02:00
local string_meta = getmetatable ( " " )
local sandbox = string_meta.__index
string_meta.__index = string -- Leave string sandbox temporarily
2013-01-13 11:05:04 +01:00
print ( dump ( param ) )
2018-07-23 14:53:32 +02:00
string_meta.__index = sandbox -- Restore string sandbox
2013-01-13 11:05:04 +01:00
end
2013-01-13 00:18:25 +01:00
2016-01-02 08:13:38 +01:00
local function safe_date ( )
return ( os.date ( " *t " , os.time ( ) ) )
end
2016-03-13 22:01:46 +01:00
-- 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
2016-03-14 12:51:57 +01:00
debug.sethook ( ) -- Clear hook
2016-03-13 22:01:46 +01:00
error ( " string.rep: string length overflow " , 2 )
end
return string.rep ( str , n )
end
2016-03-14 14:29:34 +01:00
-- 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
2017-02-15 19:52:29 +01:00
error ( " string.find: 'plain' (fourth parameter) must always be true in a Luacontroller " )
2016-03-14 14:29:34 +01:00
end
return string.find ( ... )
end
2014-08-07 04:54:17 +02:00
local function remove_functions ( x )
local tp = type ( x )
2016-04-21 00:09:38 +02:00
if tp == " function " then
return nil
end
-- Make sure to not serialize the same table multiple times, otherwise
2017-02-15 19:52:29 +01:00
-- writing mem.test = mem in the Luacontroller will lead to infinite recursion
2016-04-21 00:09:38 +02:00
local seen = { }
local function rfuncs ( x )
2018-01-22 12:26:21 +01:00
if x == nil then return end
2016-04-21 00:09:38 +02:00
if seen [ x ] then return end
seen [ x ] = true
if type ( x ) ~= " table " then return end
2014-08-07 04:54:17 +02:00
for key , value in pairs ( x ) do
2016-04-21 00:09:38 +02:00
if type ( key ) == " function " or type ( value ) == " function " then
2014-08-07 04:54:17 +02:00
x [ key ] = nil
else
2016-04-21 00:09:38 +02:00
if type ( key ) == " table " then
rfuncs ( key )
2014-08-07 04:54:17 +02:00
end
2016-04-21 00:09:38 +02:00
if type ( value ) == " table " then
rfuncs ( value )
2014-08-07 04:54:17 +02:00
end
end
2013-06-06 22:38:40 +02:00
end
end
2016-04-21 00:09:38 +02:00
rfuncs ( x )
2014-08-07 04:54:17 +02:00
return x
2013-06-06 22:38:40 +02:00
end
2020-09-21 21:32:25 +02:00
local function validate_iid ( iid )
if not iid then return true end -- nil is OK
local limit = mesecon.setting ( " luacontroller_interruptid_maxlen " , 256 )
if type ( iid ) == " string " then
if # iid <= limit then return true end -- string is OK unless too long
return false , " An interrupt ID was too large! "
end
if type ( iid ) == " number " or type ( iid ) == " boolean " then return true , " Non-string interrupt IDs are deprecated " end
local warn
local seen = { }
local function check ( t )
if type ( t ) == " function " then
warn = " Functions cannot be used in interrupt IDs "
return false
end
if type ( t ) ~= " table " then
return true
end
if seen [ t ] then
warn = " Non-tree-like tables are forbidden as interrupt IDs "
return false
end
seen [ t ] = true
for k , v in pairs ( t ) do
if not check ( k ) then return false end
if not check ( v ) then return false end
end
return true
end
if not check ( iid ) then return false , warn end
if # minetest.serialize ( iid ) > limit then
return false , " An interrupt ID was too large! "
end
return true , " Table interrupt IDs are deprecated and are unreliable; use strings instead "
end
2018-12-29 21:48:32 +01:00
-- 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 )
2018-01-22 12:26:21 +01:00
end )
2013-01-13 17:33:16 +01:00
end
2018-12-29 21:48:32 +01:00
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
2020-09-21 21:32:25 +02:00
return function ( time , iid )
2018-12-29 21:48:32 +01:00
-- 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 " )
2020-09-21 21:32:25 +02:00
local ok , warn = validate_iid ( iid )
if ok then mesecon.queue : add_action ( pos , " lc_interrupt " , { luac_id , iid } , time , iid , 1 ) end
if warn then send_warning ( warn ) end
2018-12-29 21:48:32 +01:00
end )
end
end
2013-01-13 17:33:16 +01:00
end
2018-01-13 20:27:01 +01:00
-- 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
2018-01-22 12:26:21 +01:00
-- 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 )
2018-01-13 20:27:01 +01:00
if not minetest.global_exists ( " digilines " ) then return end
2018-01-22 12:26:21 +01:00
local chan_maxlen = mesecon.setting ( " luacontroller_digiline_channel_maxlen " , 256 )
local maxlen = mesecon.setting ( " luacontroller_digiline_maxlen " , 50000 )
2014-03-19 09:09:39 +01:00
return function ( channel , msg )
2018-01-22 12:26:21 +01:00
-- NOTE: This runs within string metatable sandbox, so don't *rely* on anything of the form (""):y
-- or via anything that could.
2016-12-28 10:07:59 +01:00
-- Make sure channel is string, number or boolean
2018-01-22 12:26:21 +01:00
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. " )
2016-12-28 10:07:59 +01:00
return false
end
2018-01-13 20:27:01 +01:00
local msg_cost
msg , msg_cost = clean_and_weigh_digiline_message ( msg )
2018-01-22 12:26:21 +01:00
if msg == nil or msg_cost > maxlen then
send_warning ( " Message was too complex, or contained invalid data. " )
2016-12-28 10:07:59 +01:00
return false
end
2018-01-22 12:26:21 +01:00
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 , " lc_digiline_relay " , { channel , luac_id , msg } )
end )
2016-12-28 10:07:59 +01:00
return true
2014-03-19 09:09:39 +01:00
end
end
2021-03-26 20:43:08 +01:00
-- Mods can place their own "libraries" in here to be loaded via require() from in a Luacontroller.
-- These can take two different forms:
-- Function (recommended for libraries adding new functionality): A function that, when called, returns something that will be passed to the LuaC code.
-- Function signature is getlibrary(env, pos) where 'env' is the environment that the Luacontroller code is running in, and 'pos' is the position of the controller.
-- Table (recommended for libraries containing mostly lookup tables): A table that will be copied, and the copy returned to the LuaC code.
-- When using the table format, any functions in the table will have their environment changed to that of the Luacontroller.
2021-03-21 06:56:38 +01:00
mesecon.luacontroller_libraries = { }
2021-03-26 20:43:08 +01:00
local function get_require ( pos , env )
2021-03-21 06:56:38 +01:00
return function ( name )
2021-03-26 20:43:08 +01:00
if type ( mesecon.luacontroller_libraries [ name ] ) == " function " then
return mesecon.luacontroller_libraries [ name ] ( env , pos )
elseif type ( mesecon.luacontroller_libraries [ name ] ) == " table " then
return mesecon.tablecopy_change_env ( mesecon.luacontroller_libraries [ name ] , env )
2021-03-21 06:56:38 +01:00
end
end
end
2014-08-07 04:54:17 +02:00
local safe_globals = {
2018-01-22 12:26:21 +01:00
-- Don't add pcall/xpcall unless willing to deal with the consequences (unless very careful, incredibly likely to allow killing server indirectly)
2016-03-13 13:43:55 +01:00
" assert " , " error " , " ipairs " , " next " , " pairs " , " select " ,
" tonumber " , " tostring " , " type " , " unpack " , " _VERSION "
2014-08-07 04:54:17 +02:00
}
2016-12-28 10:07:59 +01:00
2018-01-22 12:26:21 +01:00
local function create_environment ( pos , mem , event , itbl , send_warning )
2013-01-13 00:18:25 +01:00
-- Gather variables for the environment
2013-12-01 04:13:00 +01:00
local vports = minetest.registered_nodes [ minetest.get_node ( pos ) . name ] . virtual_portstates
2014-08-07 04:54:17 +02:00
local vports_copy = { }
for k , v in pairs ( vports ) do vports_copy [ k ] = v end
local rports = get_real_port_states ( pos )
2017-02-15 19:52:29 +01:00
-- Create new library tables on each call to prevent one Luacontroller
-- from breaking a library and messing up other Luacontrollers.
2014-08-07 04:54:17 +02:00
local env = {
pin = merge_port_states ( vports , rports ) ,
port = vports_copy ,
event = event ,
mem = mem ,
2017-04-14 21:14:17 +02:00
heat = mesecon.get_heat ( pos ) ,
2014-11-22 23:04:34 +01:00
heat_max = mesecon.setting ( " overheat_max " , 20 ) ,
2014-08-07 04:54:17 +02:00
print = safe_print ,
2018-01-22 12:26:21 +01:00
interrupt = get_interrupt ( pos , itbl , send_warning ) ,
digiline_send = get_digiline_send ( pos , itbl , send_warning ) ,
2014-08-07 04:54:17 +02:00
string = {
byte = string.byte ,
char = string.char ,
format = string.format ,
len = string.len ,
lower = string.lower ,
upper = string.upper ,
2016-03-13 22:01:46 +01:00
rep = safe_string_rep ,
2014-08-07 04:54:17 +02:00
reverse = string.reverse ,
sub = string.sub ,
2016-03-14 14:29:34 +01:00
find = safe_string_find ,
2014-08-07 04:54:17 +02:00
} ,
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 ,
2016-01-02 08:13:38 +01:00
datetable = safe_date ,
2014-08-07 04:54:17 +02:00
} ,
2013-03-23 23:41:44 +01:00
}
2014-08-07 04:54:17 +02:00
env._G = env
for _ , name in pairs ( safe_globals ) do
env [ name ] = _G [ name ]
end
2021-03-21 06:56:38 +01:00
2021-03-26 20:43:08 +01:00
env.require = get_require ( pos , env )
2014-08-07 04:54:17 +02:00
return env
2013-01-13 11:05:04 +01:00
end
2013-01-13 00:18:25 +01:00
2014-08-07 04:54:17 +02:00
local function timeout ( )
2016-03-14 12:51:57 +01:00
debug.sethook ( ) -- Clear hook
2016-03-13 13:43:55 +01:00
error ( " Code timed out! " , 2 )
2014-08-07 04:54:17 +02:00
end
local function create_sandbox ( code , env )
2013-01-13 00:18:25 +01:00
if code : byte ( 1 ) == 27 then
2014-08-07 04:54:17 +02:00
return nil , " Binary code prohibited. "
2013-01-13 00:18:25 +01:00
end
2014-11-22 14:47:18 +01:00
local f , msg = loadstring ( code )
2014-08-07 04:54:17 +02:00
if not f then return nil , msg end
2013-01-13 00:18:25 +01:00
setfenv ( f , env )
2016-03-14 14:29:34 +01:00
-- 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
2018-01-22 12:26:21 +01:00
local maxevents = mesecon.setting ( " luacontroller_maxevents " , 10000 )
2014-08-07 04:54:17 +02:00
return function ( ... )
2018-01-22 12:26:21 +01:00
-- NOTE: This runs within string metatable sandbox, so the setting's been moved out for safety
2016-03-14 14:29:34 +01:00
-- Use instruction counter to stop execution
-- after luacontroller_maxevents
debug.sethook ( timeout , " " , maxevents )
2014-08-07 04:54:17 +02:00
local ok , ret = pcall ( f , ... )
debug.sethook ( ) -- Clear hook
2016-03-13 13:43:55 +01:00
if not ok then error ( ret , 0 ) end
2014-08-07 04:54:17 +02:00
return ret
2013-01-13 00:18:25 +01:00
end
2013-01-13 11:05:04 +01:00
end
2014-08-07 04:54:17 +02:00
local function load_memory ( meta )
2018-01-22 12:26:21 +01:00
return minetest.deserialize ( meta : get_string ( " lc_memory " ) , true ) or { }
2013-01-13 11:05:04 +01:00
end
2015-10-18 11:28:35 +02:00
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 )
2018-09-17 21:47:19 +02:00
meta : mark_as_private ( " lc_memory " )
2015-10-18 11:28:35 +02:00
else
print ( " Error: Luacontroller memory overflow. " .. memsize_max .. " bytes available, "
.. # memstring .. " required. Controller overheats. " )
burn_controller ( pos )
end
2013-01-14 17:58:14 +01:00
end
2018-01-22 12:26:21 +01:00
-- Returns success (boolean), errmsg (string)
-- run (as opposed to run_inner) is responsible for setting up meta according to this output
local function run_inner ( pos , code , event )
2013-12-01 04:13:00 +01:00
local meta = minetest.get_meta ( pos )
2018-01-22 12:26:21 +01:00
-- Note: These return success, presumably to avoid changing LC ID.
if overheat ( pos ) then return true , " " end
if ignore_event ( event , meta ) then return true , " " end
2013-01-13 11:05:04 +01:00
2014-08-07 04:54:17 +02:00
-- Load code & mem from meta
2013-01-13 11:05:04 +01:00
local mem = load_memory ( meta )
local code = meta : get_string ( " code " )
2018-01-22 12:26:21 +01:00
-- 'Last warning' label.
local warning = " "
local function send_warning ( str )
warning = " Warning: " .. str
end
2014-08-07 04:54:17 +02:00
-- Create environment
2018-01-22 12:26:21 +01:00
local itbl = { }
local env = create_environment ( pos , mem , event , itbl , send_warning )
2013-01-13 11:05:04 +01:00
2014-08-07 04:54:17 +02:00
-- Create the sandbox and execute code
local f , msg = create_sandbox ( code , env )
2018-01-22 12:26:21 +01:00
if not f then return false , msg 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
2013-01-14 17:58:14 +01:00
local success , msg = pcall ( f )
2018-01-22 12:26:21 +01:00
onetruestring.__index = string
-- End string true sandboxing
if not success then return false , msg end
2014-08-07 04:54:17 +02:00
if type ( env.port ) ~= " table " then
2018-01-22 12:26:21 +01:00
return false , " Ports set are invalid. "
2014-08-07 04:54:17 +02:00
end
2013-01-13 11:05:04 +01:00
2014-03-19 09:09:39 +01:00
-- Actually set the ports
2014-08-07 04:54:17 +02:00
set_port_states ( pos , env.port )
2015-10-18 11:28:35 +02:00
-- Save memory. This may burn the luacontroller if a memory overflow occurs.
save_memory ( pos , meta , env.mem )
2013-01-13 00:18:25 +01:00
2018-01-22 12:26:21 +01:00
-- Execute deferred tasks
for _ , v in ipairs ( itbl ) do
local failure = v ( )
if failure then
return false , failure
end
end
return true , warning
end
2014-11-25 17:20:05 +01:00
2018-01-22 12:26:21 +01:00
local function reset_formspec ( meta , code , errmsg )
2013-03-15 22:46:59 +01:00
meta : set_string ( " code " , code )
2018-09-17 21:55:53 +02:00
meta : mark_as_private ( " code " )
2013-12-01 04:13:00 +01:00
code = minetest.formspec_escape ( code or " " )
2017-05-08 16:33:05 +02:00
errmsg = minetest.formspec_escape ( tostring ( errmsg or " " ) )
2018-12-09 13:50:02 +01:00
meta : set_string ( " formspec " , " size[12,10] "
2020-10-09 22:28:11 +02:00
.. " style_type[label,textarea;font=mono] "
2018-12-09 13:50:02 +01:00
.. " 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;] "
)
2018-01-22 12:26:21 +01:00
end
local function reset_meta ( pos , code , errmsg )
local meta = minetest.get_meta ( pos )
reset_formspec ( meta , code , errmsg )
2014-08-07 04:54:17 +02:00
meta : set_int ( " luac_id " , math.random ( 1 , 65535 ) )
2013-01-13 00:18:25 +01:00
end
2018-01-22 12:26:21 +01:00
-- 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 = run_inner ( pos , code , event )
if not ok then
reset_meta ( pos , code , errmsg )
else
reset_formspec ( meta , code , errmsg )
end
return ok , errmsg
end
2014-08-07 04:54:17 +02:00
local function reset ( pos )
set_port_states ( pos , { a = false , b = false , c = false , d = false } )
2013-01-13 00:18:25 +01:00
end
2018-12-29 21:48:32 +01:00
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
2018-01-22 12:26:21 +01:00
-----------------------
-- A.Queue callbacks --
-----------------------
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 )
mesecon.queue : add_function ( " lc_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 . default , channel , msg )
end )
2013-01-13 11:05:04 +01:00
-----------------------
-- Node Registration --
-----------------------
2013-01-13 00:18:25 +01:00
2014-08-07 04:54:17 +02:00
local output_rules = { }
local input_rules = { }
2013-01-13 17:33:16 +01:00
2014-08-07 04:54:17 +02:00
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
2013-01-19 21:45:39 +01:00
}
2014-08-07 04:54:17 +02:00
}
2013-01-19 21:45:39 +01:00
2014-08-07 04:54:17 +02:00
local selection_box = {
type = " fixed " ,
fixed = { - 8 / 16 , - 8 / 16 , - 8 / 16 , 8 / 16 , - 5 / 16 , 8 / 16 } ,
}
2013-01-19 21:45:39 +01:00
local digiline = {
receptor = { } ,
effector = {
2014-08-07 04:54:17 +02:00
action = function ( pos , node , channel , msg )
2018-01-13 20:27:01 +01:00
msg = clean_and_weigh_digiline_message ( msg )
2014-08-07 04:54:17 +02:00
run ( pos , { type = " digiline " , channel = channel , msg = msg } )
2013-01-19 21:45:39 +01:00
end
}
}
2017-10-18 21:54:28 +02:00
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 )
2018-01-22 12:26:21 +01:00
return run ( pos , { type = " program " } )
2017-10-18 21:54:28 +02:00
end
2016-05-16 19:55:58 +02:00
local function on_receive_fields ( pos , form_name , fields , sender )
2014-08-07 04:54:17 +02:00
if not fields.program then
return
end
2016-05-16 19:55:58 +02:00
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
2017-10-18 21:54:28 +02:00
local ok , err = set_program ( pos , fields.code )
if not ok then
-- it's not an error from the server perspective
2018-07-18 21:49:34 +02:00
minetest.log ( " action " , " Lua controller programming error: " .. tostring ( err ) )
2014-08-07 04:54:17 +02:00
end
end
2013-01-19 21:45:39 +01:00
2014-08-07 04:54:17 +02:00
for a = 0 , 1 do -- 0 = off 1 = on
2013-04-01 22:24:01 +02:00
for b = 0 , 1 do
for c = 0 , 1 do
for d = 0 , 1 do
2014-08-07 04:54:17 +02:00
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
2013-01-20 17:48:43 +01:00
2014-08-07 04:54:17 +02:00
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
2013-01-13 00:18:25 +01:00
2014-08-07 04:54:17 +02:00
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 )
2014-11-29 10:56:09 +01:00
run ( pos , { type = new_state , pin = rule_name } )
2014-08-07 04:54:17 +02:00
end ,
} ,
receptor = {
state = mesecon.state . on ,
rules = output_rules [ cid ]
2017-10-18 21:54:28 +02:00
} ,
luacontroller = {
get_program = get_program ,
set_program = set_program ,
} ,
2013-01-13 00:18:25 +01:00
}
2014-08-07 04:54:17 +02:00
minetest.register_node ( node_name , {
2017-02-15 19:52:29 +01:00
description = " Luacontroller " ,
2014-08-07 04:54:17 +02:00
drawtype = " nodebox " ,
tiles = {
top ,
" jeija_microcontroller_bottom.png " ,
" jeija_microcontroller_sides.png " ,
" jeija_microcontroller_sides.png " ,
" jeija_microcontroller_sides.png " ,
" jeija_microcontroller_sides.png "
2013-01-13 00:18:25 +01:00
} ,
2014-08-07 04:54:17 +02:00
inventory_image = top ,
paramtype = " light " ,
2017-10-31 22:50:39 +01:00
is_ground_content = false ,
2014-08-07 04:54:17 +02:00
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 )
2017-04-14 21:14:17 +02:00
mesecon.do_cooldown ( pos )
2014-11-22 23:04:34 +01:00
mesecon.receptor_off ( pos , output_rules )
2014-08-07 04:54:17 +02:00
end ,
is_luacontroller = true ,
2018-12-29 21:48:32 +01:00
on_timer = node_timer ,
2017-10-07 00:44:49 +02:00
on_blast = mesecon.on_blastnode ,
2014-08-07 04:54:17 +02:00
} )
2013-01-13 00:18:25 +01:00
end
end
end
end
2014-04-20 21:44:58 +02:00
------------------------------
2017-02-15 19:52:29 +01:00
-- Overheated Luacontroller --
2014-04-20 21:44:58 +02:00
------------------------------
2013-05-16 03:36:16 +02:00
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 " ,
2015-10-18 11:28:35 +02:00
is_burnt = true ,
2013-05-16 03:36:16 +02:00
paramtype = " light " ,
2017-10-31 22:50:39 +01:00
is_ground_content = false ,
2013-05-16 03:36:16 +02:00
groups = { dig_immediate = 2 , not_in_creative_inventory = 1 } ,
drop = BASENAME .. " 0000 " ,
sunlight_propagates = true ,
2014-12-20 10:11:12 +01:00
selection_box = selection_box ,
2014-11-22 23:14:45 +01:00
node_box = node_box ,
2013-05-16 03:36:16 +02:00
on_construct = reset_meta ,
2014-08-07 04:54:17 +02:00
on_receive_fields = on_receive_fields ,
2013-05-16 03:36:16 +02:00
sounds = default.node_sound_stone_defaults ( ) ,
virtual_portstates = { a = false , b = false , c = false , d = false } ,
2014-08-07 04:54:17 +02:00
mesecons = {
effector = {
rules = mesecon.rules . flat ,
action_change = function ( pos , _ , rule_name , new_state )
2014-11-22 23:14:45 +01:00
update_real_port_states ( pos , rule_name , new_state )
2014-08-07 04:54:17 +02:00
end ,
} ,
} ,
2017-10-07 00:44:49 +02:00
on_blast = mesecon.on_blastnode ,
2013-05-16 03:36:16 +02:00
} )
2013-01-13 11:05:04 +01:00
------------------------
-- Craft Registration --
------------------------
2013-01-13 00:18:25 +01:00
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 ' , ' ' } ,
}
} )