aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--advtrains_interlocking/ars.lua285
-rw-r--r--advtrains_interlocking/smartroute.lua33
2 files changed, 252 insertions, 66 deletions
diff --git a/advtrains_interlocking/ars.lua b/advtrains_interlocking/ars.lua
index eb10497..fc9f2a3 100644
--- a/advtrains_interlocking/ars.lua
+++ b/advtrains_interlocking/ars.lua
@@ -3,26 +3,89 @@
--[[
The "ARS table" and its effects:
- Every route has (or can have) an associated ARS table. This can either be
- ars = { [n] = {ln="<line>"}/{rc="<routingcode>"}/{c="<a comment>"} }
- a list of rules involving either line or routingcode matchers (or comments, those are ignored)
- The first matching rule determines the route to set.
- - or -
- ars = {default = true}
- this means that all trains that no other rule matches on should use this route
+ Every route has (or can have) an associated ARS table:
+ ars = {
+ [n] = {
+ ln = "<line>" -- either line
+ rc = "<routingcode>" -- or routingcode
+ n = true/false -- true = logical not (matches everything that does not have this line/rc)
+ conj = { -- and conjunction, optional. This must be true in addition to the main rule
+ ln=... / rc=... / n=... -- like toplevel
+ conj=... -- can be nested
+ -- note that conj cannot have prio, in inherits the prio from main rule
+ }
+ prio = <num> -- optional, a priority number. If set, enables "multi-ARS" where train can wait for multiple routes at once
+ -- or
+ c="<a comment>"
+ }
+ default = true -- Route is the default route for trains
+ default_prio -- optional, priority for multi-ars with default route
+ default_conj = {...} -- optional, conditions (conjunction) that need to be fulfilled for this to be considered default route
+ }
- Compound ("and") conjunctions are not supported (--TODO should they?)
+ In case a train matches the ARS rules of multiple routes, precedence is as follows:
+ 1. train matches rules without priority in one or more routes -> first matching route is unconditionally set
+ 2. train matches rules with priority in one or more routes -> all of the matching routes are set (multi-ars)
+ in the order given by priority, first available route is set
+ 3. route is default=true, default_prio=nil and train matches default_conj (if present) -> first default route is set
+ 4. one or more routes are default=true, with default_prio set,and train matches default_conj (if present)
+ -> all of the matching routes are set (multi-ars) in the order given by priority, first available route is set
For editing, those tables are transformed into lines in a text area:
{ln=...} -> LN ...
{rc=...} -> RC ...
{c=...} -> #...
{default=true} -> *
- See also route_ui.lua
+ n -> ! (e.g. ln=..., n=true -> !LN ...)
+ prio -> <num> (e.g. ln=..., prio=1 -> 1 LN ...)
+
+ conj -> goes on the next line, with an & prepended, e.g.:
+ {ln="S1", conj={rc="Left"}} ->
+ LN S1
+ & RC Left
+
+ Example combining everything:
+ ars = {
+ [1] = {
+ ln = "S1"
+ n = true
+ prio = 4
+ conj = {
+ rc = "R4"
+ }
+ }
+ default = true
+ default_prio = 2
+ default_conj = {
+ rc = "R5"
+ n = true
+ }
+ } ->
+ 4 !LN S1
+ & RC R4
+ 2 *
+ & !RC R5
+
]]
local il = advtrains.interlocking
+
+local function conj_to_text(conj, txt)
+ while conj do
+ n = ""
+ if conj.n then
+ n = "!"
+ end
+ if conj.ln then
+ txt[#txt+1] = "& "..n.."LN "..conj.ln
+ elseif conj.rc then
+ txt[#txt+1] = "& "..n.."RC "..conj.rc
+ end
+ conj = conj.conj
+ end
+end
+
-- The ARS data are saved in a table format, but are entered in text format. Utility functions to transform between both.
function il.ars_to_text(arstab)
if not arstab then
@@ -32,103 +95,202 @@ function il.ars_to_text(arstab)
local txt = {}
for i, arsent in ipairs(arstab) do
+ local prio = ""
+ if arsent.prio then
+ prio = arsent.prio.." "
+ end
local n = ""
if arsent.n then
n = "!"
end
if arsent.ln then
- txt[#txt+1] = n.."LN "..arsent.ln
+ txt[#txt+1] = prio..n.."LN "..arsent.ln
elseif arsent.rc then
- txt[#txt+1] = n.."RC "..arsent.rc
+ txt[#txt+1] = prio..n.."RC "..arsent.rc
elseif arsent.c then
txt[#txt+1] = "#"..arsent.c
end
+ conj_to_text(arsent.conj, txt)
end
if arstab.default then
- return "*\n" .. table.concat(txt, "\n")
+ local prio = ""
+ if arstab.default_prio then
+ prio = arstab.default_prio.." "
+ end
+ txt[#txt+1] = prio.."*"
+ conj_to_text(arstab.default_conj, txt)
end
return table.concat(txt, "\n")
end
+local function parse_ruleexp(line)
+ local excl, key, val = string.match(line, "^%s*(!?)%s*([RL][CN])%s+(.+)%s*$")
+ if key == "RC" then
+ return {rc=val, n=(excl=="!")}
+ elseif key == "LN" then
+ return {ln=val, n=(excl=="!")}
+ end
+end
+
function il.text_to_ars(t)
if not string.match(t, "%S+") then
return nil
- elseif t=="*" then
- return {default=true}
end
local arstab = {}
+ local previtem
for line in string.gmatch(t, "[^\r\n]+") do
- if line=="*" then
- arstab.default = true
+ -- a) comment
+ local ct = string.match(line, "^#(.*)$")
+ if ct then
+ arstab[#arstab+1] = {c = ct}
+ previtem = nil
else
- local c, v = string.match(line, "^(...?)%s(.*)$")
- if c and v then
- local n = nil
- if string.sub(c,1,1) == "!" then
- n = true
- c = string.sub(c,2)
- end
- local tt=string.upper(c)
- if tt=="LN" then
- arstab[#arstab+1] = {ln=v, n=n}
- elseif tt=="RC" then
- arstab[#arstab+1] = {rc=v, n=n}
+ -- b) Conjunction to the previous item
+ local conline = string.match(line, "^%s*&(.+)$")
+ if conline then
+ local conj = parse_ruleexp(conline)
+ if conj and previtem==true then
+ -- previtem was default
+ arstab.default_conj = conj
+ previtem = conj
+ elseif conj and previtem then
+ previtem.conj = conj
+ previtem = conj
+ else
+ -- dont know what to do with line, put as comment
+ arstab[#arstab+1] = {c = "? "..line}
+ previtem = nil
end
else
- local ct = string.match(line, "^#(.*)$")
- if ct then arstab[#arstab+1] = {c = ct} end
+ -- c) Normal rule spec
+ local prio, ruleline = string.match(line, "^%s*([0-9]*)%s*(.+)%s*$")
+ if ruleline == "*" then
+ -- ruleline is the asterisk, this is default
+ arstab.default = true
+ arstab.default_prio = tonumber(prio) -- evals to nil if not given
+ previtem = true -- marks that previtem was asterisk
+ elseif ruleline then
+ -- ruleline is present, parse it
+ local rule = parse_ruleexp(ruleline)
+ if not rule then
+ -- dont know what to do with line, put as comment
+ arstab[#arstab+1] = {c = "? "..line}
+ previtem = nil
+ else
+ rule.prio = tonumber(prio) -- evals to nil if not given
+ arstab[#arstab+1] = rule
+ previtem = rule
+ end
+ else
+ -- d) nothing else works, save line as comment
+ arstab[#arstab+1] = {c = "? "..line}
+ previtem = nil
+ end
end
end
end
return arstab
end
-local function find_rtematch(routes, train)
- local default
- for rteid, route in ipairs(routes) do
- if route.ars then
- if route.ars.default then
- default = rteid
- else
- if il.ars_check_rule_match(route.ars, train) then
- return rteid
- end
- end
+
+local function match_arsent(arsent, train)
+ local rule_matches = false
+ if arsent.ln then
+ local line = train.line
+ rule_matches = line and arsent.ln == line
+ if arsent.n then rule_matches = not rule_matches end
+ elseif arsent.rc then
+ local routingcode = train.routingcode
+ rule_matches = routingcode and string.find(" "..routingcode.." ", " "..arsent.rc.." ", nil, true)
+ if arsent.n then rule_matches = not rule_matches end
+ end
+ if rule_matches then
+ -- if the entry has a conjunction, go on checking
+ if arsent.conj then
+ return match_arsent(arsent.conj, train)
+ else
+ return true
end
+ else
+ return false
end
- return default
end
--- Checks whether ARS rule explicitly matches. This does not take into account the "default" field, since a wider context is required for this.
--- Returns the rule number that matched, or nil if nothing matched
+-- Given an ARS rule table, check whether any of the clauses in it match the train.
+-- Returns: match_specific, match_default
+-- match_specific: One of the clauses explicitly matched (if this is non-false, match_default is not checked and always given false)
+-- match_default: The default clause (*) matched (as well as any conjunctions attached to the default clause)
+-- both of these can be either true (unconditional match), a number (priority for multi-ars) or false
function il.ars_check_rule_match(ars, train)
if not ars then
- return nil
+ return nil, nil
end
- local line = train.line
- local routingcode = train.routingcode
for arskey, arsent in ipairs(ars) do
- --atdebug(arsent, line, routingcode)
- if arsent.n then
- -- rule is inverse...
- if arsent.ln and (not line or arsent.ln ~= line) then
- return arskey
- elseif arsent.rc and (not routingcode or not string.find(" "..routingcode.." ", " "..arsent.rc.." ", nil, true)) then
- return arskey
- end
- return nil
+ local rule_matches = match_arsent(arsent, train)
+ if rule_matches then
+ return (arsent.prio or true), nil
+ end
+ end
+ if ars.default then
+ local def_matches = true
+ if ars.default_conj then
+ def_matches = match_arsent(ars.default_conj, train)
+ end
+ if def_matches then
+ return false, (ars.default_prio or true)
+ end
+ end
+ return false,false
+end
+
+local function sort_priority(sprio)
+ -- TODO implement and return the correct sorted table
+ -- for now just minimum
+ local maxk,maxv = nil,10000
+ for k,v in pairs(sprio) do
+ if v<maxv then
+ maxv = v
+ maxk = k
+ end
+ end
+ return maxk
+end
+
+local function find_rtematch(routes, train)
+ local sprio = {}
+ local default = nil
+ local dprio = {}
+ for rteid, route in ipairs(routes) do
+ if route.ars then
+ local mspec, mdefault = il.ars_check_rule_match(route.ars, train)
+ --atdebug("route",rteid,"ars",route.ars,"gives", mspec, mdefault)
+ if mspec == true then
+ return rteid
+ elseif mspec then
+ sprio[rteid] = mspec
end
-
- if arsent.ln and line and arsent.ln == line then
- return arskey
- elseif arsent.rc and routingcode and string.find(" "..routingcode.." ", " "..arsent.rc.." ", nil, true) then
- return arskey
+ if mdefault == true then
+ if not default then default = rteid end
+ elseif mdefault then
+ dprio[rteid] = mdefault
end
end
+ end
+ if next(sprio) then
+ atdebug("Ars: SMultiArs", sprio, "is not implemented yet!")
+ return sort_priority(sprio)
+ elseif default then
+ return default
+ elseif next(dprio) then
+ atdebug("Ars: DMultiArs", dprio, "is not implemented yet!")
+ return sort_priority(dprio)
+ else
return nil
+ end
end
+
function advtrains.interlocking.ars_check(signalpos, train, trig_from_dst)
-- check for distant signal
-- this whole check must be delayed until after the route setting has taken place,
@@ -169,6 +331,7 @@ function advtrains.interlocking.ars_check(signalpos, train, trig_from_dst)
local rteid = find_rtematch(tcbs.routes, train)
if rteid then
+ --atdebug("Ars setting ",rteid)
--delay routesetting, it should not occur inside train step
-- using after here is OK because that gets called on every path recalculation
minetest.after(0, il.route.update_route, sigd, tcbs, rteid, nil)
diff --git a/advtrains_interlocking/smartroute.lua b/advtrains_interlocking/smartroute.lua
index f03ece0..76c7814 100644
--- a/advtrains_interlocking/smartroute.lua
+++ b/advtrains_interlocking/smartroute.lua
@@ -52,7 +52,7 @@ local RTE_MAX_SECS = 16
-- find_more_than: search is aborted only if more than the specified number of routes are found
function sr.rescan(pname, sigd, tcbs, find_more_than, searching_shunt, pname)
local found_routes = {}
- local restart_tcbs = { {sigd = sigd, tcbseq = {} } }
+ local restart_tcbs = { {sigd = sigd, tcbseq = {}, secseq = {} } }
local last_len = 0
while true do
-- take first entry out of restart_tcbs (due to the way it is inserted the first entry will always be the one with the lowest length
@@ -94,7 +94,9 @@ function sr.rescan(pname, sigd, tcbs, find_more_than, searching_shunt, pname)
local nsigd = {p=end_sigd.p, s = end_sigd.s==1 and 2 or 1} -- invert to other side
-- record nsigd in the tcbseq
local ntcbseq = table.copy(cur_restart.tcbseq)
+ local nsecseq = table.copy(cur_restart.secseq)
ntcbseq[#ntcbseq+1] = nsigd
+ nsecseq[#nsecseq+1] = c_ts.name or c_ts_id
local shall_continue = true
-- check if that sigd is a route target
local tcbs = ildb.get_tcbs(nsigd)
@@ -109,6 +111,7 @@ function sr.rescan(pname, sigd, tcbs, find_more_than, searching_shunt, pname)
-- record the found route in the results
found_routes[#found_routes+1] = {
tcbseq = ntcbseq,
+ secseq = nsecseq,
shunt_route = not is_mainsignal,
name = tcbs.signal_name or atil.sigd_to_string(nsigd)
}
@@ -122,7 +125,7 @@ function sr.rescan(pname, sigd, tcbs, find_more_than, searching_shunt, pname)
end
-- unless overridden, insert the next restart point
if shall_continue then
- restart_tcbs[#restart_tcbs+1] = {sigd = nsigd, tcbseq = ntcbseq }
+ restart_tcbs[#restart_tcbs+1] = {sigd = nsigd, tcbseq = ntcbseq, secseq = nsecseq }
end
end
end
@@ -155,12 +158,20 @@ function sr.propose_next(pname, sigd, find_more_than, searching_shunt)
found_routes = found_routes
}
-- step 3: build form
- local form = "size[5,5]label[0,0;Route search: "..#found_routes.." found]"
+ local form = "size[8,5]label[0,0;Route search: "..#found_routes.." found]"
local tab = {}
for idx, froute in ipairs(found_routes) do
- tab[idx] = minetest.formspec_escape(froute.name.." (Len="..#froute.tcbseq..")")
+ local secfl = table.copy(froute.secseq)
+ table.remove(secfl, 1) -- remove first and last, because it will always be the same
+ secfl[#secfl]=nil
+ local viatext = ""
+ if next(secfl) then
+ froute.via = table.concat(secfl, ", ")
+ viatext = " (via "..froute.via..")"
+ end
+ tab[idx] = minetest.formspec_escape(froute.name..viatext)
end
- form=form.."textlist[0.5,1;4,3;rtelist;"..table.concat(tab, ",").."]"
+ form=form.."textlist[0.5,1;7,3;rtelist;"..table.concat(tab, ",").."]"
form=form.."button[0.5,4;2,1;continue;Search further]"
form=form.."button[2.5,4;2,1;apply;Apply]"
@@ -207,11 +218,19 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
end
end
local new_frte = {}
+ local endOnce = {}
+ local endTwice = {}
for _,froute in ipairs(found_routes) do
local endpoint = froute.tcbseq[#froute.tcbseq]
local endstr = advtrains.interlocking.sigd_to_string(endpoint)
if not ex_endpts[endstr] then
new_frte[#new_frte+1] = froute
+ -- record duplicate targets in froute
+ if endOnce[froute.name] then
+ endTwice[froute.name] = true
+ else
+ endOnce[froute.name] = true
+ end
else
--atdebug("(Smartroute) Throwing away",froute.name,"because endpoint",endstr,"already reached by route",ex_endpts[endstr])
end
@@ -220,6 +239,10 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
-- All remaining routes will be applied to the signal
local sel_rte = #tcbs.routes+1
for idx, froute in ipairs(new_frte) do
+ if endTwice[froute.name] then
+ -- append via text to deduplicate name
+ froute.name = froute.name .. " (via "..(froute.via or "direct")..")"
+ end
tcbs.routes[#tcbs.routes+1] = build_route_from_foundroute(froute)
end
-- if only one route present and it is newly created (there was no route before, thus sel_rte==1), make default