LuaAutomation - Basic component implementation

Implements the base code for LuaAutomation, an ATC rail and a punch-operated 'operation panel' as well as interface for passive components.
Changes in advtrains code where neccessary.
Supported passive components are light signals, switches and mesecon switches
master
orwell96 2017-02-02 16:40:51 +01:00
parent a8f9e3d43e
commit b19033b224
15 changed files with 753 additions and 2 deletions

View File

@ -11,7 +11,7 @@ advtrains = {trains={}, wagon_save={}}
advtrains.modpath = minetest.get_modpath("advtrains")
local function print_concat_table(a)
function advtrains.print_concat_table(a)
local str=""
local stra=""
for i=1,50 do
@ -43,7 +43,11 @@ local function print_concat_table(a)
end
atprint=function() end
if minetest.setting_getbool("advtrains_debug") then
atprint=function(t, ...) minetest.log("action", "[advtrains]"..print_concat_table({t, ...})) minetest.chat_send_all("[advtrains]"..print_concat_table({t, ...})) end
atprint=function(t, ...)
local text=advtrains.print_concat_table({t, ...})
minetest.log("action", "[advtrains]"..text)
minetest.chat_send_all("[advtrains]"..text)
end
end
sid=function(id) return string.sub(id, -4) end

View File

@ -250,6 +250,7 @@ function advtrains.register_tracks(tracktype, def, preset)
if newstate~=is_state then
advtrains.ndb.swap_node(pos, {name=def.nodename_prefix.."_"..suffix_target, param2=node.param2})
end
advtrains.invalidate_all_paths()
end
local mesec
if mesecon_state then -- if mesecons is not wanted, do not.

View File

@ -0,0 +1,118 @@
local ac = {nodes={}}
function ac.load(data)
ac.nodes=data and data.nodes or {}
end
function ac.save()
return {nodes = ac.nodes}
end
function ac.after_place_node(pos, player)
advtrains.ndb.update(pos)
local meta=minetest.get_meta(pos)
meta:set_string("formspec", ac.getform(pos, meta))
meta:set_string("infotext", "LuaAutomation component, unconfigured.")
local ph=minetest.hash_node_position(pos)
--just get first available key!
for en,_ in pairs(atlatc.envs) do
ac.nodes[ph]={env=en}
return
end
end
function ac.getform(pos, meta_p)
local meta = meta_p or minetest.get_meta(pos)
local envs_asvalues={}
local ph=minetest.hash_node_position(pos)
local nodetbl = ac.nodes[ph]
local env, code, err = nil, "", ""
if nodetbl then
code=nodetbl.code or ""
err=nodetbl.err or ""
env=nodetbl.env or ""
end
local sel = 1
for n,_ in pairs(atlatc.envs) do
envs_asvalues[#envs_asvalues+1]=n
if n==env then
sel=#envs_asvalues
end
end
local form = "size[10,10]dropdown[0,0;3;env;"..table.concat(envs_asvalues, ",")..";"..sel.."]"
.."button[4,0;2,1;save;Save]button[7,0;2,1;cle;Clear local env] textarea[0.2,1;10,10;code;Code;"..minetest.formspec_escape(code).."]"
.."label[0,9.8;"..err.."]"
return form
end
function ac.after_dig_node(pos, node, player)
advtrains.invalidate_all_paths()
advtrains.ndb.clear(pos)
local ph=minetest.hash_node_position(pos)
ac.nodes[ph]=nil
end
function ac.on_receive_fields(pos, formname, fields, player)
if not minetest.check_player_privs(player:get_player_name(), {atlatc=true}) then
minetest.chat_send_player(player:get_player_name(), "Missing privilege: atlatc - Operation cancelled!")
end
local meta=minetest.get_meta(pos)
local ph=minetest.hash_node_position(pos)
local nodetbl = ac.nodes[ph] or {}
--if fields.quit then return end
if fields.env then
nodetbl.env=fields.env
end
if fields.code then
nodetbl.code=fields.code
end
if fields.save then
nodetbl.err=nil
end
if fields.cle then
nodetbl.data={}
end
meta:set_string("formspec", ac.getform(pos, meta))
ac.nodes[ph]=nodetbl
if nodetbl.env then
meta:set_string("infotext", "LuaAutomation component, assigned to environment '"..nodetbl.env.."'")
else
meta:set_string("infotext", "LuaAutomation component, invalid enviroment set!")
end
end
function ac.run_in_env(pos, evtdata, customfct)
local ph=minetest.hash_node_position(pos)
local nodetbl = ac.nodes[ph] or {}
local meta
if minetest.get_node(pos) then
meta=minetest.get_meta(pos)
end
if not nodetbl.env or not atlatc.envs[nodetbl.env] then
return false, "Not an existing environment: "..(nodetbl.env or "<nil>")
end
if not nodetbl.code or nodetbl.code=="" then
return false, "No code to run!"
end
local datain=nodetbl.data or {}
local succ, dataout = atlatc.envs[nodetbl.env]:execute_code(datain, nodetbl.code, evtdata, customfct)
if succ then
atlatc.active.nodes[ph].data=atlatc.remove_invalid_data(dataout)
else
atlatc.active.nodes[ph].err=dataout
if meta then
meta:set_string("infotext", "LuaAutomation ATC interface rail, ERROR:"..dataout)
end
end
if meta then
meta:set_string("formspec", ac.getform(pos, meta))
end
end
atlatc.active=ac

View File

@ -0,0 +1,92 @@
-- atc_rail.lua
-- registers and handles the ATC rail. Active component.
-- This is the only component that can interface with trains, so train interface goes here too.
--Using subtable
local r={}
function r.fire_event(pos, evtdata)
local ph=minetest.hash_node_position(pos)
local railtbl = atlatc.active.nodes[ph] or {}
local arrowconn = railtbl.arrowconn
--prepare ingame API for ATC. Regenerate each time since pos needs to be known
local atc_valid, atc_arrow
local train_id=advtrains.detector.on_node[ph]
local train=advtrains.trains[train_id]
if not train then return false end
if not train.path then
--we happened to get in between an invalidation step
--delay
atlatc.interrupt.add(0,pos,evtdata)
return
end
for index, ppos in pairs(train.path) do
if vector.equals(advtrains.round_vector_floor_y(ppos), pos) then
atc_arrow =
vector.equals(
advtrains.dirCoordSet(pos, arrowconn),
advtrains.round_vector_floor_y(train.path[index+train.movedir])
)
atc_valid = true
end
end
local customfct={
atc_send = function(cmd)
advtrains.atc.train_reset_command(train_id)
if atc_valid then
train.atc_command=cmd
train.atc_arrow=atc_arrow
return atc_valid
end
end,
atc_reset = function(cmd)
advtrains.atc.train_reset_command(train_id)
return true
end,
atc_arrow = atc_arrow
}
atlatc.active.run_in_env(pos, evtdata, customfct)
end
advtrains.register_tracks("default", {
nodename_prefix="advtrains_luaautomation:dtrack",
texture_prefix="advtrains_dtrack_atc",
models_prefix="advtrains_dtrack_detector",
models_suffix=".b3d",
shared_texture="advtrains_dtrack_rail_atc.png",
description=atltrans("LuaAutomation ATC Rail"),
formats={},
get_additional_definiton = function(def, preset, suffix, rotation)
return {
after_place_node = atlatc.active.after_place_node,
after_dig_node = atlatc.active.after_dig_node,
on_receive_fields = function(pos, ...)
atlatc.active.on_receive_fields(pos, ...)
--set arrowconn (for ATC)
local ph=minetest.hash_node_position(pos)
local _, conn1=advtrains.get_rail_info_at(pos, advtrains.all_tracktypes)
atlatc.active.nodes[ph].arrowconn=conn1
end,
advtrains = {
on_train_enter = function(pos, train_id)
--do async. Event is fired in train steps
atlatc.interrupt.add(0, pos, {type="train", id=train_id})
end,
},
luaautomation = {
fire_event=r.fire_event
}
}
end
}, advtrains.trackpresets.t_30deg_straightonly)
atlatc.rail = r

View File

@ -0,0 +1,2 @@
advtrains
mesecons?

View File

@ -0,0 +1,253 @@
-------------
-- lua sandboxed environment
-- function to cross out functions and userdata.
-- modified from dump()
function atlatc.remove_invalid_data(o, nested)
if o==nil then return nil end
local valid_dt={["nil"]=true, boolean=true, number=true, string=true}
if type(o) ~= "table" then
--check valid data type
if not valid_dt[type(o)] then
return nil
end
return o
end
-- Contains table -> true/nil of currently nested tables
nested = nested or {}
if nested[o] then
return nil
end
nested[o] = true
for k, v in pairs(o) do
v = atlatc.remove_invalid_data(v, nested)
end
nested[o] = nil
return o
end
local env_proto={
load = function(self, envname, data)
self.name=envname
self.sdata=data.sdata and atlatc.remove_invalid_data(data.sdata) or {}
self.fdata={}
self.init_code=data.init_code or ""
self.step_code=data.step_code or ""
end,
save = function(self)
-- throw any function values out of the sdata table
self.sdata = atlatc.remove_invalid_data(self.sdata)
return {sdata = self.sdata, init_code=self.init_code, step_code=self.step_code}
end,
}
--Environment
--Code modified from mesecons_luacontroller (credit goes to Jeija and mesecons contributors)
local safe_globals = {
"assert", "error", "ipairs", "next", "pairs", "select",
"tonumber", "tostring", "type", "unpack", "_VERSION"
}
--print is actually minetest.chat_send_all()
--using advtrains.print_concat_table because it's cool
local function safe_print(t, ...)
local str=advtrains.print_concat_table({t, ...})
minetest.log("action", "[atlatc] "..str)
minetest.chat_send_all(str)
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 > 2000 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 for security reasons.")
end
return string.find(...)
end
local mp=minetest.get_modpath("advtrains_luaautomation")
local p_api_getstate, p_api_setstate = dofile(mp.."/passive.lua")
local static_env = {
--core LUA functions
print = safe_print,
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,
date = safe_date,
},
POS = function(x,y,z) return {x=x, y=y, z=z} end,
getstate = p_api_getstate,
setstate = p_api_setstate,
}
for _, name in pairs(safe_globals) do
static_env[name] = _G[name]
end
--The environment all code calls get is a table that has set static_env as metatable.
--In general, every variable is local to a single code chunk, but kept persistent over code re-runs. Data is also saved, but functions and userdata and circular references are removed
--Init code and step code's environments are not saved
-- S - Table that can contain any save data global to the environment. Will be saved statically. Can't contain functions or userdata or circular references.
-- F - Table global to the environment, can contain volatile data that is deleted when server quits.
-- The init code should populate this table with functions and other definitions.
-- returns: true, fenv if successful; nil, error if error
function env_proto:execute_code(fenv, code, evtdata, customfct)
local metatbl ={
__index = function(t, i)
print("index metamethod:",i)
if i=="S" then
return self.sdata
elseif i=="F" then
return self.fdata
elseif i=="event" then
return evtdata
elseif customfct and customfct[i] then
return customfct[i]
end
return static_env[i]
end,
__newindex = function(t, i, v)
if i=="S" or i=="F" or i=="event" or (customfct and customfct[i]) or static_env[i] then
debug.sethook()
error("Trying to overwrite environment contents")
end
rawset(t,i,v)
end,
}
setmetatable(fenv, metatbl)
local fun, err=loadstring(code)
if not fun then
return false, err
end
setfenv(fun, fenv)
local succ, data = pcall(fun)
if succ then
data=fenv
end
return succ, data
end
function env_proto:run_initcode()
if self.init_code and self.init_code~="" then
local succ, err = self:execute_code(self.init_code, nil, {}, "Global init code")
if not succ then
--TODO
end
end
end
function env_proto:run_stepcode()
if self.step_code and self.step_code~="" then
local succ, err = self:execute_code({}, self.step_code, nil, {})
if not succ then
--TODO
end
end
end
--- class interface
function atlatc.env_new(name)
local newenv={
name=name,
init_code="",
step_code="",
sdata={}
}
setmetatable(newenv, {__index=env_proto})
return newenv
end
function atlatc.env_load(name, data)
local newenv={}
setmetatable(newenv, {__index=env_proto})
newenv:load(name, data)
return newenv
end
function atlatc.run_initcode()
for envname, env in pairs(atlatc.envs) do
env:run_initcode()
end
end
function atlatc.run_stepcode()
for envname, env in pairs(atlatc.envs) do
env:run_stepcode()
end
end

View File

@ -0,0 +1,98 @@
-- advtrains_luaautomation/init.lua
-- Lua automation features for advtrains
-- Uses global table 'atlatc' (AdvTrains_LuaATC)
-- Boilerplate to support localized strings if intllib mod is installed.
if minetest.get_modpath("intllib") then
atltrans = intllib.Getter()
else
atltrans = function(s,a,...)a={a,...}return s:gsub("@(%d+)",function(n)return a[tonumber(n)]end)end
end
--Privilege
--Only trusted players should be enabled to build stuff which can break the server.
atlatc = { envs = {}}
minetest.register_privilege("atlatc", { description = "Player can place and modify LUA ATC components. Grant with care! Allows to execute bad LUA code.", give_to_singleplayer = false, default= false })
local mp=minetest.get_modpath("advtrains_luaautomation")
if not mp then
error("Mod name error: Mod folder is not named 'advtrains_luaautomation'!")
end
dofile(mp.."/environment.lua")
dofile(mp.."/interrupt.lua")
dofile(mp.."/active_common.lua")
dofile(mp.."/atc_rail.lua")
dofile(mp.."/operation_panel.lua")
dofile(mp.."/p_mesecon_iface.lua")
local filename=minetest.get_worldpath().."/advtrains_luaautomation"
local file, err = io.open(filename, "r")
if not file then
minetest.log("error", " Failed to read advtrains_luaautomation save data from file "..filename..": "..(err or "Unknown Error"))
else
local tbl = minetest.deserialize(file:read("*a"))
if type(tbl) == "table" then
if tbl.version==1 then
for envname, data in pairs(tbl.envs) do
atlatc.envs[envname]=atlatc.env_load(envname, data)
end
atlatc.active.load(tbl.active)
atlatc.interrupt.load(tbl.interrupt)
end
else
minetest.log("error", " Failed to read advtrains_luaautomation save data from file "..filename..": Not a table!")
end
file:close()
end
-- run init code of all environments
atlatc.run_initcode()
atlatc.save = function()
--versions:
-- 1 - Initial save format.
local envdata={}
for envname, env in pairs(atlatc.envs) do
envdata[envname]=env:save()
end
local save_tbl={
version = 1,
envs=envdata,
active = atlatc.active.save(),
interrupt = atlatc.interrupt.save(),
}
local datastr = minetest.serialize(save_tbl)
if not datastr then
minetest.log("error", " Failed to save advtrains_luaautomation save data to file "..filename..": Can't serialize!")
return
end
local file, err = io.open(filename, "w")
if err then
minetest.log("error", " Failed to save advtrains_luaautomation save data to file "..filename..": "..(err or "Unknown Error"))
return
end
file:write(datastr)
file:close()
end
-- globalstep for step code
local timer, step_int=0, 2
local stimer, sstep_int=0, 10
minetest.register_globalstep(function(dtime)
timer=timer+dtime
if timer>step_int then
timer=0
atlatc.run_stepcode()
end
stimer=stimer+dtime
if stimer>sstep_int then
stimer=0
atlatc.save()
end
end)
minetest.register_on_shutdown(atlatc.save)

View File

@ -0,0 +1,48 @@
-- interrupt.lua
-- implements interrupt queue
--to be saved: pos and evtdata
local iq={}
local queue={}
local timer=0
local run=false
function iq.load(data)
local d=data or {}
queue = d.queue or {}
timer = d.timer or 0
end
function iq.save()
return {queue = queue}
end
function iq.add(t, pos, evtdata)
queue[#queue+1]={t=t+timer, p=pos, e=evtdata}
run=true
end
minetest.register_globalstep(function(dtime)
if run then
timer=timer + math.min(dtime, 0.2)
for i=1,#queue do
local qe=queue[i]
if not qe then
table.remove(queue, i)
i=i-1
elseif timer>qe.t then
local pos, evtdata=queue[i].p, queue[i].e
local node=advtrains.ndb.get_node(pos)
local ndef=minetest.registered_nodes[node.name]
if ndef and ndef.luaautomation and ndef.luaautomation.fire_event then
ndef.luaautomation.fire_event(pos, evtdata)
end
table.remove(queue, i)
i=i-1
end
end
end
end)
atlatc.interrupt=iq

View File

@ -0,0 +1,23 @@
local function on_punch(pos, player)
atlatc.interrupt.add(0, pos, {type="punch", punch=true})
end
minetest.register_node("advtrains_luaautomation:oppanel", {
drawtype = "normal",
tiles={"atlatc_oppanel.png"},
description = "LuaAutomation operation panel",
groups = {
choppy = 1,
save_in_nodedb=1,
},
after_place_node = atlatc.active.after_place_node,
after_dig_node = atlatc.active.after_dig_node,
on_receive_fields = atlatc.active.on_receive_fields,
on_punch = on_punch,
luaautomation = {
fire_event=atlatc.active.run_in_env
}
})

View File

@ -0,0 +1,60 @@
-- p_mesecon_iface.lua
-- Mesecons interface by overriding the switch
if not mesecon then return end
minetest.override_item("mesecons_switch:mesecon_switch_off", {
groups = {
dig_immediate=2,
save_in_nodedb=1,
},
on_rightclick = function (pos, node)
if(mesecon.flipstate(pos, node) == "on") then
mesecon.receptor_on(pos)
else
mesecon.receptor_off(pos)
end
minetest.sound_play("mesecons_switch", {pos=pos})
advtrains.ndb.update(pos, node)
end,
on_updated_from_nodedb = function(pos, node)
mesecon.receptor_off(pos)
end,
luaautomation = {
getstate = "off",
setstate = function(pos, node, newstate)
if newstate=="on" then
advtrains.ndb.swap_node(pos, {name="mesecons_switch:mesecon_switch_on", param2=node.param2})
mesecon.receptor_on(pos)
end
end,
},
})
minetest.override_item("mesecons_switch:mesecon_switch_on", {
groups = {
dig_immediate=2,
save_in_nodedb=1,
},
on_rightclick = function (pos, node)
if(mesecon.flipstate(pos, node) == "on") then
mesecon.receptor_on(pos)
else
mesecon.receptor_off(pos)
end
minetest.sound_play("mesecons_switch", {pos=pos})
advtrains.ndb.update(pos, node)
end,
on_updated_from_nodedb = function(pos, node)
mesecon.receptor_on(pos)
end,
luaautomation = {
getstate = "on",
setstate = function(pos, node, newstate)
if newstate=="off" then
advtrains.ndb.swap_node(pos, {name="mesecons_switch:mesecon_switch_off", param2=node.param2})
mesecon.receptor_off(pos)
end
end,
},
})

View File

@ -0,0 +1,29 @@
-- passive.lua
-- API to passive components, as described in passive_api.txt
local function getstate(pos)
local node=advtrains.ndb.get_node(pos)
local ndef=minetest.registered_nodes[node.name]
if ndef and ndef.luaautomation and ndef.luaautomation.getstate then
local st=ndef.luaautomation.getstate
if type(st)=="function" then
return st(pos, node)
else
return st
end
end
return nil
end
local function setstate(pos, newstate)
local node=advtrains.ndb.get_node(pos)
local ndef=minetest.registered_nodes[node.name]
if ndef and ndef.luaautomation and ndef.luaautomation.setstate then
local st=ndef.luaautomation.setstate
st(pos, node, newstate)
end
end
-- gets called from environment.lua
-- return the values here to keep them local
return getstate, setstate

View File

@ -0,0 +1,23 @@
Lua Automation - Passive Component API
Passive components are nodes that do not have code running in them. However, active components can query these and request actions from them. Examples:
Switches
Signals
Displays
Mesecon Transmitter
All passive components have a table called 'luaautomation' in their node definition and have the group 'save_in_nodedb' set, so they work in unloaded chunks.
Example for a switch:
luaautomation = {
getstate = function(pos, node)
return "st"
end,
-- OR
getstate = "st",
setstate = function(pos, node, newstate)
if newstate=="cr" then
advtrains.ndb.swap_node(pos, <corresponding switch alt>)
end
end
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 B