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
This commit is contained in:
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