-- Setting and clearing routes-- TODO duplicatelocal lntrans = {"A","B"}local functionsigd_to_string(sigd)return minetest.pos_to_string(sigd.p).." / "..lntrans[sigd.s]endlocal ildb = advtrains.interlocking.db
local ilrs = {}local sigd_equal = advtrains.interlocking.sigd_equal
-- table containing locked points-- also manual locks (maintenance a.s.o.) are recorded here-- [pts] = { -- [n] = { [by = <ts_id>], rsn = <human-readable text>, [origin = <sigd>] }-- }
ilrs.rte_locks = {}
ilrs.rte_callbacks = {
ts = {},
lck = {}}-- main route setting. First checks if everything can be set as designated,-- then (if "try" is not set) actually sets it-- returns:-- true - route can be/was successfully set-- false, message, cbts, cblk - something went wrong, what is contained in the message.-- cbts: the ts id of the conflicting ts, cblk: the pts of the conflicting componentfunction ilrs.set_route(signal, route, try)if not try thenlocal tsuc, trsn, cbts, cblk = ilrs.set_route(signal, route,true)if not tsuc thenreturn false, trsn, cbts, cblk
endend-- we start at the tc designated by signallocal c_sigd = signal
local first =truelocal i =1local rtename = route.name
local signalname = (ildb.get_tcbs(signal).signal_name or"") ..sigd_to_string(signal)local c_tcbs, c_ts_id, c_ts, c_rseg, c_lckp
-- signals = { { pos = ., tcbs_ref = <tcbs>, role = "main_distant", main_aspect = nil, dst_type = "next_main" or "none" } local signals = {}local nodst
while c_sigd and i<=#route do
c_tcbs = ildb.get_tcbs(c_sigd)if not c_tcbs thenif not try thenatwarn("Did not find TCBS",c_sigd,"while setting route",rtename,"of",signal)endreturn false,"No TCB found at "..sigd_to_string(c_sigd)..". Please update or reconfigure route!"endif i ==1then
nodst = c_tcbs.nodst
end
c_ts_id = c_tcbs.ts_id
if not c_ts_id thenif not try thenatwarn("Encountered End-Of-Interlocking while setting route",rtename,"of",signal)endreturn false,"No track section adjacent to "..sigd_to_string(c_sigd)..". Please reconfigure route!"end
c_ts = ildb.get_ts(c_ts_id)
c_rseg = route[i]
c_lckp = {}if not c_ts thenif not try thenatwarn("Encountered ts missing during a real run of routesetting routine, at ts=",c_ts_id,"while setting route",rtename,"of",signal)endreturn false,"Section '"..(c_ts_id).."' not found!", c_ts_id,nilelseif c_ts.route thenif not try thenatwarn("Encountered ts lock during a real run of routesetting routine, at ts=",c_ts_id,"while setting route",rtename,"of",signal)endreturn false,"Section '"..(c_ts.name or c_ts_id).."' already has route set from "..sigd_to_string(c_ts.route.origin)..":\n"..c_ts.route.rsn, c_ts_id,nilendif c_ts.trains and#c_ts.trains>0thenif c_rseg.call_on then--atdebug("Routesetting: Call-on situation in", c_ts_id)elseif not try thenatwarn("Encountered ts occupied during a real run of routesetting routine, at ts=",c_ts_id,"while setting route",rtename,"of",signal)endreturn false,"Section '"..(c_ts.name or c_ts_id).."' is occupied!", c_ts_id,nilendend-- collect locks from rs cache and from route deflocal c_locks = {}if route.use_rscache and c_ts.rs_cache and c_rseg.nextthen-- rscache needs to be enabled, present and next must be definedlocal start_pkey = advtrains.encode_pos(c_sigd.p)local end_pkey = advtrains.encode_pos(c_rseg.next.p)if c_ts.rs_cache[start_pkey]and c_ts.rs_cache[start_pkey][end_pkey]thenfor lp,lst inpairs(c_ts.rs_cache[start_pkey][end_pkey])do--atdebug("Add lock from RSCache:",lp,"->",lst)
c_locks[lp] = lst
endelseif not try thenatwarn("While setting route",rtename,"of",signal,"segment "..i..",required path from",c_tcbs,"to",c_rseg.next,"was not found in the track section's RS cache. Please check!")endend-- add all from locks, these override the rscachefor lpts,lst inpairs(c_rseg.locks)do--atdebug("Add lock from Routedef:",lpts,"->",lst,"overrides",c_locks[lpts] or "none")
c_locks[lpts] = lst
endfor lp, state inpairs(c_locks)dolocal confl = ilrs.has_route_lock(lp, state)local pos = advtrains.decode_pos(lp)if advtrains.is_passive(pos)thenlocal cstate = advtrains.getstate(pos)if cstate ~= state thenlocal confl = ilrs.has_route_lock(lp)if confl thenif not try thenatwarn("Encountered route lock while a real run of routesetting routine, at position",pos,"while setting route",rtename,"of",signal)endreturn false,"Lock conflict at "..minetest.pos_to_string(pos)..", Held locked by:\n"..confl,nil, lp
elseif not try then
advtrains.setstate(pos, state)endendif not try then
ilrs.add_route_lock(lp, c_ts_id,"Route '"..rtename.."' from signal '"..signalname.."'", signal)
c_lckp[#c_lckp+1] = lp
endelseif not try thenatwarn("Encountered route lock misconfiguration (no passive component) while a real run of routesetting routine, at position",pts,"while setting route",rtename,"of",signal)endreturn false,"No passive component at "..minetest.pos_to_string(pos)..". Please update track section or reconfigure route!"endend-- sanity check, is section at next the same as the current?local nvar = c_rseg.nextif nvar thenlocal re_tcbs = ildb.get_tcbs({p = nvar.p, s = (nvar.s==1)and2or1})if(not re_tcbs or not re_tcbs.ts_id or re_tcbs.ts_id~=c_ts_id)and route[i+1]then--FIX 2025-01-08: in old worlds the final TCB may be wrong (it didn't matter back then), don't error out here (route still shown invalid in UI)if not try thenatwarn("Encountered inconsistent ts (front~=back) while a real run of routesetting routine, at position",pts,"while setting route",rtename,"of",signal)endreturn false,"TCB at "..minetest.pos_to_string(nvar.p).." has different section than previous TCB. Please update track section or reconfigure route!"endend-- reserve ts and write locksif not try thenif not route[i+1]then-- We shouldn't use the "next" value of the final route segment, because this can lead to accidental route-cancelling of already set routes from another signal.
nvar =nilend
c_ts.route = {
origin = signal,
entry = c_sigd,
rsn ="Route '"..rtename.."' from signal '"..signalname.."', segment #"..i,
first = first,}
c_ts.route_post = {
locks = c_lckp,next= nvar,}if c_tcbs.signal then
c_tcbs.route_committed =true
c_tcbs.route_origin = signal
-- determine route rolelocal ndef = advtrains.ndb.get_ndef(c_tcbs.signal)local assign_dst = c_rseg.assign_dst
if assign_dst ==nil then
assign_dst = (i~=1)-- special behavior when assign_dst is nil (and not false):-- defaults to false for the very first signal and true for all others (= minimal user configuration overhead)endlocal sig_table = {
pos = c_tcbs.signal,
tcbs_ref = c_tcbs,
role = ndef and ndef.advtrains and ndef.advtrains.route_role,
main_aspect = c_rseg.main_aspect,
assign_dst = assign_dst
}
signals[#signals+1] = sig_table
endend-- advance
first =nil
c_sigd = c_rseg.next
i = i +1end-- Get reference to signal at end of routelocal last_mainsig =nilif c_sigd thenlocal e_tcbs = ildb.get_tcbs(c_sigd)local pos = e_tcbs and e_tcbs.signal
if pos then
last_mainsig = pos
endendfor i = #signals,1, -1do-- note the signals are iterated backwards. Switch depending on the rolelocal sig = signals[i]-- apply mainaspect
sig.tcbs_ref.route_aspect = sig.main_aspect or"_default"-- or route.main_aspect : TODO this does not work if a distant signal is on the path! Implement per-sig aspects!if sig.role =="distant"or sig.role =="distant_repeater"or sig.role =="main_distant"thenif last_mainsig then-- assign the remote as the last mainsig if desiredif sig.assign_dst then
sig.tcbs_ref.route_remote = last_mainsig
end-- if it wasn't a distant_repeater clear the mainsigif sig.role ~="distant_repeater"then
last_mainsig =falseendendendif sig.role =="main"or sig.role =="main_distant"or sig.role =="end"then-- record this as the new last mainsig
last_mainsig = sig.pos
end-- for shunt signals nothing happens-- update the signal aspect on map
advtrains.interlocking.signal.update_route_aspect(sig.tcbs_ref, i ~=1)endreturn trueend-- Change 2024-01-27: pts is not an encoded pos, not a pos-to-string!-- Checks whether there is a route lock that prohibits setting the component-- to the wanted state. returns string with reasons on conflictfunction ilrs.has_route_lock(pts)-- look this uplocal e = ilrs.rte_locks[pts]if not e then return nilelseif#e==0then
ilrs.rte_locks[pts] =nilreturn nilendlocal txts = {}for _, ent inipairs(e)do
txts[#txts+1] = ent.rsn
endreturntable.concat(txts,"\n")end-- adds route lock for positionfunction ilrs.add_route_lock(pts, ts, rsn, origin)
ilrs.free_route_locks_indiv(pts, ts,true)local elm = {by=ts, rsn=rsn, origin=origin}if not ilrs.rte_locks[pts]then
ilrs.rte_locks[pts] = { elm }elsetable.insert(ilrs.rte_locks[pts], elm)endend-- adds route lock for positionfunction ilrs.add_manual_route_lock(pts, rsn)local elm = {rsn=rsn}if not ilrs.rte_locks[pts]then
ilrs.rte_locks[pts] = { elm }elsetable.insert(ilrs.rte_locks[pts], elm)endend-- frees route locking for all points (components) that were set by this tsfunction ilrs.free_route_locks(ts, lcks, nocallbacks)for _,pts inpairs(lcks)do
ilrs.free_route_locks_indiv(pts, ts, nocallbacks)endendfunction ilrs.free_route_locks_indiv(pts, ts, nocallbacks)-- legacy: if starts with bracket then pts is still in old pos_to_string format (may happen because ts.route_post is not migrated)ifstring.match(pts,"^%(")thenatdebug("free_route_locks_indiv: converting position",pts)
pts = advtrains.encode_pos(minetest.string_to_pos(pts))endlocal e = ilrs.rte_locks[pts]if not e then return nilelseif#e==0then
ilrs.rte_locks[pts] =nilreturn nilendlocal i =1while i <= #e doif e[i].by == ts then--atdebug("free_route_locks_indiv",pts,"clearing entry",e[i].by,e[i].rsn)table.remove(e,i)else
i = i +1endend-- This must be delayed, because this code is executed in-between a train step-- TODO use luaautomation timers?if not nocallbacks then
minetest.after(0, ilrs.update_waiting,"lck", pts)
minetest.after(0.5, advtrains.set_fallback_state, advtrains.decode_pos(pts))endend-- frees all route locks, even manual ones set with the tool, at a specific positionfunction ilrs.remove_route_locks(pts, nocallbacks)
ilrs.rte_locks[pts] =nil-- This must be delayed, because this code is executed in-between a train step-- TODO use luaautomation timers?if not nocallbacks then
minetest.after(0, ilrs.update_waiting,"lck", pts)endend-- starting from the designated sigd, clears all subsequent route and route_post-- information from the track sections.-- note that this does not clear the routesetting status from the entry signal,-- only from the ts'sfunction ilrs.cancel_route_from(sigd)-- we start at the tc designated by signallocal c_sigd = sigd
local c_tcbs, c_ts_id, c_ts, c_rseg, c_lckp
while c_sigd do--atdebug("cancel_route_from: at sigd",c_sigd)
c_tcbs = ildb.get_tcbs(c_sigd)if not c_tcbs thenatwarn("Failed to cancel route, no TCBS at",c_sigd)return falseend--atdebug("cancelling",c_ts.route.rsn)-- clear signal aspect and routesetting state
c_tcbs.route_committed =nil
c_tcbs.route_aspect =nil
c_tcbs.route_remote =nil
c_tcbs.routeset =nil
c_tcbs.route_auto =nil
c_tcbs.route_origin =nil
advtrains.interlocking.signal.update_route_aspect(c_tcbs)
c_ts_id = c_tcbs.ts_id
if not c_tcbs thenatwarn("Failed to cancel route, end of interlocking at",c_sigd)return falseend
c_ts = ildb.get_ts(c_ts_id)if not c_ts
or not c_ts.route
or notsigd_equal(c_ts.route.entry, c_sigd)then--atdebug("cancel_route_from: abort (eoi/no route):")return falseend
c_ts.route =nilif c_ts.route_post then
advtrains.interlocking.route.free_route_locks(c_ts_id, c_ts.route_post.locks)
c_sigd = c_ts.route_post.nextelse
c_sigd =nilend
c_ts.route_post =nil
minetest.after(0, advtrains.interlocking.route.update_waiting,"ts", c_ts_id)end--atdebug("cancel_route_from: done (no final sigd)")return trueend-- TCBS Routesetting helper: generic update function for-- route setting-- Call this function to set and cancel routes!-- sigd, tcbs: self-explanatory-- newrte: If a new route should be set, the route index of it (in tcbs.routes). Can also be a table (multi-ars). nil otherwise-- cancel: true in combination with newrte=nil causes cancellation of the current route.function ilrs.update_route(sigd, tcbs, newrte, cancel)--atdebug("Update_Route for",sigd,tcbs.signal_name)local has_changed_aspect =falseif tcbs.route_origin and notsigd_equal(tcbs.route_origin, sigd)then--atdebug("Signal not in control, held by",tcbs.signal_name)returnend-- clear route_rsn, it will be set again if needed
tcbs.route_rsn =nilif(newrte and tcbs.routeset and tcbs.routeset ~= newrte)or cancel thenif tcbs.route_committed then--atdebug("Cancelling:",tcbs.routeset)
advtrains.interlocking.route.cancel_route_from(sigd)end
tcbs.route_committed =nil
tcbs.route_aspect =nil
tcbs.route_remote =nil
has_changed_aspect =true
tcbs.routeset =nil
tcbs.route_auto =nilendif newrte or tcbs.routeset thenif tcbs.route_committed thenreturnendif newrte theniftype(newrte)=="table"and notnext(newrte)thenerror("update_route got multi-ARS with empty table, this is not allowed")end
tcbs.routeset = newrte
elseiftype(tcbs.routeset)=="table"and notnext(tcbs.routeset)then-- just unset, don't erroratwarn(sigd,"had multi-ARS route set with empty list! Cancelled!")
tcbs.routeset =nilreturnendend--atdebug("Setting:",tcbs.routeset)-- check: single-ars or multi-ars?local multi_rte
iftype(tcbs.routeset) =="table"then
multi_rte = tcbs.routeset
else
multi_rte = {tcbs.routeset}endfor multi_idx, rteid inipairs(multi_rte)dolocal succ, rsn, cbts, cblk
local route = tcbs.routes[rteid]if route then
succ, rsn, cbts, cblk = ilrs.set_route(sigd, route)else
succ =false
rsn =attrans("Route with index @1 not found", rteid)endif not succ thenif multi_idx==1then
tcbs.route_rsn = rsn
else
tcbs.route_rsn = (tcbs.route_rsn or"").."\n"..rsn
end--atdebug("Routesetting",rteid,"failed:",rsn,"(multi-idx",multi_idx,")")-- add cbts or cblk to callback tableif cbts then--atdebug("cbts =",cbts)if not ilrs.rte_callbacks.ts[cbts]then ilrs.rte_callbacks.ts[cbts]={}end
advtrains.insert_once(ilrs.rte_callbacks.ts[cbts], sigd, sigd_equal)endif cblk then--atdebug("cblk =",cblk)if not ilrs.rte_callbacks.lck[cblk]then ilrs.rte_callbacks.lck[cblk]={}end
advtrains.insert_once(ilrs.rte_callbacks.lck[cblk], sigd, sigd_equal)endelse--atdebug("Committed Route:",rteid,"(multi-idx",multi_idx,")")-- replace multi_route by single actually committed route
tcbs.routeset = rteid
-- set_route now sets the signal aspects--has_changed_aspect = true-- route success. apply default_autoworking flag if requestedif route.default_autoworking then
tcbs.route_auto =true--FIX 2025-01-08: never set it to false if it was true!end-- break out of the for loop, dont try any more routesbreakendendendif has_changed_aspect then-- FIX: prevent an minetest.after() loop caused by update_signal_aspect dispatching path invalidation, which in turn calls ARS again
advtrains.interlocking.signal.update_route_aspect(tcbs)end
advtrains.interlocking.update_player_forms(sigd)end-- Try to re-set routes that conflicted with this point-- sys can be one of "ts" and "lck"-- key is then ts_id or pts respectivelyfunction ilrs.update_waiting(sys, key)--atdebug("update_waiting:",sys,".",key)local t = ilrs.rte_callbacks[sys][key]
ilrs.rte_callbacks[sys][key] =nilif t thenfor _,sigd inipairs(t)do--atdebug("Updating", sigd)-- While these are run, the table we cleared before may be populated again, which is in our interest.-- (that's the reason we needed to copy it)local tcbs = ildb.get_tcbs(sigd)if tcbs then
ilrs.update_route(sigd, tcbs)endendendend
advtrains.interlocking.route = ilrs