-- interlocking/database.lua-- saving the location of TCB's, their neighbors and their state--[[== THIS COMMENT IS PARTIALLY INCORRECT AND OUTDATED! ==The interlocking system is based on track circuits.Track circuit breaks must be manually set by the user. Signals must be assigned to track circuit breaks and to a direction(connid).To simplify the whole system, there is no overlap.== Trains ==Trains always occupy certain track circuits. These are shown red in the signalbox view (TRAIN occupation entry).== Database storage ==The things that are actually saved are the Track Circuit Breaks. Each TCB holds a list of the TCBs that are adjacent in each direction.TC occupation/state is then saved inside each (TCB,Direction) and held in sync across all TCBs adjacent to this one. If something should not be in sync,all entries are merged to perform the most restrictive setup.== Traverser function ==To determine and update the list of neighboring TCBs, we need a traverser function.It will start at one TCB in a specified direction (connid) and use get_adjacent_rail to crawl along the track. When encountering a turnout or a crossing,it needs to branch(call itself recursively) to find all required TCBs. Those found TCBs are then saved in a list as tuples (TCB,Dir)In the last step, they exchange their neighbors.== TC states ==A track circuit does not have a state as such, but has more or less a list of "reservations"type can be one of these:TRAIN See Trains oboveROUTE Route set from a signal, but no train has yet passed that signal.Not implemented (see note by reversible): OWNED - former ROUTE segments that a train has begun passing (train_id assigned) - Space behind a train up to the next signal, when a TC is set as REVERSIBLECertain TCs can be marked as "allow call-on".== Route setting: ==Routes are set from a signal (the entry signal) to another signal facing the same direction (the exit signal)Remember that signals are assigned to a TCB and a connid.Whenever this is done, the following track circuits are set "reserved" by the train by saving the entry signal's ID:- all TCs on the direct way of the route - set as ROUTERoute setting fails whenever any TC that we want to set ROUTE to is already set ROUTE or TRAIN from another signal (except call-on, see below)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- there are TRAIN reservations only inside TCs that have "allow call-on" set== TC Properties ==Note: Reversible property will not be implemented, assuming everything as non-rev.This is sufficient to cover all use cases, and is done this way in reality. REVERSIBLE - Whether trains are allowed to reverse while on track circuit This property is supposed to be set for station tracks, where there is a signal at each end, and for sidings. It should in no case be set for TCs covering turnouts, or for main running lines. When a TC is not set as reversible, the OWNED status is cleared from the TC right after the train left it, to allow other trains to pass it. If it is set reversible, interlocking will keep the OWNED state behind the train up to the next signal, clearing it as soon as the train passes another signal or enters a non-reversible section.CALL_ON_ALLOWED - Whether this TC being blocked (TRAIN or ROUTE) does not prevent shunt routes being set through this TC== 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 signalTrain passes entry signal and enters first TC past the signal-> Route from signal cleared (TCs remain locked)-> ROUTE status of first TC past signal clearedTrain 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 TCNote that this prohibits by design that the train clears the route ahead of it.== 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 |-OT->---|->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 routeis 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 everhaving touched the right TC.]]--local TRAVERSER_LIMIT =1000local ildb = {}local track_circuit_breaks = {}local track_sections = {}-- Assignment of signals to TCBslocal signal_assignments = {}-- track+direction -> signal positionlocal influence_points = {}
advtrains.interlocking.npr_rails = {}function ildb.load(data)if not data then return endif data.tcbs then
track_circuit_breaks = data.tcbs
endif data.ts then
track_sections = data.ts
endif data.signalass then
signal_assignments = data.signalass
endif data.rs_locks then
advtrains.interlocking.route.rte_locks = data.rs_locks
endif data.rs_callbacks then
advtrains.interlocking.route.rte_callbacks = data.rs_callbacks
endif data.influence_points then
influence_points = data.influence_points
endif data.npr_rails then
advtrains.interlocking.npr_rails = data.npr_rails
end--COMPATIBILITY to Signal aspect format-- TODO remove in time...for pts,tcb inpairs(track_circuit_breaks)dofor connid, tcbs inipairs(tcb)doif tcbs.routes thenfor _,route inipairs(tcbs.routes)doif route.aspect then-- transform the signal aspect formatlocal asp = route.aspect
iftype(asp.main) =="table"thenatwarn("Transforming route aspect of signal",pts,"/",connid,"")if asp.main.free then
asp.main = asp.main.speed
else
asp.main =0endif asp.dst.free then
asp.dst = asp.dst.speed
else
asp.dst =0end
asp.proceed_as_main = asp.shunt.proceed_as_main
asp.shunt = asp.shunt.free
-- Note: info table not transferred, it's not used right nowendendendendendendendfunction ildb.save()return{
tcbs = track_circuit_breaks,
ts=track_sections,
signalass = signal_assignments,
rs_locks = advtrains.interlocking.route.rte_locks,
rs_callbacks = advtrains.interlocking.route.rte_callbacks,
influence_points = influence_points,
npr_rails = advtrains.interlocking.npr_rails,}end----[[TCB data structure{-- This is the "A" side of the TCB[1] = { -- Variant: with adjacent TCs. ts_id = <id> -- ID of the assigned track section signal = <pos> -- optional: when set, routes can be set from this tcb/direction and signal -- aspect will be set accordingly. routeset = <index in routes> -- 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 aspect = <asp> -- The aspect the signal should show. If this is nil, should show the most restrictive aspect (red) signal_name = <string> -- The human-readable name of the signal, only for documenting purposes routes = { <route definition> } -- a collection of routes from this signal route_auto = <boolean> -- When set, we will automatically re-set the route (designated by routeset)},-- This is the "B" side of the TCB[2] = { -- Variant: end of track-circuited area (initial state of TC) ts_id = nil, -- this is the indication for end_of_interlocking section_free = <boolean>, --this can be set by an exit node via mesecons or atlatc, -- or from the tc formspec.}}Track section[id] = { name = "Some human-readable name" tc_breaks = { <signal specifier>,... } -- Bounding TC's (signal specifiers) -- Can be direct ends (auto-detected), conflicting routes or TCBs that are too far away from each other route = { origin = <signal>, -- route origin entry = <sigd>, -- supposed train entry point rsn = <string>, first = <bool> } route_post = { locks = {[n] = <pts>} next = <sigd> } -- Set whenever a route has been set through this TC. It saves the origin tcb id and side -- (=the origin signal). rsn is some description to be shown to the user -- first says whether to clear the routesetting status from the origin signal. -- locks contains the positions where locks are held by this ts. -- 'route' is cleared when train enters the section, while 'route_post' cleared when train leaves section. trains = {<id>, ...} -- Set whenever a train (or more) reside in this TC}Signal specifier (sigd) (a pair of TCB/Side):{p = <pos>, s = <1/2>}Signal Assignments: reverse lookup of signals assigned to TCBssignal_assignments = {[<signal pts>] = <sigd>}]]--function ildb.create_tcb(pos)local new_tcb = {[1] = {},[2] = {},}local pts = advtrains.roundfloorpts(pos)if not track_circuit_breaks[pts]then
track_circuit_breaks[pts] = new_tcb
return trueelsereturn falseendendfunction ildb.get_tcb(pos)local pts = advtrains.roundfloorpts(pos)return track_circuit_breaks[pts]endfunction ildb.get_tcbs(sigd)local tcb = ildb.get_tcb(sigd.p)if not tcb then return nil endreturn tcb[sigd.s]endfunction ildb.create_ts(sigd)local tcbs = ildb.get_tcbs(sigd)local id = advtrains.random_id()while track_sections[id]do
id = advtrains.random_id()end
track_sections[id] = {
name ="Section "..id,
tc_breaks = { sigd }}
tcbs.ts_id = id
endfunction ildb.get_ts(id)return track_sections[id]end-- various helper functions handling sigd'slocal sigd_equal = advtrains.interlocking.sigd_equal
local functioninsert_sigd_nodouble(list, sigd)for idx, cmp inpairs(list)doifsigd_equal(sigd, cmp)thenreturnendendtable.insert(list, sigd)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 calculatedlocal functiontraverser(found_tcbs, pos, conns, connid, count, brk_when_found_n)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--atdebug("Traverser found end-of-track at",pos, connid)returnend-- look whether there is a TCB hereif#next_conns ==2then--if not, don't even try!local tcb = ildb.get_tcb(adj_pos)if tcb then-- done with this branch--atdebug("Traverser found tcb at",adj_pos, adj_connid)insert_sigd_nodouble(found_tcbs, {p=adj_pos, s=adj_connid})returnendend-- recursion abort conditionif count > TRAVERSER_LIMIT then--atdebug("Traverser hit counter at",adj_pos, adj_connid)return trueend-- continue traversinglocal counter_hit =falsefor nconnid, nconn inipairs(next_conns)doif adj_connid ~= nconnid then
counter_hit = counter_hit ortraverser(found_tcbs, adj_pos, next_conns, nconnid, count +1, brk_when_found_n)if brk_when_found_n and#found_tcbs>=brk_when_found_n thenbreakendendendreturn counter_hit
end-- Merges the TS with merge_id into root_id and then deletes merge_idlocal functionmerge_ts(root_id, merge_id)local rts = ildb.get_ts(root_id)local mts = ildb.get_ts(merge_id)if not mts then return end-- This may be the case when sync_tcb_neighbors-- inserts the same id twice. do nothing.if not ildb.may_modify_ts(rts)then return false endif not ildb.may_modify_ts(mts)then return false end-- cobble together the list of TCBsfor _, msigd inipairs(mts.tc_breaks)dolocal tcbs = ildb.get_tcbs(msigd)if tcbs theninsert_sigd_nodouble(rts.tc_breaks, msigd)
tcbs.ts_id = root_id
end
advtrains.interlocking.show_tcb_marker(msigd.p)end-- done
track_sections[merge_id] =nilendlocal lntrans = {"A","B"}local functionsigd_to_string(sigd)return minetest.pos_to_string(sigd.p).." / "..lntrans[sigd.s]end-- Check for near TCBs and connect to their TS if they have one, and syncs their data.function ildb.sync_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 thenatwarn("update_tcb_neighbors but node is NOK: "..minetest.pos_to_string(pos))returnend--atdebug("Traversing from ",pos, connid)local counter_hit =traverser(found_tcbs, pos, conns, connid,0)local ts_id
local list_eoi = {}local list_ok = {}local list_mismatch = {}local ts_to_merge = {}for idx, sigd inpairs(found_tcbs)dolocal tcbs = ildb.get_tcbs(sigd)if not tcbs.ts_id then--atdebug("Sync: put",sigd_to_string(sigd),"into list_eoi")table.insert(list_eoi, sigd)elseif not ts_id and tcbs.ts_id thenif not ildb.get_ts(tcbs.ts_id)thenatwarn("Track section database is inconsistent, there's no TS with ID=",tcbs.ts_id)
tcbs.ts_id =niltable.insert(list_eoi, sigd)else--atdebug("Sync: put",sigd_to_string(sigd),"into list_ok")
ts_id = tcbs.ts_id
table.insert(list_ok, sigd)endelseif ts_id and tcbs.ts_id and tcbs.ts_id ~= ts_id thenatwarn("Track section database is inconsistent, sections share track!")atwarn("Merging",tcbs.ts_id,"into",ts_id,".")table.insert(list_mismatch, sigd)table.insert(ts_to_merge, tcbs.ts_id)endendif ts_id thenlocal ts = ildb.get_ts(ts_id)for _, sigd inipairs(list_eoi)dolocal tcbs = ildb.get_tcbs(sigd)
tcbs.ts_id = ts_id
table.insert(ts.tc_breaks, sigd)
advtrains.interlocking.show_tcb_marker(sigd.p)endfor _, mts inipairs(ts_to_merge)domerge_ts(ts_id, mts)endendendfunction ildb.link_track_sections(merge_id, root_id)if merge_id == root_id thenreturnendmerge_ts(root_id, merge_id)endfunction ildb.remove_from_interlocking(sigd, no_tcb_marker)local tcbs = ildb.get_tcbs(sigd)if not ildb.may_modify_tcbs(tcbs)then return false endif tcbs.ts_id thenlocal tsid = tcbs.ts_id
local ts = ildb.get_ts(tsid)if not ts then
tcbs.ts_id =nilreturn trueend-- remove entry from the listlocal idx =1while idx <= #ts.tc_breaks dolocal cmp = ts.tc_breaks[idx]ifsigd_equal(sigd, cmp)thentable.remove(ts.tc_breaks, idx)else
idx = idx +1endend
tcbs.ts_id =nil--ildb.sync_tcb_neighbors(sigd.p, sigd.s)if#ts.tc_breaks ==0then
track_sections[tsid] =nilendendif not no_tcb_marker then
advtrains.interlocking.show_tcb_marker(sigd.p)endif tcbs.signal thenreturn falseendreturn trueendfunction ildb.remove_tcb(pos)local pts = advtrains.roundfloorpts(pos)if not track_circuit_breaks[pts]thenreturn true--FIX: not an error, because tcb is already removedendfor connid=1,2doif not ildb.remove_from_interlocking({p=pos, s=connid},true)thenreturn falseendend
advtrains.interlocking.remove_tcb_marker_pts(pts)
track_circuit_breaks[pts] =nilreturn trueendfunction ildb.dissolve_ts(ts_id)local ts = ildb.get_ts(ts_id)if not ildb.may_modify_ts(ts)then return false endlocal tcbr = advtrains.merge_tables(ts.tc_breaks)for _,sigd inipairs(tcbr)do
ildb.remove_from_interlocking(sigd)end-- Note: ts gets removed in the moment of the removal of the last TCB.return trueend-- Returns true if it is allowed to modify any property of a track section, such as-- - removing TCBs-- - merging and dissolving sections-- As of now the action will be denied if a route is set or if a train is in the section.function ildb.may_modify_ts(ts)if ts.route or ts.route_post or(ts.trains and#ts.trains>0)thenreturn falseendreturn trueendfunction ildb.may_modify_tcbs(tcbs)if tcbs.ts_id thenlocal ts = ildb.get_ts(tcbs.ts_id)if ts and not ildb.may_modify_ts(ts)thenreturn falseendendreturn trueend-- Utilize the traverser to find the track section at the specified position-- Returns:-- ts_id, origin - the first found ts and the sigd of the found tcb-- nil - there were no TCBs in TRAVERSER_MAX range of the position-- false - the first found TCB stated End-Of-Interlocking, or track ends were reachedfunction ildb.get_ts_at_pos(pos)local node_ok, conns, rhe = advtrains.get_rail_info_at(pos, advtrains.all_tracktypes)if not node_ok thenerror("get_ts_at_pos but node is NOK: "..minetest.pos_to_string(pos))endlocal limit_hit =falselocal found_tcbs = {}for connid, conn inipairs(conns)do-- Note: a breadth-first-search would be better for performance
limit_hit = limit_hit ortraverser(found_tcbs, pos, conns, connid,0,1)if#found_tcbs >=1thenlocal tcbs = ildb.get_tcbs(found_tcbs[1])local ts
if tcbs.ts_id thenreturn tcbs.ts_id, found_tcbs[1]elsereturn falseendendendif limit_hit then-- there was at least one limit hitreturn nilelse-- all traverser ends were track endsreturn falseendend-- returns the sigd the signal at pos belongs to, if this is knownfunction ildb.get_sigd_for_signal(pos)local pts = advtrains.roundfloorpts(pos)local sigd = signal_assignments[pts]if sigd thenif not ildb.get_tcbs(sigd)then
signal_assignments[pts] =nilreturn nilendreturn sigd
endreturn nilendfunction ildb.set_sigd_for_signal(pos, sigd)local pts = advtrains.roundfloorpts(pos)
signal_assignments[pts] = sigd
end-- checks if there's any influence point set to this position-- if purge is true, checks whether the associated signal still exists-- and deletes the ip if not.function ildb.is_ip_at(pos, purge)local pts = advtrains.roundfloorpts(pos)if influence_points[pts]thenif purge then-- is there still a signal assigned to it?for connid, sigpos inpairs(influence_points[pts])dolocal asp = advtrains.interlocking.signal_get_aspect(sigpos)if not asp thenatlog("Clearing orphaned signal influence point", pts,"/", connid)
ildb.clear_ip_signal(pts, connid)endend-- if there's no side left after purging, return falseif not influence_points[pts]then return false endendreturn trueendreturn falseend-- checks if a signal is influencing herefunction ildb.get_ip_signal(pts, connid)if influence_points[pts]thenreturn influence_points[pts][connid]endend-- Tries to get aspect to obey here, if there-- is a signal ip at this location-- auto-clears invalid assignmentsfunction ildb.get_ip_signal_asp(pts, connid)local p = ildb.get_ip_signal(pts, connid)if p thenlocal asp = advtrains.interlocking.signal_get_aspect(p)if not asp thenatlog("Clearing orphaned signal influence point", pts,"/", connid)
ildb.clear_ip_signal(pts, connid)return nilendreturn asp, p
endreturn nilend-- set signal assignment.function ildb.set_ip_signal(pts, connid, spos)
ildb.clear_ip_by_signalpos(spos)if not influence_points[pts]then
influence_points[pts] = {}end
influence_points[pts][connid] = spos
end-- clear signal assignment.function ildb.clear_ip_signal(pts, connid)
influence_points[pts][connid] =nilfor _,_ inpairs(influence_points[pts])doreturnend
influence_points[pts] =nilendfunction ildb.get_ip_by_signalpos(spos)for pts,tab inpairs(influence_points)dofor connid,pos inpairs(tab)doif vector.equals(pos, spos)thenreturn pts, connid
endendendendfunction ildb.check_for_duplicate_ip(spos)local main_ip_found =false-- first pass: check for duplicatesfor pts,tab inpairs(influence_points)dofor connid,pos inpairs(tab)doif vector.equals(pos, spos)thenif main_ip_found thenatwarn("Signal at",spos,": Deleting duplicate signal influence point at",pts,"/",connid)
tab[connid] =nilend
main_ip_found =trueendendend-- second pass: delete empty tablesfor pts,tab inpairs(influence_points)doif not tab[1]and not tab[2]then-- only those two connids may exist
influence_points[pts] =nilendendend-- clear signal assignment given the signal positionfunction ildb.clear_ip_by_signalpos(spos)local pts, connid = ildb.get_ip_by_signalpos(spos)if pts then ildb.clear_ip_signal(pts, connid)endend
advtrains.interlocking.db = ildb