User Tools

Site Tools


Minetest Forum
Content Database
Git Repository
Bug Tracker


Linuxforks Subway Interlocking


Note: This is actually taken from a Forum Post written by Blockhead. This is just a modified version of the post designed to fit with the wiki (but 56i, the creator of this wiki page did inspire this post, so part-credit goes to him).

I've actually wanted to make a video about the subway code just because I think it's of historical interest but also as an example of how to make an interlocking system out of just LuaATC. Consider this like a partial script dump for that video.

The subway code is written by orwell, and uses the GPLv3 license. It's a form of interlocking that predates what we call “TSS Interlocking”. It used still in some corners of what we now call Spawn Subway (previously often just 'the subway'); or it was for some time after TSS interlocking was deployed, I haven't been paying keen attention. It was also previously used on South Forest Subway which is just one line, but that system was reworked into an interlocked one 2019. It was also used on the Mountain Railway from Euler Street Station up Mt Gabriel, nominally 'line 10', but that system has been out of service for years now since the TSS update.

The current code is as follows (it's not all properly indented, much of it may have been edited in-game. It probably started back in 2017, but has barely changed since 2019 due to maturity, stability and decreasing relevance.


Basic Operation

The guts of it is in the [icode]stn_union[/icode] function which is an interesting read.

The basic operating is like you guessed. A train waits for a while before departing for the next station by closing the doors. Before leaving it checks for a green signal in a loop. It polls the signal state at random interval of 5-8 seconds, and if it gets stuck it will set the train text first to say it's waiting for preceding/oncoming train, and if that happens too many times it will say the line is out of order. When the train gets the green, it then leaves for the next station. Pre-braking with ATC tracks usually has to be applied, or at least it used to - I think advtrains may treat LuaATC tracks as LZB checkpoints now, I'm not sure exactly but station overruns seem to have decreased. If the train overruns the station, it stops the train and sets the train text to a “BrakeFail” message with the date and time. When the train arrives at the next station, it sets the signal for the previous station of the previous station back to green. The departure speed is configurable or set to maximum by default.

Returns/dead ends

Other than basic functionality there are a few different stop types under F.stn_*.

When the train gets the green, it can set/reset one turnout, this used for end of line stations when the train reverses. There is another variation but where there's no passenger stop at the end, so it's an immediate turnaround. Those are stn_return and stn_return_nohalt. In both cases the reversed train needs to have a stn_return_free after passing the turnout to set the turnout back to the right state for the next train, as well as give it the green signal.

Crossing signals and Union Stations

The other advanced function is the 'osig' or 'crossing signal' and 'union station'. Where two subway lines meet and share tracks, there is a priority and a yielding line leading into this 'union' station. The driver of the train on the priority line sees only one signal, and the yielding line sees two. Trains on the priority line set a red signal at the yielding line's station as well as the previous station on their own line, then clear both of those signals when they depart the union station. Trains on the yielding line wait for two green signals before they can depart.

The priority line doesn't have absolute priority, it just gets checked first. This really tricky bit about 'osigs' happens around lines 263-270. I'm not certain of the following explantation, it would be worth testing.. but I have inspected the code for a while now: When a train arrives at the yielding station to find it red, it adds itself to the S.union_waiting[signal_name] table entry by calling F.union_wait. This registers its intent to enter the junction, and avoids the problem of the priority line blocking all yielding line traffic. First a note on variable names: I think its prev2 is meant to be the osig from the yielding. prev1 and prev2 should be understood as the previous signal at station #1 (the priority station) and station #2 (the yielding station), not the previous station and the station before that; also the prev1 and prev2 must be consistent and not swap the priority and yielding stations, which means you should not use a conditional in the station track code. Anyway, when a train leaves the union station it will set both signals behind it to green by default. However, if a priority train leaves and the yielding line is waiting, the priority line gets blocked; vice versa - if a yielding train leaves and the priority line is waiting, the yielding line gets blocked. Looking at it from another perspective again: If a train assigned to one line leaves a union station, the only way it will set any red signal behind is if the a train on the other line is waiting for priority.

osigs cover joining lines; splitting lines are easier. The station code is wrapped in an if statement according to the train's Line Number, and the usual F.stn function can be used. There are two signals just like the yielding station, except their meaning indicates route and not two boxes that have to both be ticked.

[b] Interlocking boundaries[/b] That is it for LuaATC-only equipped stations. Interesting things happen at the boundaries where a line is part-LuaATC with subway env, and part-TSS-interlocking, and less excitingly at TSS interlocked stations with LuaATC instead of station/stop tracks.

F.stn_ilk is a lot like the station/stop track or F.stn_union. It uses the passive component API call “get_aspect” to poll the signal, and if the section is blocked updates the user that the section is blocked. It relies on ARS to set the signal, though setting signals can be done with set_route and then you can disable ARS; the reworked South Forest Subway used this approach which was useful before the update in later advtrains that disabled ARS while a train is stopped by a station/stop track. It may exist because it predates the station/stop track or to include more functionality; you don't see it in many places.

F.stn_ilkentry calls F.stn_ilk then sets the previous signal green, allowing any LuaATC-operating train behind to proceed, while the first train leaves the interlocked area. This is kind of technically unsafe since the second train could rear-end the first, but only if for some bizarre reason the first train fails to clear the influence point of the interlocking signal before the second train arrives, which probably means something went horribly wrong like the tracks getting griefed.

Criticism of the Subway env code

While the subway code comes with some nice features like the automatic out of order messages or waiting for train messages, some of its code is only necessary due to unreliability. For instance, the BrakeFail messages are unnecessary for an interlocked line because LZB always ensures near-perfect braking curves. Other cumbersome things are baked into the code as well, such as having to list the previous, current and next stations in the LuaATC, and having to name your signals with the passive component naming tool according to a strict naming scheme so LuaATC can find them. Also, the crossing signal/union station system is obviously quite cumbersome compared to TSS signals that just check the section state.

Using a mix of TSS interlocking and LuaATC-based interlocking creates its own problems with complexity at the boundaries, which stn_ilk* display clearly. Some boundaries were left in due to lack of time to convert the whole line, and lack of trust in TSS being reliable in the early days. In practice, both systems have had reliability issues: LuaATC code when people fiddle with the trains or the braking curve goes horribly wrong, and interlocking with malformed sections and sometimes ghost trains.


The current code is as follows (it's not all properly indented, much of it may have been edited in-game The indentation is automatic and may have some faults). It probably started back in 2017, but has barely changed since 2019 due to maturity, stability and decreasing relevance.

Linuxforks Subway Code
Copyright (C) 2017-2021 orwell96 and other LinuxForks contributors.
Licensed under the terms of the GNU General Public License, Version 3 or later
-- 'subway' environment --
--F.stn_union=function(line1, prev1, prev2, this, next, doors, dps, osig, ret_sw, ret_st, nohalt, waittime)
F.stationnames = {
    Ewb = "Edenwood Beach",
    Ban = "Bananame",
    ctr = "Coulomb Street Triangle",
    Cht = "Churchill Street",
    Bbe = "Birch Bay East",
    Bap = "Turtle Rock",
    Icm = "Ice Mountain",
    Eft = "BHS10",
    Apl = "Apple Plains",
    Pal = "Palm Bay",
    Slh = "Smacker's Land of Hope and Glory",
    Lks = "Leekston",
    Ta1 = "Testing Area 1",
    Ta2 = "Testing Area 2",
    Ahr = "AHRAZHUL's Station",
    Ahz = "Large Beach",
    Wim = "Windy Mountains",
    Dam = "Szymon's Dam",
    Wva = "Windy Mountains Valley 1",
    Wvb = "Windy Mountains Valley 2",
    Wvc = "Windy Mountains Valley 3",
    App = "Apple Grove",
    Dem = "Desert Mountain",
    Dev = "Desert View (OCP)",
    Lvc = "Levenshtein Canyon",
    Gho = "Green Hope",
    Snb = "Snake Bend",
    Adb = "Adorno Boulevard",
    Duf = "Duff Road",
    Wat = "Something in the water",
    Ram = "Ramanujan Street",
    Per = "Perelman Street",
    Trp = "Trump Park",
    Sfs = "South Forest Station",
    Lok = "Jude Milhon Street",
    Bam = "Bamboo Hills",
    Sfa = "unnamed",
    Gcl = "Green Cliffs",
    Dri = "Dry Island",
    Ged = "Green Edge",
    Ghb = "Green Hill Beach",
    Acm = "Acacia Mountains",
    Ghm = "Greenhat Mountain",
    Pna = "Pence Avenue",
    Dbl = "Dubulti",
    Sws = "Schwarzschildt Street",
    Mnk = "Minkowsky Street",
    Rgs = "Robert Gardon Street",
    Ehl = "Ehlodex",
    Lus = "Lusin Street",
    Lin = "Lesnoi Industrial Area",
    Boz = "Booze Grove",
    Mrh = "Mirzakhani Street",
    Plt = "Planetarium",
    Mcf = "McFly Street",
    Tha = "Theodor Adorno Street",
    Oni = "Onionland",
    Ora = "Orange Lake",
    Uaa = "Eiffel Street",
    Leo = "Leonhard Street",
    Bby = "Birch Bay",
    Stb = "Stone Beach",
    Jis = "Jungle Island",
    Ice = "Eternal Ice",
    Bnt = "Pierre Berton Street",
    Osa = "Origin Sands",
    OBa = "Cartesian Square",
    OOr = "School",
    OSc = "ARA",
    ONb = "Intel ME Stairs",
    OIs = "SCSI Connector Mess",
    OSm = "Origin Sands (Plaza de la Republica)",
    ioa = "Cow Bridge",
    iob = "Babbage Road",
    Wcs = "Watson-Crick Street",
    Rru = "Rockefeller Runway",
    Ewd = "Edenwood",
    Chu = "Marcuse Street Station",
    Erd = "Erdos Street",
    Uni = "Museum",
    Mar = "Felfa's Market (Bracket Road)",
    Wac = "Watson-Crick",
    OLv = "Market",
    Irk = "Ice Rink",
    Sbr = "Suburb",
    Unv = "University",
    Arc = "Archangel",
    Dar = "Darwin Road",
    Hmi = "Half-Mile Island",
    Zoo = "Zoo",
    Bea = "Beach",
    Yos = "Yoshi Island",
    Krs = "Kernighan&Ritchie Street",
    Rkb = "Robert Koch Boulevard",
    Rsi = "Riverside",
    Swr = "Swimming Rabbit Street",
    Wbb = "Banana Forest",
    Ori = "Origin",
    Snl = "Snowland",
    Sys = "Ship Rock",
    Rfo = "Redwood forest",
    Moj = "Mom Junction",
    Wfr = "Wolf Rock",
    Spa = "Shanielle Park",
    Thh = "Treehouse Hotel",
    Stn = "Main station",
    WB1 = "Riverside",
    WB2 = "Banana Forest",
    WB3 = "Eiffel Street",
    WB4 = "Buckminster Fuller Street",
    WB5 = "White Beaches",
    Shn = "Shanielle City",
    Jus = "Tom Lehrer Street",
    Fre = "Frege Street",
    Min = "MinerLand",
    Vlc = "Volcano Cliffs",
    Mio = "Minio",
    Wpy = "Water Pyramid",
    Cat = "Cathedral",
    Dca = "Desert Canyon",
    Spn = "Spawn",
    Brn = "Ministry of Transport (bernhardd)",
    Kav = "Knuth Avenue",
    Lvf = "Library",
    Fms = "John Horton Conway Street",
    Mnt = "Mountain",
    Mnv = "Mountain Valley",
    Mnn = "Mountain View",
    Max = "Maxwell Street",
    Snp = "Snowy Peak",
    Scl = "ScottishLion's City",
    Lza = "Laza's City",
    Bld = "BlackDog",
    Hts = "Hotel Shanielle",
    Fmn = "Euler Street",
    Gpl = "Market",
    Jun = "Jungle",
    Jng = "Franklin Road",
    Uic = "Coulomb Street",
    Grs = "Gram-Schmidt Street",
    Lih = "Lighthouse",
    Rea = "Reactor",
    Hhs = "Henderson-Hasselbalch Street",
    Ack = "Ackermann Avenue",
    Lis = "Lone Island",
    Pyr = "Pytagoras Road",
    Nha = "North Harbour",
    STn = "Technic Station",
    SPo = "Post Office",
    SSw = "Spawn, westbound",
    SSe = "Spawn, eastbound",
    SPa = "Papyrus Farm",
    STo = "Tourist Info",
    SMi = "Public Mine",
    MR1 = "Euler Street",
    MSt = "Main Station (Spawn)",
    MOr = "Marcuse Street Station (Origin)"
Signal names:
F.stn(<previous>, <this>, <next> 
          <door side>, <Depart speed (maximum if omitted)>
Halt here and continue when signal is green.
no halt:
F.stn_nohalt=function(prev, this, next, dps, osig)
F.stn_return(<previous>, <this>, <next>, 
          <door side>, <Switch to set>, <State to set switch to (st/cr)>,
          <Depart speed (maximum if omitted)>
Halt here. Set the switch to desired state and return when signal is green. Does not free previous section!
F.stn_return_free=function(<Signal at previous station>, <Switch>, <State>)
To be called after train left the switch of a stn_return station. Sets the switch back to incoming trains and sets signal to green.
--Warning: Expects line to be a string!
local linet = {
    ["1"] = {W = "Palm Bay", E = "Windy Mountains"},
    ["2"] = {N = "Szymon's Dam", S = "Onionland"},
    ["3"] = {S = "Bananame", N = "Large Beach"},
    ["4"] = {E = "Schwarzschildt Street", W = "Ice Mountain"},
    ["5"] = {W = "Lighthouse", E = "Leekston"},
    ["7"] = {N = "Birch Bay East", S = "Planetarium"}
F.lineterm = function(line, terminal)
    if linet[line] and linet[line][terminal] then
        return linet[line][terminal]
    return terminal
F.lineset = function(line, terminal)
    if event.train then
        atc_set_text_outside("Line " .. line .. " - " .. F.lineterm(line, terminal))
        S.line[atc_id] = line
F.rant = function()
    return math.random(5, 8)
F.stnname = function(cap)
    return F.stationnames[string.sub(cap, 1, 3)] or "?"
F.stn = function(prev, this, next, doors, dps, osig)
    F.stn_union(nil, prev, nil, this, next, doors, dps, osig)
F.stn_return = function(prev, this, next, doors, switch, state, dps, osig, waittime)
    F.stn_union(nil, prev, nil, this, next, doors, dps, osig, switch, state, false, waittime)
F.stn_return_nohalt = function(prev, this, next, switch, state, dps)
    F.stn_union(nil, prev, nil, this, next, "C", dps, osig, switch, state, true)
F.stn_return_free = function(prev, switch, state)
    if event.train then
        setstate(prev, "green")
        setstate(switch, state)
F.stn_nohalt = function(prev, this, next, dps, osig)
    F.stn_union(nil, prev, nil, this, next, "C", dps, osig, nil, nil, true)
F.union_wait = function(sect)
    S.union_waiting[sect] = not depart
F.stn_union = function(line1, prev1, prev2, this, next, doors, dps, osig, ret_sw, ret_st, nohalt, waittime)
    if not atc_id then
        error("Train has disappeared!")
    if not atc_arrow then
        error("Train passed in wrong direction!")
    depart = false
    if event.train then
        setstate(prev1, "red")
        if prev2 then
            setstate(prev2, "red")
        atc_send("B0O" .. doors)
        if atc_speed and atc_speed > 10 then
            local dt =
                "BrakeFail speed=" ..
                    atc_speed ..
                        " when=" ..
                            dt.year ..
                                "-" .. dt.month .. "-" .. .. " " .. dt.hour .. ":" .. dt.min .. ":" .. dt.sec
            error("Train " .. atc_id .. " has passed rail at speed of " .. atc_speed)
        if not nohalt then
            interrupt(waittime or (ret_sw and 20 or 7), "ready")
    if ( and event.message == "ready") or (event.train and nohalt) then
        if getstate(this) == "green" and (not osig or getstate(osig) == "green") then
            if ret_sw then
                atc_send("OCD1B0WRS" .. (dps or "M"))
                setstate(ret_sw, ret_st)
                atc_send("OCD1S" .. (dps or "M"))
                setstate(prev1, "green")
                if line1 then --this call did not come from F.stn, do union stuff
                    setstate(prev2, "green")
                    if S.line[atc_id] == line1 then
                        if S.union_waiting[prev2] then
                            setstate(prev1, "red")
                        if S.union_waiting[prev1] then
                            setstate(prev2, "red")
            setstate(this, "red")
            atc_set_text_inside("Next stop: " .. F.stnname(next))
            depart = true
            nodepartc = nil
            interrupt(F.rant(), "ready")
            nodepartc = nodepartc and nodepartc + 1 or 0
            if nodepartc >= 10 then
                atc_set_text_inside(F.stnname(this) .. "\nLine out of order!")
                if (not osig or getstate(osig) == "green") then
                    atc_set_text_inside(F.stnname(this) .. "\nWaiting for preceding train...")
                    atc_set_text_inside(F.stnname(this) .. "\nWaiting for oncoming train...")
F.pre = function(signal)
    if getstate(signal) == "red" then
F.uiclog = function()
F.stat = function(line, init)
    -- init
    if init then
        reftrain = atc_id
        a_tbt = 30
        a_tbtmax = 30
        a_rtt = 500
        a_not = 0
        c_not = 0
        c_tbtmax = 0
        time_lt = os.time()
        time_rt = os.time()
    if not a_tbtmax then
        a_tbtmax = 30
    if not c_tbtmax then
        c_tbtmax = 0
    --real code
    if event.train then
        local time = os.time()
        c_not = c_not + 1
        a_tbt = (a_tbt + (time - time_lt)) / 2
        c_tbtmax = math.max(c_tbtmax, (time - time_lt))
        if atc_id == reftrain then
            a_rtt = (a_rtt * 0.2 + (time - time_rt) * 0.8)
            a_not = c_not
            c_not = 0
            a_tbtmax = (a_tbtmax + c_tbtmax) / 2
            c_tbtmax = 0
        digiline_send("stats", "Stat: " .. line ..
            " NoT:"..a_not.."("..c_not.. ")"..
            " TbT:"..math.floor(a_tbt).."("..(time-time_lt)..")"..
            " Tmx:"..math.floor(a_tbtmax).."("..c_tbtmax..")"..
            " R:"..math.floor(a_rtt).."("..(time-time_rt)..")"
        time_lt = time
        if atc_id == reftrain then
            time_rt = time
local function aspect_is_free(asp)
    if type(asp.main) == "table" then
        return asp.main ~= 0
-- 21.1.19, the rise of tss
F.stn_ilk = function(prev, this, next, doors, dps)
    depart = false
    if event.train then
        atc_send("B0 W O" .. doors)
        interrupt(7, "ready")
    elseif then
        local asp = get_aspect(this)
        if not asp then
            atc_set_text_inside(F.stnname(this) .. "\nNo aspect for " .. this)
            if aspect_is_free(asp) then
                atc_set_text_inside("Next stop:\n" .. F.stnname(next))
                atc_send("OC D1 S" .. (dps or "M"))
                depart = true
                atc_set_text_inside(F.stnname(this) .. "\nSection ahead is blocked...")
        interrupt(7, "ready")
F.stn_ilkentry = function(prev, this, next, doors, dps)
    F.stn_ilk(prev, this, next, doors, dps)
    if depart then
        setstate(prev, "green")
usage/atlatc/examples/linuxforks_subway_code.txt · Last modified: 2024-05-23 05:03 by blockhead