aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--advtrains/helpers.lua4
-rw-r--r--advtrains_interlocking/database.lua192
-rw-r--r--advtrains_interlocking/init.lua1
-rw-r--r--advtrains_interlocking/tcb.lua180
4 files changed, 377 insertions, 0 deletions
diff --git a/advtrains/helpers.lua b/advtrains/helpers.lua
index ce059de..2160444 100644
--- a/advtrains/helpers.lua
+++ b/advtrains/helpers.lua
@@ -334,4 +334,8 @@ function advtrains.random_id()
end
return idst
end
+-- Shorthand for pos_to_string and round_vector_floor_y
+function advtrains.roundfloorpts(pos)
+ return minetest.pos_to_string(advtrains.round_vector_floor_y(pos))
+end
diff --git a/advtrains_interlocking/database.lua b/advtrains_interlocking/database.lua
index 61bf5ad..e318dd2 100644
--- a/advtrains_interlocking/database.lua
+++ b/advtrains_interlocking/database.lua
@@ -32,6 +32,9 @@ Route setting fails whenever any TC that we want to set ROUTE to is already set
Apart from this, we need to set turnouts
- Turnouts on the track are set held as ROUTE
- Turnouts that purpose as flank protection are set held as FLANK (NOTE: left as an idea for later, because it's not clear how to do this properly without an engineer)
+Note: In SimSig, it is possible to set a route into an still occupied section on the victoria line sim. (at the depot exit at seven sisters), although
+ there are still segments set ahead of the first train passing, remaining from another route.
+ Because our system will be able to remember "requested routes" and set them automatically once ready, this is not necessary here.
== Call-On/Multiple Trains ==
It will be necessary to join and split trains using call-on routes. A call-on route may be set when:
- there are no ROUTE reservations
@@ -50,7 +53,196 @@ CALL_ON_ALLOWED - Whether this TC being blocked (TRAIN or ROUTE) does not preven
== More notes ==
- It may not be possible to switch turnouts when their TC has any state entry
+== Route releasing (TORR) ==
+A train passing through a route happens as follows:
+Route set from entry to exit signal
+Train passes entry signal and enters first TC past the signal
+-> Route from signal cleared (TCs remain locked)
+-> ROUTE status of first TC past signal cleared
+Train continues along the route.
+Whenever train leaves a TC
+-> Clearing any routes set from this TC outward recursively - see "Reversing problem"
+Whenever train enters a TC
+-> Clear route status from the just entered TC
+== Reversing Problem ==
+Encountered at the Royston simulation in SimSig. It is solved there by imposing a time limit on the set route. Call-on routes can somehow be set anyway.
+Imagine this setup: (T=Train, R=Route, >=in_dir TCB)
+ O-| Royston P2 |-O
+T->---|->RRR-|->RRR-|--
+Train T enters from the left, the route is set to the right signal. But train is supposed to reverse here and stops this way:
+ O-| Royston P2 |-O
+------|-TTTT-|->RRR-|--
+The "Route" on the right is still set. Imposing a timeout here is a thing only professional engineers can determine, not an algorithm.
+ O-| Royston P2 |-O
+<-T---|------|->RRR-|--
+The train has left again, while route on the right is still set.
+So, we have to clear the set route when the train has left the left TC.
+This does not conflict with call-on routes, because both station tracks are set as "allow call-on"
+Because none of the routes extends past any non-call-on sections, call-on route would be allowed here, even though the route
+is locked in opposite direction at the time of routesetting.
+Another case of this:
+--TTT/--|->RRR--
+The / here is a non-interlocked turnout (to a non-frequently used siding). For some reason, there is no exit node there,
+so the route is set to the signal at the right end. The train is taking the exit to the siding and frees the TC, without ever
+having touched the right TC.
]]--
+local TRAVERSER_LIMIT = 100
+
+
+local ildb = {}
+
+local track_circuit_breaks = {}
+
+function ildb.load(data)
+
+end
+
+function ildb.save()
+ return {}
+end
+
+--
+--[[
+TCB data structure
+{
+[1] = { -- Variant: with adjacent TCs.
+ == Synchronized properties == Apply to the whole TC
+ adjacent = { <signal specifier>,... } -- Adjacent TCBs, forms a TC with these
+ conflict = { <signal specifier>,... } -- Conflicting TC's (chosen as a representative TCB member)
+ -- Used e.g. for crossing rails that do not have nodes in common (like it's currently done)
+ incomplete = <boolean> -- Set when the recursion counter hit during traverse. Probably needs to add
+ -- another tcb at some far-away place
+ route = {origin = <signal>, in_dir = <boolean>}
+ -- Set whenever a route has been set through this TC. It saves the origin tcb id and side
+ -- (=the origin signal). in_dir is set when the train will enter the TC from this side
+
+ == Unsynchronized properties == Apply only to this side of the TC
+ signal = <pos> -- optional: when set, routes can be set from this tcb/direction and signal
+ -- aspect will be set accordingly.
+ routetar = <signal> -- Route set from this signal. This is the entry that is cleared once
+ -- train has passed the signal. (which will set the aspect to "danger" again)
+ route_committed = <boolean> -- When setting/requesting a route, routetar will be set accordingly,
+ -- while the signal still displays danger and nothing is written to the TCs
+ -- As soon as the route can actually be set, all relevant TCs and turnouts are set and this field
+ -- is set true, clearing the signal
+},
+[2] = { -- Variant: end of track-circuited area (initial state of TC)
+ end_of_interlocking = true,
+ section_free = <boolean>, --this can be set by an exit node via mesecons or atlatc,
+ -- or from the tc formspec.
+}
+}
+Signal specifier (a pair of TCB/Side):
+{p = <pos>, s = <1/2>}
+]]
+
+
+--
+function ildb.create_tcb(pos)
+ local new_tcb = {
+ [1] = {end_of_interlocking = true},
+ [2] = {end_of_interlocking = true},
+ }
+ local pts = advtrains.roundfloorpts(pos)
+ track_circuit_breaks[pts] = new_tcb
+end
+
+function ildb.get_tcb(pos)
+ local pts = advtrains.roundfloorpts(pos)
+ return track_circuit_breaks[pts]
+end
+
+-- This function will actually handle the node that is in connid direction from the node at pos
+-- so, this needs the conns of the node at pos, since these are already calculated
+local function traverser(found_tcbs, pos, conns, connid, count)
+ local adj_pos, adj_connid, conn_idx, nextrail_y, next_conns = advtrains.get_adjacent_rail(pos, conns, connid, advtrains.all_tracktypes)
+ if not adj_pos then
+ -- end of track
+ return
+ end
+ -- look whether there is a TCB here
+ if #next_conns == 2 then --if not, don't even try!
+ local tcb = ildb.get_tcb(adj_pos)
+ if tcb then
+ -- done with this branch
+ table.insert(found_tcbs, {p=adj_pos, s=adj_connid})
+ return
+ end
+ end
+ -- recursion abort condition
+ if count > TRAVERSER_LIMIT then
+ atdebug("Traverser hit counter at",adj_pos, adj_connid,"found tcb's:",found_tcbs)
+ return true
+ end
+ -- continue traversing
+ local counter_hit = false
+ for nconnid, nconn in ipairs(next_conns) do
+ if adj_connid ~= nconnid then
+ counter_hit = counter_hit or traverser(found_tcbs, adj_pos, next_conns, nconnid, count + 1, hit_counter)
+ end
+ end
+ return counter_hit
+end
+
+local function sigd_equal(sigd, cmp)
+ return vector.equals(sigd.p, cmp.p) and sigd.s==cmp.s
+end
+
+
+
+
+
+-- Updates the neighbors of this TCB using the traverser function (see comments above)
+-- returns true if the traverser hit the counter, which means that there could be another
+-- TCB outside of the traversed range.
+function ildb.update_tcb_neighbors(pos, connid)
+ local found_tcbs = { {p = pos, s = connid} }
+ local node_ok, conns, rhe = advtrains.get_rail_info_at(pos, advtrains.all_tracktypes)
+ if not node_ok then
+ error("update_tcb_neighbors but node is NOK: "..minetest.pos_to_string(pos))
+ end
+
+ local counter_hit = traverser(found_tcbs, pos, conns, connid, 0, hit_counter)
+
+ for idx, sigd in pairs(found_tcbs) do
+ local tcb = ildb.get_tcb(sigd.p)
+ local tcbs = tcb[sigd.s]
+
+ tcbs.end_of_interlocking = nil
+ tcbs.incomplete = counter_hit
+ tcbs.adjacent = {}
+
+ for idx2, other_sigd in pairs(found_tcbs) do
+ if idx~=idx2 then
+ ildb.add_adjacent(tcbs, sigd.p, sigd.s, other_sigd)
+ end
+ end
+ end
+
+ return hit_counter
+end
+
+-- Add the adjacency entry into the tcbs, but without duplicating it
+-- and without adding a self-reference
+function ildb.add_adjacent(tcbs, this_pos, this_connid, sigd)
+ if sigd_equal(sigd, {p=this_pos, s=this_connid}) then
+ return
+ end
+ tcbs.end_of_interlocking = nil
+ if not tcbs.adjacent then
+ tcbs.adjacent = {}
+ end
+ for idx, cmp in pairs(tcbs.adjacent) do
+ if sigd_equal(sigd, cmp) then
+ return
+ end
+ end
+ table.insert(tcbs.adjacent, sigd)
+end
+
+advtrains.interlocking.db = ildb
+
+
diff --git a/advtrains_interlocking/init.lua b/advtrains_interlocking/init.lua
index ab79573..e3be234 100644
--- a/advtrains_interlocking/init.lua
+++ b/advtrains_interlocking/init.lua
@@ -6,5 +6,6 @@ advtrains.interlocking = {}
local modpath = minetest.get_modpath(minetest.get_current_modname()) .. DIR_DELIM
dofile(modpath.."database.lua")
+dofile(modpath.."tcb.lua")
dofile(modpath.."signal_api.lua")
dofile(modpath.."demosignals.lua")
diff --git a/advtrains_interlocking/tcb.lua b/advtrains_interlocking/tcb.lua
new file mode 100644
index 0000000..742bb62
--- /dev/null
+++ b/advtrains_interlocking/tcb.lua
@@ -0,0 +1,180 @@
+-- Track Circuit Breaks - Player interaction
+
+local players_assign_tcb = {}
+local players_addfar_tcb = {}
+
+local lntrans = { "A", "B" }
+
+local function sigd_to_string(sigd)
+ return minetest.pos_to_string(sigd.p).." / "..lntrans[sigd.s]
+end
+
+minetest.register_node("advtrains_interlocking:tcb_node", {
+ drawtype = "mesh",
+ paramtype="light",
+ paramtype2="facedir",
+ walkable = true,
+ selection_box = {
+ type = "fixed",
+ fixed = {-1/4, -1/2, -1/4, 1/4, 1/2, 1/4},
+ },
+ mesh = "at_il_tcb_node.obj",
+ tiles = {"at_il_tcb_node.png"},
+ description="Track Circuit Break",
+ sunlight_propagates=true,
+ groups = {
+ cracky=3,
+ not_blocking_trains=1,
+ --save_in_at_nodedb=2,
+ },
+ after_place_node = function(pos, node, player)
+ local meta = minetest.get_meta(pos)
+ meta:set_string("infotext", "Unconfigured Track Circuit Break, right-click to assign.")
+ end,
+ on_rightclick = function(pos, node, player)
+ local meta = minetest.get_meta(pos)
+ local tcbpts = meta:get_string("tcb_pos")
+ local pname = player:get_player_name()
+ if tcbpts ~= "" then
+ local tcbpos = minetest.string_to_pos(tcbpts)
+ advtrains.interlocking.show_tcb_form(tcbpos, pname)
+ else
+ --unconfigured
+ --TODO security
+ minetest.chat_send_player(pname, "Configuring TCB: Please punch the rail you want to assign this TCB to.")
+
+ players_assign_tcb[pname] = pos
+ end
+ end,
+ on_punch = function(pos, node, player)
+ local meta = minetest.get_meta(pos)
+ atwarn("Would show tcb marker.")
+ -- TODO TCB-Marker anzeigen
+ end,
+})
+
+minetest.register_on_punchnode(function(pos, node, player, pointed_thing)
+ local pname = player:get_player_name()
+ local tcbnpos = players_assign_tcb[pname]
+ if tcbnpos then
+ if vector.distance(pos, tcbnpos)<=20 then
+ local node_ok, conns, rhe = advtrains.get_rail_info_at(pos, advtrains.all_tracktypes)
+ if node_ok and #conns == 2 then
+ advtrains.interlocking.db.create_tcb(pos)
+
+ advtrains.interlocking.db.update_tcb_neighbors(pos, 1)
+ advtrains.interlocking.db.update_tcb_neighbors(pos, 2)
+
+ local meta = minetest.get_meta(tcbnpos)
+ meta:set_string("tcb_pos", minetest.pos_to_string(pos))
+ meta:set_string("infotext", "TCB assigned to "..minetest.pos_to_string(pos))
+ minetest.chat_send_player(pname, "Configuring TCB: Successfully configured TCB")
+ else
+ minetest.chat_send_player(pname, "Configuring TCB: This is not a normal two-connection rail! Aborted.")
+ end
+ else
+ minetest.chat_send_player(pname, "Configuring TCB: Node is too far away. Aborted.")
+ end
+ players_assign_tcb[pname] = nil
+ end
+end)
+
+
+local function mkformspec(tcbs, btnpref, offset, pname)
+ local form = "label[0.5,"..offset..";Side "..btnpref..": "..(tcbs.end_of_interlocking and "End of interlocking" or "Track Circuit").."]"
+ if tcbs.end_of_interlocking then
+ form = form.."button[0.5,"..(offset+1)..";3,1;"..btnpref.."_clearadj;Activate Interlocking]"
+ if tcbs.section_free then
+ form = form.."button[4.5,"..(offset+1)..";3,1;"..btnpref.."_setlocked;Section is free]"
+ else
+ form = form.."button[4.5,"..(offset+1)..";3,1;"..btnpref.."_setfree;Section is blocked]"
+ end
+ else
+ local strtab = {}
+ for idx, sigd in ipairs(tcbs.adjacent) do
+ strtab[idx] = minetest.formspec_escape(sigd_to_string(sigd))
+ end
+ form = form.."textlist[0.5,"..(offset+1)..";5,3;"..btnpref.."_adjlist;"..table.concat(strtab, ",").."]"
+ if players_addfar_tcb[pname] then
+ local sigd = players_addfar_tcb[pname]
+ form = form.."button[5.5,"..(offset+2)..";2.5,1;"..btnpref.."_addadj;Link TCB to "..sigd_to_string(sigd).."]"
+ form = form.."button[8,"..(offset+2)..";0.5,1;"..btnpref.."_canceladdadj;X]"
+ else
+ form = form.."button[5.5,"..(offset+2)..";3,1;"..btnpref.."_addadj;Add far TCB]"
+ end
+ form = form.."button[5.5,"..(offset+1)..";3,1;"..btnpref.."_clearadj;Clear&Update]"
+ form = form.."button[5.5,"..(offset+3)..";3,1;"..btnpref.."_mknonint;Make non-interlocked]"
+ if tcbs.incomplete then
+ form = form.."label[0.5,"..(offset+0.5)..";Warning: You possibly need to add TCBs manually!]"
+ end
+ end
+ return form
+end
+
+
+
+function advtrains.interlocking.show_tcb_form(pos, pname)
+ local tcb = advtrains.interlocking.db.get_tcb(pos)
+ if not tcb then return end
+
+ local form = "size[10,10] label[0.5,0.5;Track Circuit Break Configuration]"
+ form = form .. mkformspec(tcb[1], "A", 1, pname)
+ form = form .. mkformspec(tcb[2], "B", 6, pname)
+
+ minetest.show_formspec(pname, "at_il_tcbconfig_"..minetest.pos_to_string(pos), form)
+end
+
+
+minetest.register_on_player_receive_fields(function(player, formname, fields)
+ local pname = player:get_player_name()
+ local pts = string.match(formname, "^at_il_tcbconfig_(.+)$")
+ local pos
+ if pts then
+ pos = minetest.string_to_pos(pts)
+ end
+ if pos and not fields.quit then
+ local tcb = advtrains.interlocking.db.get_tcb(pos)
+ if not tcb then return end
+ local f_clearadj = {fields.A_clearadj, fields.B_clearadj}
+ local f_addadj = {fields.A_addadj, fields.B_addadj}
+ local f_canceladdadj = {fields.A_canceladdadj, fields.B_canceladdadj}
+ local f_setlocked = {fields.A_setlocked, fields.B_setlocked}
+ local f_setfree = {fields.A_setfree, fields.B_setfree}
+ local f_mknonint = {fields.A_mknonint, fields.B_mknonint}
+
+ for connid=1,2 do
+ if f_clearadj[connid] then
+ advtrains.interlocking.db.update_tcb_neighbors(pos, connid)
+ end
+ if f_mknonint[connid] then
+ --TODO: remove this from the other tcb's
+ tcb[connid].end_of_interlocking = true
+ end
+ if f_addadj[connid] then
+ if players_addfar_tcb[pname] then
+ local sigd = players_addfar_tcb[pname]
+ advtrains.interlocking.db.add_adjacent(tcb[connid], pos, connid, sigd)
+ players_addfar_tcb[pname] = nil
+ else
+ players_addfar_tcb[pname] = {p = pos, s = connid}
+ end
+ end
+ if f_canceladdadj[connid] then
+ players_addfar_tcb[pname] = nil
+ end
+ if f_setfree[connid] then
+ tcb[connid].section_free = true
+ end
+ if f_setlocked[connid] then
+ tcb[connid].section_free = nil
+ end
+ end
+ advtrains.interlocking.show_tcb_form(pos, pname)
+ end
+
+end)
+
+
+
+
+