diff options
-rw-r--r-- | advtrains_interlocking/ars.lua | 285 |
1 files changed, 224 insertions, 61 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) |