Add Automatic Train Control system

h137
orwell96 2017-01-04 12:02:00 +01:00
parent a9d43ce2ca
commit 853a9e690e
19 changed files with 424 additions and 24 deletions

View File

@ -103,7 +103,7 @@ minetest.register_node(nodename, {
after_place_node=function(pos)
advtrains.reset_trackdb_position(pos)
end,
^- the code in these 3 functions is required for advtrains to work.
^- the code in these 3 default minetest API functions is required for advtrains to work, however you can add your own code
advtrains = {
on_train_enter=function(pos, train_id) end

View File

@ -1,4 +1,294 @@
--atc.lua
--registers and controls the ATC system
--(simple)mesecon detector rails
local atc={}
-- ATC persistence table
atc.controllers = {}
--contents: {command="...", arrowconn=0-15 where arrow points}
advtrains.fpath_atc=minetest.get_worldpath().."/advtrains_atc"
local file, err = io.open(advtrains.fpath_atc, "r")
if not file then
local er=err or "Unknown Error"
print("[advtrains]Failed loading advtrains atc save file "..er)
else
local tbl = minetest.deserialize(file:read("*a"))
if type(tbl) == "table" then
atc.controllers=tbl.controllers
end
file:close()
end
function atc.save()
--leave space for more save data.
local datastr = minetest.serialize({controllers = atc.controllers})
if not datastr then
minetest.log("error", "[advtrains] Failed to serialize trackdb data!")
return
end
local file, err = io.open(advtrains.fpath_atc, "w")
if err then
return err
end
file:write(datastr)
file:close()
end
--call from advtrains.detector subprogram
function atc.trigger_controller_train_enter(pos, train_id)
atc.send_command(pos)
end
--general
function atc.send_command(pos)
local pts=minetest.pos_to_string(pos)
if atc.controllers[pts] then
print("Called send_command at "..pts)
local train_id = advtrains.detector.on_node[pts]
if train_id then
if advtrains.trains[train_id] then
print("send_command inside if: "..sid(train_id))
atc.train_reset_command(train_id)
local arrowconn=atc.controllers[pts].arrowconn
local train=advtrains.trains[train_id]
for index, ppos in pairs(train.path) do
if vector.equals(advtrains.round_vector_floor_y(ppos), pos) then
advtrains.trains[train_id].atc_arrow =
vector.equals(
advtrains.dirCoordSet(pos, arrowconn),
advtrains.round_vector_floor_y(train.path[index+train.movedir])
)
advtrains.trains[train_id].atc_command=atc.controllers[pts].command
print("Sending ATC Command: "..atc.controllers[pts].command)
end
end
end
end
end
return false
end
function atc.train_reset_command(train_id)
advtrains.trains[train_id].atc_command=nil
advtrains.trains[train_id].atc_delay=0
advtrains.trains[train_id].atc_brake_target=nil
advtrains.trains[train_id].atc_wait_finish=nil
advtrains.trains[train_id].atc_arrow=nil
end
--nodes
local idxtrans={static=1, mesecon=2, digiline=3}
local apn_func=function(pos)
advtrains.reset_trackdb_position(pos)
local meta=minetest.get_meta(pos)
if meta then
meta:set_string("infotext", "ATC controller, unconfigured.")
meta:set_string("formspec", atc.get_atc_controller_formspec(pos, meta))
end
end
advtrains.register_tracks("default", {
nodename_prefix="advtrains:dtrack_atc",
texture_prefix="advtrains_dtrack_atc",
models_prefix="advtrains_dtrack_detector",
models_suffix=".b3d",
shared_texture="advtrains_dtrack_rail_atc.png",
description="ATC controller",
formats={},
get_additional_definiton = function(def, preset, suffix, rotation)
return {
after_place_node=apn_func,
on_place_rail=apn_func,
after_dig_node=function(pos)
advtrains.invalidate_all_paths()
advtrains.reset_trackdb_position(pos)
local pts=minetest.pos_to_string(pos)
atc.controllers[pts]=nil
end,
on_receive_fields = function(pos, formname, fields, player)
if minetest.is_protected(pos, player:get_player_name()) then
minetest.chat_send_player(player:get_player_name(), "This position is protected!")
return
end
local meta=minetest.get_meta(pos)
if meta then
if not fields.save then
--maybe only the dropdown changed
if fields.mode then
meta:set_string("mode", idxtrans[fields.mode])
meta:set_string("infotext", "ATC controller, mode "..fields.mode.."\n"..( fields.mode=="digiline" and "Channel: "..meta:get_string("channel") or "Command: "..meta:get_string("command") ) )
meta:set_string("formspec", atc.get_atc_controller_formspec(pos, meta))
end
return
end
meta:set_string("mode", idxtrans[fields.mode])
meta:set_string("command", fields.command)
meta:set_string("command_on", fields.command_on)
meta:set_string("channel", fields.channel)
meta:set_string("infotext", "ATC controller, mode "..fields.mode.."\n"..( fields.mode=="digiline" and "Channel: "..meta:get_string("channel") or "Command: "..meta:get_string("command") ) )
meta:set_string("formspec", atc.get_atc_controller_formspec(pos, meta))
local pts=minetest.pos_to_string(pos)
local _, conn1=advtrains.get_rail_info_at(pos, advtrains.all_tracktypes)
atc.controllers[pts]={command=fields.command, arrowconn=conn1}
atc.send_command(pos)
end
end,
}
end
}, advtrains.trackpresets.t_30deg_straightonly)
function atc.get_atc_controller_formspec(pos, meta)
local mode=tonumber(meta:get_string("mode")) or 1
local command=meta:get_string("command")
local command_on=meta:get_string("command_on")
local channel=meta:get_string("channel")
local formspec="size[8,6]"..
"dropdown[0,0;3;mode;static,mesecon,digiline;"..mode.."]"
if mode<3 then
formspec=formspec.."field[0.5,1.5;7,1;command;Command;"..minetest.formspec_escape(command).."]"
if tonumber(mode)==2 then
formspec=formspec.."field[0.5,3;7,1;command_on;Command (on);"..minetest.formspec_escape(command_on).."]"
end
else
formspec=formspec.."field[0.5,1.5;7,1;channel;Digiline channel;"..minetest.formspec_escape(channel).."]"
end
return formspec.."button_exit[0.5,4.5;7,1;save;Save]"
end
--from trainlogic.lua train step
local matchptn={
["SM"]=function(id, train)
train.tarvelocity=train.max_speed
return 2
end,
["S([0-9]+)"]=function(id, train, match)
train.tarvelocity=tonumber(match)
return #match+1
end,
["B([0-9]+)"]=function(id, train, match)
if train.velocity>tonumber(match) then
train.atc_brake_target=tonumber(match)
if train.tarvelocity>train.atc_brake_target then
train.tarvelocity=train.atc_brake_target
end
end
return #match+1
end,
["W"]=function(id, train)
train.atc_wait_finish=true
return 1
end,
["D([0-9]+)"]=function(id, train, match)
train.atc_delay=tonumber(match)
return #match+1
end,
["R"]=function(id, train)
if train.velocity<=0 then
train.movedir=train.movedir*-1
train.atc_arrow = not train.atc_arrow
else
print("ATC Reverse command warning: didn't reverse train!")
end
return 1
end,
}
function atc.execute_atc_command(id, train)
--strip whitespaces
local command=string.match(train.atc_command, "^%s*(.*)$")
if string.match(command, "^%s*$") then
train.atc_command=nil
return
end
--conditional statement?
local is_cond, cond_applies
local cond, rest=string.match(command, "^I([%+%-])(.+)$")
if cond then
is_cond=true
if cond=="+" then
cond_applies=train.atc_arrow
end
if cond=="-" then
cond_applies=not train.atc_arrow
end
else
cond, compare, rest=string.match(command, "^I([<>]=?)([0-9]+)(.+)$")
if cond and compare then
is_cond=true
if cond=="<" then
cond_applies=train.velocity<tonumber(compare)
end
if cond==">" then
cond_applies=train.velocity>tonumber(compare)
end
if cond=="<=" then
cond_applies=train.velocity<=tonumber(compare)
end
if cond==">=" then
cond_applies=train.velocity>=tonumber(compare)
end
end
end
if is_cond then
print("Evaluating if statement: "..command)
print("Cond: "..(cond or "nil"))
print("Applies: "..(cond_applies and "true" or "false"))
print("Rest: "..rest)
--find end of conditional statement
local nest, pos, elsepos=0, 1
while nest>=0 do
if pos>#rest then
print("ATC command syntax error: I statement not closed: "..command)
atc.train_reset_command(id)
return
end
local char=string.sub(rest, pos, pos)
if char=="I" then
nest=nest+1
end
if char==";" then
nest=nest-1
end
if nest==0 and char=="E" then
elsepos=pos+0
end
pos=pos+1
end
if not elsepos then elsepos=pos-1 end
if cond_applies then
command=string.sub(rest, 1, elsepos-1)..string.sub(rest, pos)
else
command=string.sub(rest, elsepos+1, pos-2)..string.sub(rest, pos)
end
print("Result: "..command)
train.atc_command=command
atc.execute_atc_command(id, train)
return
else
for pattern, func in pairs(matchptn) do
local match=string.match(command, "^"..pattern)
if match then
local patlen=func(id, train, match)
print("Executing: "..string.sub(command, 1, patlen))
train.atc_command=string.sub(command, patlen+1)
if train.atc_delay<=0 and not train.atc_wait_finish then
--continue (recursive, cmds shouldn't get too long, and it's a end-recursion.)
atc.execute_atc_command(id, train)
end
return
end
end
end
print("ATC command parse error: "..command)
atc.train_reset_command(id)
end
--move table to desired place
advtrains.atc=atc

68
advtrains/atc_command.txt Normal file
View File

@ -0,0 +1,68 @@
ATC Command Syntax
A train runs the current ATC command once it receives it, including delayed instructions. If the train receives a new command, the current command is discarded.
Spaces can be inserted into commands as needed and are ignored.
# simple commands:
S<[0-9]+ speed or 'M'>
Set target speed of train to <speed>. Accelerates if slower, rolls if faster. 'M' means maximum speed.
Execution of command continues immediately.
B<[0-9]+ speed>
Brake until speed is reached. If faster, apply brakes, if slower, do nothing.
Execution of command continues immediately.
Examples:
SM : accelerate to maximum speed
S0 : roll to stand
B0 : brake to stand
S0B3 or B3S0: brake to 3, then roll to stand.
W
Wait until S and/or B commands reached the desired speed before continuing execution.
D<[0-9]+ time>
Delay: Wait for time seconds before continuing execution.
R
Reverse: change movement direction of train. ONLY WORKS WHEN TRAIN STANDS, else no-op.
Use B0WR to definitely change direction.
Examples:
B0 W R D10 SM
Subway train stopping in dead end station and returning in opposite direction
# conditional statements:
I<condition><code>;
Execute code only if condition applies
I<condition><code1>E<code2>;
Execute code1 only if condition applies, else execute code2
Conditions:
+ / -
Tests the train's movement direction against the arrow on the ATC rail: M+ is true when train drives in direction of arrow.
[</>/<=/>=][speed]
Test if train's speed is greater or smaller than the given value
Examples:
I- B0 W R ; S8
If the train drives in the 'wrong' direction, stop and reverse; independently accelerate to speed 8 afterwards.
I<8 S8 ;
If the train is slower than 8, accelerate to 8.
# ATC controller operation modes
static: Only give 1 static command.
mesecon: Give 2 different commands depending on if the controller is mesecon-powered or not
digiline: Don't give any commands by itself. When a train passes, a digiline message in the form of "[+/-][speed]" is sent on the set channel (where +/- means the same as with conditions). Any digiline message sent to the controller will be interpreted as ATC command and sent to the train.
# Persistence
ATC controllers that are configured as 'static' or 'mesecon' are persistent over mapblock unloads and will even command the train when the mapblock is unloaded. This is not possible with digilines since these do not work in unloaded mapchunks.
# LUA ATC controller (in development)
The LUA ATC Controller will operate by using LUA code. All operations shown above will have a function equivalent. Additionally all LUA ATC controllers share an environment and setting signal and switch status will be possible to allow for complicated railway systems/fully automated subways a.s.o.
Also planned:
- digicompute add-on to allow computer access to the ATC environment (railway maps... ... ... ... ...)

Binary file not shown.

After

Width:  |  Height:  |  Size: 857 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -129,7 +129,7 @@ function tp.bend_rail(originpos, conn, nnpref)
return false
end
end
function tp.placetrack(pos, nnpref)
function tp.placetrack(pos, nnpref, placer, itemstack, pointed_thing)
--1. find all rails that are likely to be connected
local tr=tp.tracks[nnpref]
local p_rails={}
@ -140,9 +140,16 @@ function tp.placetrack(pos, nnpref)
end
if #p_rails==0 then
minetest.set_node(pos, {name=nnpref.."_"..tr.default})
if minetest.registered_nodes[nnpref.."_"..tr.default] and minetest.registered_nodes[nnpref.."_"..tr.default].after_place_node then
minetest.registered_nodes[nnpref.."_"..tr.default].after_place_node(pos, placer, itemstack, pointed_thing)
end
elseif #p_rails==1 then
tp.bend_rail(pos, p_rails[1], nnpref)
minetest.set_node(pos, tr.single_conn[p_rails[1]])
local nname=tr.single_conn[p_rails[1]].name
if minetest.registered_nodes[nname] and minetest.registered_nodes[nname].after_place_node then
minetest.registered_nodes[nname].after_place_node(pos, placer, itemstack, pointed_thing)
end
else
--iterate subsets
for k1, conn1 in ipairs(p_rails) do
@ -152,6 +159,10 @@ function tp.placetrack(pos, nnpref)
tp.bend_rail(pos, conn1, nnpref)
tp.bend_rail(pos, conn2, nnpref)
minetest.set_node(pos, tr.double_conn[conn1.."_"..conn2])
local nname=tr.double_conn[conn1.."_"..conn2].name
if minetest.registered_nodes[nname] and minetest.registered_nodes[nname].after_place_node then
minetest.registered_nodes[nname].after_place_node(pos, placer, itemstack, pointed_thing)
end
return
end
end
@ -160,6 +171,10 @@ function tp.placetrack(pos, nnpref)
--not found
tp.bend_rail(pos, p_rails[1], nnpref)
minetest.set_node(pos, tr.single_conn[p_rails[1]])
local nname=tr.single_conn[p_rails[1]].name
if minetest.registered_nodes[nname] and minetest.registered_nodes[nname].after_place_node then
minetest.registered_nodes[nname].after_place_node(pos, placer, itemstack, pointed_thing)
end
end
end
@ -183,7 +198,7 @@ function tp.register_track_placer(nnprefix, imgprefix, dispname)
end
if minetest.registered_nodes[minetest.get_node(pos).name] and minetest.registered_nodes[minetest.get_node(pos).name].buildable_to
and minetest.registered_nodes[minetest.get_node(upos).name] and minetest.registered_nodes[minetest.get_node(upos).name].walkable then
tp.placetrack(pos, nnprefix)
tp.placetrack(pos, nnprefix, placer, itemstack, pointed_thing)
if not minetest.setting_getbool("creative_mode") then
itemstack:take_item()
end
@ -230,7 +245,7 @@ minetest.register_craftitem("advtrains:trackworker",{
local modext=tp.tracks[nnprefix].twrotate[suffix]
if rotation==modext[#modext] then --increase param2
minetest.set_node(pos, {name=nnprefix.."_"..suffix..modext[1], param2=(node.param2+1)%4})
minetest.swap_node(pos, {name=nnprefix.."_"..suffix..modext[1], param2=(node.param2+1)%4})
return
else
local modpos
@ -239,7 +254,7 @@ minetest.register_craftitem("advtrains:trackworker",{
minetest.chat_send_player(placer:get_player_name(), "This node can't be rotated using the trackworker!")
return
end
minetest.set_node(pos, {name=nnprefix.."_"..suffix..modext[modpos+1], param2=node.param2})
minetest.swap_node(pos, {name=nnprefix.."_"..suffix..modext[modpos+1], param2=node.param2})
end
advtrains.invalidate_all_paths()
end
@ -269,7 +284,7 @@ minetest.register_craftitem("advtrains:trackworker",{
end
end
local nextsuffix=tp.tracks[nnprefix].twcycle[suffix]
minetest.set_node(pos, {name=nnprefix.."_"..nextsuffix..rotation, param2=node.param2})
minetest.swap_node(pos, {name=nnprefix.."_"..nextsuffix..rotation, param2=node.param2})
--invalidate trains
advtrains.invalidate_all_paths()
else

View File

@ -36,7 +36,8 @@ advtrains.all_tracktypes={}
--definition preparation
local function conns(c1, c2, r1, r2, rh, rots) return {conn1=c1, conn2=c2, rely1=r1, rely2=r2, railheight=rh} end
local t_30deg={
local ap={}
ap.t_30deg={
regstep=1,
variant={
st=conns(0,8),
@ -110,7 +111,7 @@ local t_30deg={
rotation={"", "_30", "_45", "_60"},
increativeinv={},
}
local t_30deg_straightonly={
ap.t_30deg_straightonly={
regstep=1,
variant={
st=conns(0,8),
@ -135,7 +136,7 @@ local t_30deg_straightonly={
rotation={"", "_30", "_45", "_60"},
increativeinv={st},
}
local t_30deg_straightonly_noplacer={
ap.t_30deg_straightonly_noplacer={
regstep=1,
variant={
st=conns(0,8),
@ -160,7 +161,7 @@ local t_30deg_straightonly_noplacer={
rotation={"", "_30", "_45", "_60"},
increativeinv={st},
}
local t_45deg={
ap.t_45deg={
regstep=2,
variant={
st=conns(0,8),
@ -215,6 +216,7 @@ local t_45deg={
rotation={"", "_45"},
increativeinv={vst1=true, vst2=true}
}
advtrains.trackpresets = ap
--definition format: ([] optional)
--[[{
@ -302,6 +304,9 @@ function advtrains.register_tracks(tracktype, def, preset)
after_place_node=function(pos)
advtrains.reset_trackdb_position(pos)
end,
on_place_rail=function(pos)
advtrains.reset_trackdb_position(pos)
end,
}, def.common or {})
--make trackplacer base def
advtrains.trackplacer.register_tracktype(def.nodename_prefix, preset.tpdefault)
@ -386,10 +391,10 @@ advtrains.detector.clean_step_before = false
--The entry already being contained in advtrains.detector.on_node_restore will not trigger an on_train_enter event on the node. (when path is reset, this is saved).
function advtrains.detector.enter_node(pos, train_id)
local pts = minetest.pos_to_string(advtrains.round_vector_floor_y(pos))
print("enterNode "..pts.." "..sid(train_id))
--print("enterNode "..pts.." "..sid(train_id))
if advtrains.detector.on_node[pts] then
if advtrains.trains[advtrains.detector.on_node[pts]] then
print(""..pts.." already occupied")
--print(""..pts.." already occupied")
return false
else
advtrains.detector.leave_node(pos, advtrains.detector.on_node[pts])
@ -405,9 +410,9 @@ function advtrains.detector.enter_node(pos, train_id)
end
function advtrains.detector.leave_node(pos, train_id)
local pts = minetest.pos_to_string(advtrains.round_vector_floor_y(pos))
print("leaveNode "..pts.." "..sid(train_id))
--print("leaveNode "..pts.." "..sid(train_id))
if not advtrains.detector.on_node[pts] then
print(""..pts.." leave: nothing here")
--print(""..pts.." leave: nothing here")
return false
end
if advtrains.detector.on_node[pts]==train_id then
@ -415,7 +420,7 @@ function advtrains.detector.leave_node(pos, train_id)
advtrains.detector.on_node[pts]=nil
else
if advtrains.trains[advtrains.detector.on_node[pts]] then
print(""..pts.." occupied by another train")
--print(""..pts.." occupied by another train")
return false
else
advtrains.detector.leave_node(pos, advtrains.detector.on_node[pts])
@ -446,7 +451,10 @@ function advtrains.detector.call_enter_callback(pos, train_id)
local mregnode=minetest.registered_nodes[node.name]
if mregnode and mregnode.advtrains and mregnode.advtrains.on_train_enter then
mregnode.advtrains.on_train_enter(pos, train_id)
end
end
--atc code wants to be notified too
advtrains.atc.trigger_controller_train_enter(pos, train_id)
end
function advtrains.detector.call_leave_callback(pos, train_id)
--print("instructed to call leave calback")
@ -576,7 +584,7 @@ advtrains.register_tracks("regular", {
shared_model="trackplane.b3d",
description="Deprecated Track",
formats={vst1={}, vst2={}},
}, t_45deg)
}, ap.t_45deg)
advtrains.register_tracks("default", {
@ -587,7 +595,7 @@ advtrains.register_tracks("default", {
shared_texture="advtrains_dtrack_rail.png",
description="Track",
formats={vst1={true, false, true}, vst2={true, false, true}, vst31={true}, vst32={true}, vst33={true}},
}, t_30deg)
}, ap.t_30deg)
--bumpers
advtrains.register_tracks("default", {
@ -598,7 +606,7 @@ advtrains.register_tracks("default", {
shared_texture="advtrains_dtrack_rail.png",
description="Bumper",
formats={},
}, t_30deg_straightonly)
}, ap.t_30deg_straightonly)
--legacy bumpers
for _,rot in ipairs({"", "_30", "_45", "_60"}) do
minetest.register_alias("advtrains:dtrack_bumper"..rot, "advtrains:dtrack_bumper_st"..rot)
@ -629,7 +637,7 @@ if mesecon then
}
}
end
}, t_30deg_straightonly)
}, ap.t_30deg_straightonly)
advtrains.register_tracks("default", {
nodename_prefix="advtrains:dtrack_detector_on",
texture_prefix="advtrains_dtrack_detector",
@ -654,7 +662,7 @@ if mesecon then
}
}
end
}, t_30deg_straightonly_noplacer)
}, ap.t_30deg_straightonly_noplacer)
end
--TODO legacy
--I know lbms are better for this purpose

View File

@ -11,7 +11,7 @@ local mletter={[1]="F", [-1]="R", [0]="N"}
function advtrains.on_control_change(pc, train, flip)
if pc.sneak then
if pc.up then
train.tarvelocity = advtrains.all_traintypes[train.traintype].max_speed or 10
train.tarvelocity = train.max_speed or 10
end
if pc.down then
train.tarvelocity = 0

View File

@ -119,6 +119,7 @@ advtrains.save = function()
file:close()
advtrains.save_trackdb()
advtrains.atc.save()
end
minetest.register_on_shutdown(advtrains.save)
@ -302,6 +303,24 @@ function advtrains.train_step(id, train, dtime)
if train.locomotives_in_train==0 then
train.tarvelocity=0
end
--interpret ATC command
if train.atc_brake_target and train.atc_brake_target>=train.velocity then
train.atc_brake_target=nil
end
if train.atc_wait_finish then
if not train.atc_brake_target and train.velocity==train.tarvelocity then
train.atc_wait_finish=nil
end
end
if train.atc_command then
if train.atc_delay<=0 and not train.atc_wait_finish then
advtrains.atc.execute_atc_command(id, train)
else
train.atc_delay=train.atc_delay-dtime
end
end
--make brake adjust the tarvelocity if necessary
if train.brake and (math.ceil(train.velocity)-1)<train.tarvelocity then
train.tarvelocity=math.max((math.ceil(train.velocity)-1), 0)
@ -318,7 +337,7 @@ function advtrains.train_step(id, train, dtime)
if front_off_track or back_off_track or train.recently_collided_with_env then --every wagon has a brake, so not divided by mass.
--print("braking with emergency force")
applydiff= -(math.min((advtrains.train_emerg_force*dtime), math.abs(diff)))
elseif train.brake then
elseif train.brake or (train.atc_brake_target and train.atc_brake_target<train.velocity) then
--print("braking with default force")
--no math.min, because it can grow beyond tarvelocity, see up there
--dont worry, it will never fall below zero.