aboutsummaryrefslogtreecommitdiff
path: root/advtrains_interlocking
diff options
context:
space:
mode:
Diffstat (limited to 'advtrains_interlocking')
-rw-r--r--advtrains_interlocking/approach.lua126
-rw-r--r--advtrains_interlocking/ars.lua155
-rw-r--r--advtrains_interlocking/database.lua648
-rw-r--r--advtrains_interlocking/demosignals.lua97
-rw-r--r--advtrains_interlocking/init.lua30
-rw-r--r--advtrains_interlocking/mod.conf7
-rw-r--r--advtrains_interlocking/models/at_il_tcb_node.obj248
-rw-r--r--advtrains_interlocking/route_prog.lua549
-rw-r--r--advtrains_interlocking/route_ui.lua153
-rw-r--r--advtrains_interlocking/routesetting.lua342
-rw-r--r--advtrains_interlocking/settingtypes.txt4
-rw-r--r--advtrains_interlocking/signal_api.lua515
-rwxr-xr-xadvtrains_interlocking/tcb_ts_ui.lua830
-rw-r--r--advtrains_interlocking/textures/advtrains_dtrack_npr_placer.pngbin0 -> 1238 bytes
-rw-r--r--advtrains_interlocking/textures/advtrains_dtrack_shared_npr.pngbin0 -> 3176 bytes
-rw-r--r--advtrains_interlocking/textures/at_il_route_end.pngbin0 -> 451 bytes
-rw-r--r--advtrains_interlocking/textures/at_il_route_lock.pngbin0 -> 534 bytes
-rw-r--r--advtrains_interlocking/textures/at_il_route_lock_edit.pngbin0 -> 533 bytes
-rw-r--r--advtrains_interlocking/textures/at_il_route_set.pngbin0 -> 398 bytes
-rw-r--r--advtrains_interlocking/textures/at_il_route_start.pngbin0 -> 380 bytes
-rw-r--r--advtrains_interlocking/textures/at_il_routep_advance.pngbin0 -> 304 bytes
-rw-r--r--advtrains_interlocking/textures/at_il_routep_end_here.pngbin0 -> 243 bytes
-rw-r--r--advtrains_interlocking/textures/at_il_routep_end_over.pngbin0 -> 281 bytes
-rw-r--r--advtrains_interlocking/textures/at_il_routep_end_over_last.pngbin0 -> 277 bytes
-rw-r--r--advtrains_interlocking/textures/at_il_signal_asp_danger.pngbin0 -> 247 bytes
-rw-r--r--advtrains_interlocking/textures/at_il_signal_asp_free.pngbin0 -> 245 bytes
-rw-r--r--advtrains_interlocking/textures/at_il_signal_asp_slow.pngbin0 -> 245 bytes
-rw-r--r--advtrains_interlocking/textures/at_il_signal_ip.pngbin0 -> 285 bytes
-rw-r--r--advtrains_interlocking/textures/at_il_signal_off.pngbin0 -> 236 bytes
-rw-r--r--advtrains_interlocking/textures/at_il_tcb_marker.pngbin0 -> 308 bytes
-rw-r--r--advtrains_interlocking/textures/at_il_tcb_node.pngbin0 -> 279 bytes
-rw-r--r--advtrains_interlocking/textures/at_il_tool.pngbin0 -> 337 bytes
-rw-r--r--advtrains_interlocking/textures/at_il_turnout_cr_l.pngbin0 -> 314 bytes
-rw-r--r--advtrains_interlocking/textures/at_il_turnout_cr_r.pngbin0 -> 298 bytes
-rw-r--r--advtrains_interlocking/textures/at_il_turnout_free.pngbin0 -> 367 bytes
-rw-r--r--advtrains_interlocking/textures/at_il_turnout_st.pngbin0 -> 229 bytes
-rw-r--r--advtrains_interlocking/tool.lua66
-rw-r--r--advtrains_interlocking/train_sections.lua199
-rw-r--r--advtrains_interlocking/tsr_rail.lua66
39 files changed, 4035 insertions, 0 deletions
diff --git a/advtrains_interlocking/approach.lua b/advtrains_interlocking/approach.lua
new file mode 100644
index 0000000..f60468a
--- /dev/null
+++ b/advtrains_interlocking/approach.lua
@@ -0,0 +1,126 @@
+-- Interlocking counterpart of LZB, which has been moved into the core...
+-- Registers LZB callback for signal management.
+
+--[[
+usage of lzbdata:
+{
+ travsht = boolean indicating whether the train will be a shunt move at "trav"
+ travspd = speed restriction at end of traverser
+ travwspd = warning speed res.t
+}
+]]
+
+local SHUNT_SPEED_MAX = advtrains.SHUNT_SPEED_MAX
+
+local il = advtrains.interlocking
+
+local function get_over_function(speed, shunt)
+ return function(pos, id, train, index, speed, lzbdata)
+ if speed == 0 and minetest.settings:get_bool("at_il_force_lzb_halt") then
+ atwarn(id,"overrun LZB 0 restriction (red signal) ",pos)
+ -- Set train 1 index backward. Hope this does not lead to bugs...
+ --train.index = index - 0.5
+ train.speed_restriction = 0
+
+ --TODO temporary
+ --advtrains.drb_dump(id)
+ --error("Debug: "..id.." triggered LZB-0")
+ else
+ train.speed_restriction = speed
+ train.is_shunt = shunt
+ end
+ --atdebug("train drove over IP: speed=",speed,"shunt=",shunt)
+ end
+end
+
+advtrains.tnc_register_on_approach(function(pos, id, train, index, has_entered, lzbdata)
+
+ --atdebug(id,"IL ApprC",pos,index,lzbdata)
+ --train.debug = advtrains.print_concat_table({train.is_shunt,"|",index,"|",lzbdata})
+
+ local pts = advtrains.roundfloorpts(pos)
+ local cn = train.path_cn[index]
+ local travsht = lzbdata.il_shunt
+
+ local travspd = lzbdata.il_speed
+
+ if travsht==nil then
+ -- lzbdata has reset
+ travspd = train.speed_restriction
+ travsht = train.is_shunt or false
+ end
+
+
+
+ -- check for signal
+ local asp, spos = il.db.get_ip_signal_asp(pts, cn)
+
+ -- do ARS if needed
+ local ars_enabled = not train.ars_disable
+ -- Note on ars_disable:
+ -- Theoretically, the ars_disable flag would need to behave like the speed restriction field: it should be
+ -- stored in lzbdata and updated once the train drives over. However, for the sake of simplicity, it is simply
+ -- a value in the train. In this case, this is sufficient because once a train triggers ARS for the first time,
+ -- resetting the path does not matter to the set route and ARS doesn't need to be called again.
+ if spos and ars_enabled then
+ --atdebug(id,"IL Spos (ARS)",spos,asp)
+ local sigd = il.db.get_sigd_for_signal(spos)
+ if sigd then
+ il.ars_check(sigd, train)
+ end
+ end
+ --atdebug("trav: ",pos, cn, asp, spos, "travsht=", lzb.travsht)
+ local lspd
+ if asp then
+ --atdebug(id,"IL Signal",spos, asp, lzbdata, "trainstate", train.speed_restriction, train.is_shunt)
+ local nspd = 0
+ --interpreting aspect and determining speed to proceed
+ if travsht then
+ --shunt move
+ if asp.shunt then
+ nspd = SHUNT_SPEED_MAX
+ elseif asp.proceed_as_main and asp.main ~= 0 then
+ nspd = asp.main
+ travsht = false
+ end
+ else
+ --train move
+ if asp.main ~= 0 then
+ nspd = asp.main
+ elseif asp.shunt then
+ nspd = SHUNT_SPEED_MAX
+ travsht = true
+ end
+ end
+ -- nspd can now be: 1. !=0: new speed restriction, 2. =0: stop here or 3. nil: keep travspd
+ if nspd then
+ if nspd == -1 then
+ travspd = nil
+ else
+ travspd = nspd
+ end
+ end
+
+ --atdebug("ns,ts", nspd, travspd)
+
+ lspd = travspd
+
+ local udata = {signal_pos = spos}
+ local callback = get_over_function(lspd, travsht)
+ lzbdata.il_shunt = travsht
+ lzbdata.il_speed = travspd
+ --atdebug("new lzbdata",lzbdata)
+ advtrains.lzb_add_checkpoint(train, index, lspd, callback, lzbdata, udata)
+ end
+end)
+
+-- Set the ars_disable flag to the value passed
+-- Triggers a path invalidation if set to false
+function advtrains.interlocking.ars_set_disable(train, value)
+ if value then
+ train.ars_disable = true
+ else
+ train.ars_disable = nil
+ minetest.after(0, advtrains.path_invalidate, train)
+ end
+end
diff --git a/advtrains_interlocking/ars.lua b/advtrains_interlocking/ars.lua
new file mode 100644
index 0000000..434ae2c
--- /dev/null
+++ b/advtrains_interlocking/ars.lua
@@ -0,0 +1,155 @@
+-- ars.lua
+-- automatic routesetting
+
+--[[
+ 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
+
+ Compound ("and") conjunctions are not supported (--TODO should they?)
+
+ For editing, those tables are transformed into lines in a text area:
+ {ln=...} -> LN ...
+ {rc=...} -> RC ...
+ {c=...} -> #...
+ {default=true} -> *
+ See also route_ui.lua
+]]
+
+local il = advtrains.interlocking
+
+-- 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
+ return ""
+ end
+
+ local txt = {}
+
+ for i, arsent in ipairs(arstab) do
+ local n = ""
+ if arsent.n then
+ n = "!"
+ end
+ if arsent.ln then
+ txt[#txt+1] = n.."LN "..arsent.ln
+ elseif arsent.rc then
+ txt[#txt+1] = n.."RC "..arsent.rc
+ elseif arsent.c then
+ txt[#txt+1] = "#"..arsent.c
+ end
+ end
+
+ if arstab.default then
+ return "*\n" .. table.concat(txt, "\n")
+ end
+ return table.concat(txt, "\n")
+end
+
+function il.text_to_ars(t)
+ if t=="" then
+ return nil
+ elseif t=="*" then
+ return {default=true}
+ end
+ local arstab = {}
+ for line in string.gmatch(t, "[^\r\n]+") do
+ if line=="*" then
+ arstab.default = true
+ 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}
+ end
+ else
+ local ct = string.match(line, "^#(.*)$")
+ if ct then arstab[#arstab+1] = {c = ct} 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
+ end
+ 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
+function il.ars_check_rule_match(ars, train)
+ if not ars then
+ return 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
+ 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
+ end
+ end
+ return nil
+end
+
+function advtrains.interlocking.ars_check(sigd, train)
+ local tcbs = il.db.get_tcbs(sigd)
+ if not tcbs or not tcbs.routes then return end
+
+ if tcbs.ars_disabled then
+ -- No-ARS mode of signal.
+ -- ignore...
+ return
+ end
+
+ if tcbs.routeset then
+ -- ARS is not in effect when a route is already set
+ -- just "punch" routesetting, just in case callback got lost.
+ minetest.after(0, il.route.update_route, sigd, tcbs, nil, nil)
+ return
+ end
+
+ local rteid = find_rtematch(tcbs.routes, train)
+ if rteid then
+ --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)
+ end
+end
diff --git a/advtrains_interlocking/database.lua b/advtrains_interlocking/database.lua
new file mode 100644
index 0000000..a35d446
--- /dev/null
+++ b/advtrains_interlocking/database.lua
@@ -0,0 +1,648 @@
+-- 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 obove
+ROUTE 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 REVERSIBLE
+Certain 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 ROUTE
+Route 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 signal
+Train passes entry signal and enters first TC past the signal
+-> Route from signal cleared (TCs remain locked)
+-> ROUTE status of first TC past signal cleared
+Train 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 TC
+Note 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 |-O
+T->---|->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 route
+is 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 ever
+having touched the right TC.
+]]--
+
+local TRAVERSER_LIMIT = 1000
+
+
+local ildb = {}
+
+local track_circuit_breaks = {}
+local track_sections = {}
+
+-- Assignment of signals to TCBs
+local signal_assignments = {}
+
+-- track+direction -> signal position
+local influence_points = {}
+
+advtrains.interlocking.npr_rails = {}
+
+
+function ildb.load(data)
+ if not data then return end
+ if data.tcbs then
+ track_circuit_breaks = data.tcbs
+ end
+ if data.ts then
+ track_sections = data.ts
+ end
+ if data.signalass then
+ signal_assignments = data.signalass
+ end
+ if data.rs_locks then
+ advtrains.interlocking.route.rte_locks = data.rs_locks
+ end
+ if data.rs_callbacks then
+ advtrains.interlocking.route.rte_callbacks = data.rs_callbacks
+ end
+ if data.influence_points then
+ influence_points = data.influence_points
+ end
+ if data.npr_rails then
+ advtrains.interlocking.npr_rails = data.npr_rails
+ end
+
+ --COMPATIBILITY to Signal aspect format
+ -- TODO remove in time...
+ for pts,tcb in pairs(track_circuit_breaks) do
+ for connid, tcbs in ipairs(tcb) do
+ if tcbs.routes then
+ for _,route in ipairs(tcbs.routes) do
+ if route.aspect then
+ -- transform the signal aspect format
+ local asp = route.aspect
+ if type(asp.main) == "table" then
+ atwarn("Transforming route aspect of signal",pts,"/",connid,"")
+ if asp.main.free then
+ asp.main = asp.main.speed
+ else
+ asp.main = 0
+ end
+ if asp.dst.free then
+ asp.dst = asp.dst.speed
+ else
+ asp.dst = 0
+ end
+ asp.proceed_as_main = asp.shunt.proceed_as_main
+ asp.shunt = asp.shunt.free
+ -- Note: info table not transferred, it's not used right now
+ end
+ end
+ end
+ end
+ end
+ end
+end
+
+function 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 TCBs
+signal_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 true
+ else
+ return false
+ end
+end
+
+function ildb.get_tcb(pos)
+ local pts = advtrains.roundfloorpts(pos)
+ return track_circuit_breaks[pts]
+end
+
+function ildb.get_tcbs(sigd)
+ local tcb = ildb.get_tcb(sigd.p)
+ if not tcb then return nil end
+ return tcb[sigd.s]
+end
+
+
+function 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
+end
+
+function ildb.get_ts(id)
+ return track_sections[id]
+end
+
+
+
+-- various helper functions handling sigd's
+local sigd_equal = advtrains.interlocking.sigd_equal
+local function insert_sigd_nodouble(list, sigd)
+ for idx, cmp in pairs(list) do
+ if sigd_equal(sigd, cmp) then
+ return
+ end
+ end
+ table.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 calculated
+local function traverser(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)
+ return
+ end
+ -- look whether there is a TCB here
+ if #next_conns == 2 then --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})
+ return
+ end
+ end
+ -- recursion abort condition
+ if count > TRAVERSER_LIMIT then
+ --atdebug("Traverser hit counter at",adj_pos, adj_connid)
+ return true
+ end
+ -- continue traversing
+ local counter_hit = false
+ for nconnid, nconn in ipairs(next_conns) do
+ if adj_connid ~= nconnid then
+ counter_hit = counter_hit or traverser(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 then
+ break
+ end
+ end
+ end
+ return counter_hit
+end
+
+
+
+-- Merges the TS with merge_id into root_id and then deletes merge_id
+local function merge_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 end
+ if not ildb.may_modify_ts(mts) then return false end
+
+ -- cobble together the list of TCBs
+ for _, msigd in ipairs(mts.tc_breaks) do
+ local tcbs = ildb.get_tcbs(msigd)
+ if tcbs then
+ insert_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] = nil
+end
+
+local lntrans = { "A", "B" }
+local function sigd_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 then
+ atwarn("update_tcb_neighbors but node is NOK: "..minetest.pos_to_string(pos))
+ return
+ end
+
+ --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 in pairs(found_tcbs) do
+ local 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 then
+ if not ildb.get_ts(tcbs.ts_id) then
+ atwarn("Track section database is inconsistent, there's no TS with ID=",tcbs.ts_id)
+ tcbs.ts_id = nil
+ table.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)
+ end
+ elseif ts_id and tcbs.ts_id and tcbs.ts_id ~= ts_id then
+ atwarn("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)
+ end
+ end
+ if ts_id then
+ local ts = ildb.get_ts(ts_id)
+ for _, sigd in ipairs(list_eoi) do
+ local tcbs = ildb.get_tcbs(sigd)
+ tcbs.ts_id = ts_id
+ table.insert(ts.tc_breaks, sigd)
+ advtrains.interlocking.show_tcb_marker(sigd.p)
+ end
+ for _, mts in ipairs(ts_to_merge) do
+ merge_ts(ts_id, mts)
+ end
+ end
+end
+
+function ildb.link_track_sections(merge_id, root_id)
+ if merge_id == root_id then
+ return
+ end
+ merge_ts(root_id, merge_id)
+end
+
+function ildb.remove_from_interlocking(sigd)
+ local tcbs = ildb.get_tcbs(sigd)
+ if not ildb.may_modify_tcbs(tcbs) then return false end
+
+ if tcbs.ts_id then
+ local tsid = tcbs.ts_id
+ local ts = ildb.get_ts(tsid)
+ if not ts then
+ tcbs.ts_id = nil
+ return true
+ end
+
+ -- remove entry from the list
+ local idx = 1
+ while idx <= #ts.tc_breaks do
+ local cmp = ts.tc_breaks[idx]
+ if sigd_equal(sigd, cmp) then
+ table.remove(ts.tc_breaks, idx)
+ else
+ idx = idx + 1
+ end
+ end
+ tcbs.ts_id = nil
+
+ --ildb.sync_tcb_neighbors(sigd.p, sigd.s)
+
+ if #ts.tc_breaks == 0 then
+ track_sections[tsid] = nil
+ end
+ end
+ advtrains.interlocking.show_tcb_marker(sigd.p)
+ if tcbs.signal then
+ return false
+ end
+ return true
+end
+
+function ildb.remove_tcb(pos)
+ local pts = advtrains.roundfloorpts(pos)
+ if not track_circuit_breaks[pts] then
+ return true --FIX: not an error, because tcb is already removed
+ end
+ for connid=1,2 do
+ if not ildb.remove_from_interlocking({p=pos, s=connid}) then
+ return false
+ end
+ end
+ track_circuit_breaks[pts] = nil
+ return true
+end
+
+function ildb.dissolve_ts(ts_id)
+ local ts = ildb.get_ts(ts_id)
+ if not ildb.may_modify_ts(ts) then return false end
+ local tcbr = advtrains.merge_tables(ts.tc_breaks)
+ for _,sigd in ipairs(tcbr) do
+ ildb.remove_from_interlocking(sigd)
+ end
+ -- Note: ts gets removed in the moment of the removal of the last TCB.
+ return true
+end
+
+-- 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) then
+ return false
+ end
+ return true
+end
+
+
+function ildb.may_modify_tcbs(tcbs)
+ if tcbs.ts_id then
+ local ts = ildb.get_ts(tcbs.ts_id)
+ if ts and not ildb.may_modify_ts(ts) then
+ return false
+ end
+ end
+ return true
+end
+
+-- 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 reached
+function ildb.get_ts_at_pos(pos)
+ local node_ok, conns, rhe = advtrains.get_rail_info_at(pos, advtrains.all_tracktypes)
+ if not node_ok then
+ error("get_ts_at_pos but node is NOK: "..minetest.pos_to_string(pos))
+ end
+ local limit_hit = false
+ local found_tcbs = {}
+ for connid, conn in ipairs(conns) do -- Note: a breadth-first-search would be better for performance
+ limit_hit = limit_hit or traverser(found_tcbs, pos, conns, connid, 0, 1)
+ if #found_tcbs >= 1 then
+ local tcbs = ildb.get_tcbs(found_tcbs[1])
+ local ts
+ if tcbs.ts_id then
+ return tcbs.ts_id, found_tcbs[1]
+ else
+ return false
+ end
+ end
+ end
+ if limit_hit then
+ -- there was at least one limit hit
+ return nil
+ else
+ -- all traverser ends were track ends
+ return false
+ end
+end
+
+
+-- returns the sigd the signal at pos belongs to, if this is known
+function ildb.get_sigd_for_signal(pos)
+ local pts = advtrains.roundfloorpts(pos)
+ local sigd = signal_assignments[pts]
+ if sigd then
+ if not ildb.get_tcbs(sigd) then
+ signal_assignments[pts] = nil
+ return nil
+ end
+ return sigd
+ end
+ return nil
+end
+function 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] then
+ if purge then
+ -- is there still a signal assigned to it?
+ for connid, sigpos in pairs(influence_points[pts]) do
+ local asp = advtrains.interlocking.signal_get_aspect(sigpos)
+ if not asp then
+ atlog("Clearing orphaned signal influence point", pts, "/", connid)
+ ildb.clear_ip_signal(pts, connid)
+ end
+ end
+ -- if there's no side left after purging, return false
+ if not influence_points[pts] then return false end
+ end
+ return true
+ end
+ return false
+end
+
+-- checks if a signal is influencing here
+function ildb.get_ip_signal(pts, connid)
+ if influence_points[pts] then
+ return influence_points[pts][connid]
+ end
+end
+
+-- Tries to get aspect to obey here, if there
+-- is a signal ip at this location
+-- auto-clears invalid assignments
+function ildb.get_ip_signal_asp(pts, connid)
+ local p = ildb.get_ip_signal(pts, connid)
+ if p then
+ local asp = advtrains.interlocking.signal_get_aspect(p)
+ if not asp then
+ atlog("Clearing orphaned signal influence point", pts, "/", connid)
+ ildb.clear_ip_signal(pts, connid)
+ return nil
+ end
+ return asp, p
+ end
+ return nil
+end
+
+-- 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] = nil
+ for _,_ in pairs(influence_points[pts]) do
+ return
+ end
+ influence_points[pts] = nil
+end
+
+function ildb.get_ip_by_signalpos(spos)
+ for pts,tab in pairs(influence_points) do
+ for connid,pos in pairs(tab) do
+ if vector.equals(pos, spos) then
+ return pts, connid
+ end
+ end
+ end
+end
+-- clear signal assignment given the signal position
+function ildb.clear_ip_by_signalpos(spos)
+ local pts, connid = ildb.get_ip_by_signalpos(spos)
+ if pts then ildb.clear_ip_signal(pts, connid) end
+end
+
+
+advtrains.interlocking.db = ildb
+
+
+
+
diff --git a/advtrains_interlocking/demosignals.lua b/advtrains_interlocking/demosignals.lua
new file mode 100644
index 0000000..1c1b8b2
--- /dev/null
+++ b/advtrains_interlocking/demosignals.lua
@@ -0,0 +1,97 @@
+-- Demonstration signals
+-- Those can display the 3 main aspects of Ks signals
+
+-- Note that the group value of advtrains_signal is 2, which means "step 2 of signal capabilities"
+-- advtrains_signal=1 is meant for signals that do not implement set_aspect.
+
+
+local setaspect = function(pos, node, asp)
+ if asp.main == 0 then
+ advtrains.ndb.swap_node(pos, {name="advtrains_interlocking:ds_danger"})
+ else
+ if asp.dst ~= 0 and asp.main == -1 then
+ advtrains.ndb.swap_node(pos, {name="advtrains_interlocking:ds_free"})
+ else
+ advtrains.ndb.swap_node(pos, {name="advtrains_interlocking:ds_slow"})
+ end
+ end
+ local meta = minetest.get_meta(pos)
+ if meta then
+ meta:set_string("infotext", minetest.serialize(asp))
+ end
+end
+
+local suppasp = {
+ main = {0, 6, -1},
+ dst = {0, false},
+ shunt = false,
+ proceed_as_main = true,
+ info = {
+ call_on = false,
+ dead_end = false,
+ w_speed = nil,
+ }
+}
+
+minetest.register_node("advtrains_interlocking:ds_danger", {
+ description = "Demo signal at Danger",
+ tiles = {"at_il_signal_asp_danger.png"},
+ groups = {
+ cracky = 3,
+ advtrains_signal = 2,
+ save_in_at_nodedb = 1,
+ },
+ advtrains = {
+ set_aspect = setaspect,
+ supported_aspects = suppasp,
+ get_aspect = function(pos, node)
+ return advtrains.interlocking.DANGER
+ end,
+ },
+ on_rightclick = advtrains.interlocking.signal_rc_handler,
+ can_dig = advtrains.interlocking.signal_can_dig,
+ after_dig_node = advtrains.interlocking.signal_after_dig,
+})
+minetest.register_node("advtrains_interlocking:ds_free", {
+ description = "Demo signal at Free",
+ tiles = {"at_il_signal_asp_free.png"},
+ groups = {
+ cracky = 3,
+ advtrains_signal = 2,
+ save_in_at_nodedb = 1,
+ },
+ advtrains = {
+ set_aspect = setaspect,
+ supported_aspects = suppasp,
+ get_aspect = function(pos, node)
+ return {
+ main = -1,
+ }
+ end,
+ },
+ on_rightclick = advtrains.interlocking.signal_rc_handler,
+ can_dig = advtrains.interlocking.signal_can_dig,
+ after_dig_node = advtrains.interlocking.signal_after_dig,
+})
+minetest.register_node("advtrains_interlocking:ds_slow", {
+ description = "Demo signal at Slow",
+ tiles = {"at_il_signal_asp_slow.png"},
+ groups = {
+ cracky = 3,
+ advtrains_signal = 2,
+ save_in_at_nodedb = 1,
+ },
+ advtrains = {
+ set_aspect = setaspect,
+ supported_aspects = suppasp,
+ get_aspect = function(pos, node)
+ return {
+ main = 6,
+ }
+ end,
+ },
+ on_rightclick = advtrains.interlocking.signal_rc_handler,
+ can_dig = advtrains.interlocking.signal_can_dig,
+ after_dig_node = advtrains.interlocking.signal_after_dig,
+})
+
diff --git a/advtrains_interlocking/init.lua b/advtrains_interlocking/init.lua
new file mode 100644
index 0000000..a2f5882
--- /dev/null
+++ b/advtrains_interlocking/init.lua
@@ -0,0 +1,30 @@
+-- Advtrains interlocking system
+-- See database.lua for a detailed explanation
+
+advtrains.interlocking = {}
+
+advtrains.SHUNT_SPEED_MAX = 6
+
+function advtrains.interlocking.sigd_equal(sigd, cmp)
+ return vector.equals(sigd.p, cmp.p) and sigd.s==cmp.s
+end
+
+
+local modpath = minetest.get_modpath(minetest.get_current_modname()) .. DIR_DELIM
+
+dofile(modpath.."database.lua")
+dofile(modpath.."signal_api.lua")
+dofile(modpath.."demosignals.lua")
+dofile(modpath.."train_sections.lua")
+dofile(modpath.."route_prog.lua")
+dofile(modpath.."routesetting.lua")
+dofile(modpath.."tcb_ts_ui.lua")
+dofile(modpath.."route_ui.lua")
+dofile(modpath.."tool.lua")
+
+dofile(modpath.."approach.lua")
+dofile(modpath.."ars.lua")
+dofile(modpath.."tsr_rail.lua")
+
+
+minetest.register_privilege("interlocking", {description = "Can set up track sections, routes and signals.", give_to_singleplayer = true})
diff --git a/advtrains_interlocking/mod.conf b/advtrains_interlocking/mod.conf
new file mode 100644
index 0000000..3b2d029
--- /dev/null
+++ b/advtrains_interlocking/mod.conf
@@ -0,0 +1,7 @@
+name=advtrains_interlocking
+title=Advanced Trains Interlocking System
+description=Interlocking system for Advanced Trains
+author=orwell96
+
+depends=advtrains
+optional_depends=advtrains_train_track
diff --git a/advtrains_interlocking/models/at_il_tcb_node.obj b/advtrains_interlocking/models/at_il_tcb_node.obj
new file mode 100644
index 0000000..bb6aab5
--- /dev/null
+++ b/advtrains_interlocking/models/at_il_tcb_node.obj
@@ -0,0 +1,248 @@
+# Blender v2.76 (sub 0) OBJ File: ''
+# www.blender.org
+mtllib at_il_tcb_node.mtl
+o Cube
+v 0.038370 -0.500000 -0.038370
+v 0.038370 -0.500000 0.038370
+v -0.038370 -0.500000 0.038370
+v -0.038370 -0.500000 -0.038370
+v 0.038370 0.098086 -0.038370
+v 0.038370 0.098086 0.038370
+v -0.038370 0.098086 0.038370
+v -0.038370 0.098086 -0.038370
+v -0.182395 0.065479 0.099357
+v -0.182395 0.182395 0.099357
+v -0.182395 0.065479 -0.171034
+v -0.182395 0.182395 -0.171034
+v 0.182395 0.065479 0.099357
+v 0.182395 0.182395 0.099357
+v 0.182395 0.065479 -0.171034
+v 0.182395 0.182395 -0.171034
+v -0.112374 0.070035 -0.139406
+v -0.112374 -0.500000 -0.139406
+v 0.112189 -0.500000 -0.139406
+v 0.112189 0.070035 -0.139406
+v 0.122883 -0.500000 -0.137278
+v 0.122883 0.070035 -0.137278
+v 0.131950 -0.500000 -0.131220
+v 0.131950 0.070035 -0.131220
+v 0.138008 -0.500000 -0.122154
+v 0.138008 0.070035 -0.122154
+v 0.140135 -0.500000 -0.111459
+v 0.140135 0.070035 -0.111459
+v 0.138008 -0.500000 -0.100765
+v 0.138008 0.070035 -0.100765
+v 0.131950 -0.500000 -0.091698
+v 0.131950 0.070035 -0.091698
+v 0.122883 -0.500000 -0.085640
+v 0.122883 0.070035 -0.085640
+v 0.112189 -0.500000 -0.083513
+v 0.112189 0.070035 -0.083513
+v 0.101494 -0.500000 -0.085640
+v 0.101494 0.070035 -0.085640
+v 0.092428 -0.500000 -0.091698
+v 0.092428 0.070035 -0.091698
+v 0.086370 -0.500000 -0.100765
+v 0.086370 0.070035 -0.100765
+v 0.084242 -0.500000 -0.111459
+v 0.084242 0.070035 -0.111459
+v 0.086370 -0.500000 -0.122154
+v 0.086370 0.070035 -0.122154
+v 0.092428 -0.500000 -0.131220
+v 0.092428 0.070035 -0.131220
+v 0.101494 -0.500000 -0.137278
+v 0.101494 0.070035 -0.137278
+v -0.101679 -0.500000 -0.137278
+v -0.101679 0.070035 -0.137278
+v -0.092613 -0.500000 -0.131220
+v -0.092613 0.070035 -0.131220
+v -0.086555 -0.500000 -0.122154
+v -0.086555 0.070035 -0.122154
+v -0.084428 -0.500000 -0.111459
+v -0.084428 0.070035 -0.111459
+v -0.086555 -0.500000 -0.100765
+v -0.086555 0.070035 -0.100765
+v -0.092613 -0.500000 -0.091698
+v -0.092613 0.070035 -0.091698
+v -0.101679 -0.500000 -0.085640
+v -0.101679 0.070035 -0.085640
+v -0.112374 -0.500000 -0.083513
+v -0.112374 0.070035 -0.083513
+v -0.123069 -0.500000 -0.085640
+v -0.123069 0.070035 -0.085640
+v -0.132135 -0.500000 -0.091698
+v -0.132135 0.070035 -0.091698
+v -0.138193 -0.500000 -0.100765
+v -0.138193 0.070035 -0.100765
+v -0.140320 -0.500000 -0.111459
+v -0.140320 0.070035 -0.111459
+v -0.138193 -0.500000 -0.122154
+v -0.138193 0.070035 -0.122154
+v -0.132135 -0.500000 -0.131220
+v -0.132135 0.070035 -0.131220
+v -0.123069 -0.500000 -0.137278
+v -0.123069 0.070035 -0.137278
+vt 0.876073 0.266665
+vt 0.876073 0.977812
+vt 0.784827 0.977812
+vt 0.784827 0.266665
+vt 0.693582 0.977812
+vt 0.693582 0.266665
+vt 0.602336 0.977812
+vt 0.602336 0.266665
+vt 0.967319 0.266665
+vt 0.967319 0.977812
+vt 0.147929 0.032040
+vt 0.469434 0.032040
+vt 0.469434 0.171057
+vt 0.147929 0.171057
+vt 0.903184 0.032040
+vt 0.903184 0.171057
+vt 0.147929 0.032751
+vt 0.469434 0.032751
+vt 0.469434 0.171768
+vt 0.147929 0.171768
+vt 0.903184 0.032751
+vt 0.903183 0.171768
+vt 0.263807 0.270252
+vt 0.585312 0.270252
+vt 0.585312 0.704001
+vt 0.263807 0.704001
+vt 0.584297 0.703059
+vt 0.262792 0.703059
+vt 0.262793 0.269309
+vt 0.584297 0.269309
+vt 0.108472 0.980897
+vt 0.108473 0.303114
+vt 0.121438 0.303114
+vt 0.121438 0.980897
+vt 0.081877 0.980125
+vt 0.081879 0.302342
+vt 0.094844 0.302342
+vt 0.094843 0.980125
+vt 0.095507 0.980897
+vt 0.095508 0.303114
+vt 0.107809 0.302342
+vt 0.107808 0.980125
+vt 0.082541 0.980897
+vt 0.082543 0.303114
+vt 0.120774 0.302342
+vt 0.120774 0.980125
+vt 0.069575 0.980897
+vt 0.069577 0.303114
+vt 0.133739 0.302342
+vt 0.133740 0.980125
+vt 0.056609 0.980897
+vt 0.056612 0.303114
+vt 0.146705 0.302342
+vt 0.146706 0.980125
+vt 0.043643 0.980897
+vt 0.043647 0.303114
+vt 0.159670 0.302342
+vt 0.159672 0.980125
+vt 0.030677 0.980897
+vt 0.030682 0.303113
+vt 0.172635 0.302342
+vt 0.172638 0.980125
+vt 0.017711 0.980897
+vt 0.017717 0.303113
+vt 0.185600 0.302342
+vt 0.185604 0.980125
+vt 0.212200 0.980896
+vt 0.212195 0.303113
+vt 0.225160 0.303113
+vt 0.225166 0.980896
+vt 0.198565 0.302342
+vt 0.198570 0.980125
+vt 0.199234 0.980897
+vt 0.199230 0.303114
+vt 0.211531 0.302342
+vt 0.211536 0.980125
+vt 0.186268 0.980897
+vt 0.186264 0.303114
+vt 0.224496 0.302342
+vt 0.224502 0.980125
+vt 0.173302 0.980897
+vt 0.173299 0.303114
+vt 0.017047 0.980125
+vt 0.017052 0.302342
+vt 0.030018 0.302342
+vt 0.030013 0.980125
+vt 0.134403 0.303114
+vt 0.134404 0.980897
+vt 0.160336 0.980897
+vt 0.160334 0.303114
+vt 0.042983 0.302342
+vt 0.042979 0.980125
+vt 0.147369 0.303114
+vt 0.147370 0.980897
+vt 0.055948 0.302342
+vt 0.055945 0.980125
+vt 0.068911 0.980125
+vt 0.068913 0.302342
+vn 1.000000 0.000000 0.000000
+vn -0.000000 -0.000000 1.000000
+vn -1.000000 -0.000000 -0.000000
+vn 0.000000 0.000000 -1.000000
+vn 0.000000 -1.000000 0.000000
+vn 0.000000 1.000000 0.000000
+vn -0.831500 0.000000 -0.555600
+vn 0.195100 0.000000 -0.980800
+vn -0.980800 0.000000 -0.195100
+vn 0.555600 0.000000 -0.831500
+vn -0.980800 0.000000 0.195100
+vn 0.831500 0.000000 -0.555600
+vn -0.831500 0.000000 0.555600
+vn 0.980800 0.000000 -0.195100
+vn -0.555600 0.000000 0.831500
+vn 0.980800 0.000000 0.195100
+vn -0.195100 0.000000 0.980800
+vn 0.831500 0.000000 0.555600
+vn 0.195100 0.000000 0.980800
+vn 0.555600 0.000000 0.831500
+vn -0.555600 0.000000 -0.831500
+vn -0.195100 0.000000 -0.980800
+usemtl Material
+s off
+f 1/1/1 5/2/1 6/3/1 2/4/1
+f 2/4/2 6/3/2 7/5/2 3/6/2
+f 3/6/3 7/5/3 8/7/3 4/8/3
+f 5/2/4 1/1/4 4/9/4 8/10/4
+f 10/11/3 12/12/3 11/13/3 9/14/3
+f 12/12/4 16/15/4 15/16/4 11/13/4
+f 16/17/1 14/18/1 13/19/1 15/20/1
+f 14/18/2 10/21/2 9/22/2 13/19/2
+f 9/23/5 11/24/5 15/25/5 13/26/5
+f 14/27/6 16/28/6 12/29/6 10/30/6
+f 75/31/7 76/32/7 78/33/7 77/34/7
+f 19/35/8 20/36/8 22/37/8 21/38/8
+f 73/39/9 74/40/9 76/32/9 75/31/9
+f 21/38/10 22/37/10 24/41/10 23/42/10
+f 71/43/11 72/44/11 74/40/11 73/39/11
+f 23/42/12 24/41/12 26/45/12 25/46/12
+f 69/47/13 70/48/13 72/44/13 71/43/13
+f 25/46/14 26/45/14 28/49/14 27/50/14
+f 67/51/15 68/52/15 70/48/15 69/47/15
+f 27/50/16 28/49/16 30/53/16 29/54/16
+f 65/55/17 66/56/17 68/52/17 67/51/17
+f 29/54/18 30/53/18 32/57/18 31/58/18
+f 63/59/19 64/60/19 66/56/19 65/55/19
+f 31/58/20 32/57/20 34/61/20 33/62/20
+f 61/63/20 62/64/20 64/60/20 63/59/20
+f 33/62/19 34/61/19 36/65/19 35/66/19
+f 59/67/18 60/68/18 62/69/18 61/70/18
+f 35/66/17 36/65/17 38/71/17 37/72/17
+f 57/73/16 58/74/16 60/68/16 59/67/16
+f 37/72/15 38/71/15 40/75/15 39/76/15
+f 55/77/14 56/78/14 58/74/14 57/73/14
+f 39/76/13 40/75/13 42/79/13 41/80/13
+f 53/81/12 54/82/12 56/78/12 55/77/12
+f 41/83/11 42/84/11 44/85/11 43/86/11
+f 77/34/21 78/33/21 80/87/21 79/88/21
+f 51/89/10 52/90/10 54/82/10 53/81/10
+f 43/86/9 44/85/9 46/91/9 45/92/9
+f 79/88/22 80/87/22 17/93/22 18/94/22
+f 18/94/8 17/93/8 52/90/8 51/89/8
+f 45/92/7 46/91/7 48/95/7 47/96/7
+f 49/97/22 50/98/22 20/36/22 19/35/22
+f 47/96/21 48/95/21 50/98/21 49/97/21
diff --git a/advtrains_interlocking/route_prog.lua b/advtrains_interlocking/route_prog.lua
new file mode 100644
index 0000000..6abe431
--- /dev/null
+++ b/advtrains_interlocking/route_prog.lua
@@ -0,0 +1,549 @@
+-- Route programming system
+
+--[[
+Progamming routes:
+1. Select "program new route" in the signalling dialog
+-> route_start marker will appear to designate route-program mode
+2. Do those actions in any order:
+A. punch a TCB marker node to proceed route along this TCB. This will only work if
+ this is actually a TCB bordering the current TS, and will place a
+ route_set marker and shift to the next TS
+B. right-click a turnout to switch it (no impact to route programming
+C. punch a turnout (or some other passive component) to fix its state (toggle)
+ for the route. A sprite telling "Route Fix" will show that fact.
+3. To complete route setting, use the chat command '/at_program_route <route name>'.
+ The last punched TCB will get a 'route end' marker
+ The end of a route should be at another signal facing the same direction as the entrance signal,
+ however this is not enforced and left up to the signal engineer (the programmer)
+
+The route visualization will also be used to visualize routes after they have been programmed.
+]]--
+
+
+-- table with objectRefs
+local markerent = {}
+
+minetest.register_entity("advtrains_interlocking:routemarker", {
+ visual = "mesh",
+ mesh = "trackplane.b3d",
+ textures = {"at_il_route_set.png"},
+ collisionbox = {-1,-0.5,-1, 1,-0.4,1},
+ visual_size = {x=10, y=10},
+ on_punch = function(self)
+ self.object:remove()
+ end,
+ get_staticdata = function() return "STATIC" end,
+ on_activate = function(self, sdata) if sdata=="STATIC" then self.object:remove() end end,
+ static_save = false,
+})
+
+
+-- Spawn or update a route marker entity
+-- pos: position where this is going to be
+-- key: something unique to determine which entity to remove if this was set before
+-- img: texture
+local function routemarker(context, pos, key, img, yaw, itex)
+ if not markerent[context] then
+ markerent[context] = {}
+ end
+ if markerent[context][key] then
+ markerent[context][key]:remove()
+ end
+
+ local obj = minetest.add_entity(vector.add(pos, {x=0, y=0.3, z=0}), "advtrains_interlocking:routemarker")
+ if not obj then return end
+ obj:set_yaw(yaw)
+ obj:set_properties({
+ infotext = itex,
+ textures = {img},
+ })
+
+ markerent[context][key] = obj
+end
+
+minetest.register_entity("advtrains_interlocking:routesprite", {
+ visual = "sprite",
+ textures = {"at_il_turnout_free.png"},
+ collisionbox = {-0.2,-0.2,-0.2, 0.2,0.2,0.2},
+ visual_size = {x=1, y=1},
+ on_punch = function(self)
+ if self.callback then
+ self.callback()
+ end
+ self.object:remove()
+ end,
+ get_staticdata = function() return "STATIC" end,
+ on_activate = function(self, sdata) if sdata=="STATIC" then self.object:remove() end end,
+ static_save = false,
+})
+
+
+-- Spawn or update a route sprite entity
+-- pos: position where this is going to be
+-- key: something unique to determine which entity to remove if this was set before
+-- img: texture
+local function routesprite(context, pos, key, img, itex, callback)
+ if not markerent[context] then
+ markerent[context] = {}
+ end
+ if markerent[context][key] then
+ markerent[context][key]:remove()
+ end
+
+ local obj = minetest.add_entity(vector.add(pos, {x=0, y=0, z=0}), "advtrains_interlocking:routesprite")
+ if not obj then return end
+ obj:set_properties({
+ infotext = itex,
+ textures = {img},
+ })
+
+ if callback then
+ obj:get_luaentity().callback = callback
+ end
+
+ markerent[context][key] = obj
+end
+
+--[[
+Route definition:
+route = {
+ name = <string>
+ [n] = {
+ next = <sigd>, -- of the next (note: next) TCB on the route
+ locks = {<pts> = "state"} -- route locks of this route segment
+ }
+ terminal = <sigd>,
+ aspect = <signal aspect>,--note, might change in future
+}
+The first item in the TCB path (namely i=0) is always the start signal of this route,
+so this is left out.
+All subsequent entries, starting from 1, contain:
+- all route locks of the segment on TS between the (i-1). and the i. TCB
+- the next TCB signal describer in proceeding direction of the route.
+'Terminal' once again repeats the "next" entry of the last route segment.
+It is needed for distant signal aspect determination. If it is not set,
+the distant signal aspect is determined as DANGER.
+]]--
+
+local function chat(pname, message)
+ minetest.chat_send_player(pname, "[Route programming] "..message)
+end
+local function clear_lock(locks, pname, pts)
+ locks[pts] = nil
+ chat(pname, pts.." is no longer affected when this route is set.")
+end
+
+local function otherside(s)
+ if s==1 then return 2 else return 1 end
+end
+
+function advtrains.interlocking.clear_visu_context(context)
+ if not markerent[context] then return end
+ for key, obj in pairs(markerent[context]) do
+ obj:remove()
+ end
+ markerent[context] = nil
+end
+
+-- visualize route. 'context' is a string that identifies the context of this visualization
+-- e.g. prog_<player> or vis_<pts> for later visualizations
+-- last 2 parameters are only to be used in the context of route programming!
+function advtrains.interlocking.visualize_route(origin, route, context, tmp_lcks, pname)
+ advtrains.interlocking.clear_visu_context(context)
+
+ local oyaw = 0
+ local onode_ok, oconns, orhe = advtrains.get_rail_info_at(origin.p, advtrains.all_tracktypes)
+ if onode_ok then
+ oyaw = advtrains.dir_to_angle(oconns[origin.s].c)
+ end
+ routemarker(context, origin.p, "rte_origin", "at_il_route_start.png", oyaw, route.name)
+
+ local c_sigd = origin
+ for k,v in ipairs(route) do
+ c_sigd = v.next
+ -- display route path
+ -- Final "next" marker can be EOI, thus undefined. This is legitimate.
+ if c_sigd then
+ local yaw = 0
+ local node_ok, conns, rhe = advtrains.get_rail_info_at(c_sigd.p, advtrains.all_tracktypes)
+ if node_ok then
+ yaw = advtrains.dir_to_angle(conns[c_sigd.s].c)
+ end
+ local img = "at_il_route_set.png"
+ if k==#route and not tmp_lcks then
+ img = "at_il_route_end.png"
+ end
+ routemarker(context, c_sigd.p, "rte"..k, img, yaw, route.name.." #"..k)
+ end
+ -- display locks
+ for pts, state in pairs(v.locks) do
+ local pos = minetest.string_to_pos(pts)
+ routesprite(context, pos, "fix"..k..pts, "at_il_route_lock.png", "Fixed in state '"..state.."' by route "..route.name.." until segment #"..k.." is freed.")
+ end
+ end
+
+ -- The presence of tmp_lcks tells us that we are displaying during route programming.
+ if tmp_lcks then
+ -- display route end markers at appropriate places (check next TS, if it exists)
+ local terminal = c_sigd
+ if terminal then
+ local term_tcbs = advtrains.interlocking.db.get_tcbs(terminal)
+ if term_tcbs.ts_id then
+ local over_ts = advtrains.interlocking.db.get_ts(term_tcbs.ts_id)
+ for i, sigd in ipairs(over_ts.tc_breaks) do
+ if not vector.equals(sigd.p, terminal.p) then
+ local yaw = 0
+ local node_ok, conns, rhe = advtrains.get_rail_info_at(sigd.p, advtrains.all_tracktypes)
+ if node_ok then
+ yaw = advtrains.dir_to_angle(conns[otherside(sigd.s)].c)
+ end
+ routemarker(context, sigd.p, "rteterm"..i, "at_il_route_end.png", yaw, route.name.." Terminal "..i)
+ end
+ end
+ end
+ end
+ -- display locks set by player
+ for pts, state in pairs(tmp_lcks) do
+ local pos = minetest.string_to_pos(pts)
+ routesprite(context, pos, "fixp"..pts, "at_il_route_lock_edit.png", "Fixed in state '"..state.."' by route "..route.name.." (punch to unfix)",
+ function() clear_lock(tmp_lcks, pname, pts) end)
+ end
+ end
+end
+
+
+local player_rte_prog = {}
+
+function advtrains.interlocking.init_route_prog(pname, sigd)
+ if not minetest.check_player_privs(pname, "interlocking") then
+ minetest.chat_send_player(pname, "Insufficient privileges to use this!")
+ return
+ end
+ player_rte_prog[pname] = {
+ origin = sigd,
+ route = {
+ name = "PROG["..pname.."]",
+ },
+ tmp_lcks = {},
+ }
+ advtrains.interlocking.visualize_route(sigd, player_rte_prog[pname].route, "prog_"..pname, player_rte_prog[pname].tmp_lcks, pname)
+ minetest.chat_send_player(pname, "Route programming mode active. Punch TCBs to add route segments, punch turnouts to lock them.")
+end
+
+local function get_last_route_item(origin, route)
+ if #route == 0 then
+ return origin
+ end
+ return route[#route].next
+end
+
+local function do_advance_route(pname, rp, sigd, tsname)
+ table.insert(rp.route, {next = sigd, locks = rp.tmp_lcks})
+ rp.tmp_lcks = {}
+ chat(pname, "Added track section '"..tsname.."' to the route.")
+end
+
+local function finishrpform(pname)
+ local rp = player_rte_prog[pname]
+ if not rp then return end
+
+ local form = "size[7,6]label[0.5,0.5;Finish programming route]"
+ local terminal = get_last_route_item(rp.origin, rp.route)
+ if terminal then
+ local term_tcbs = advtrains.interlocking.db.get_tcbs(terminal)
+
+ if term_tcbs.signal then
+ form = form .. "label[0.5,1.5;Route ends at signal:]"
+ form = form .. "label[0.5,2 ;"..term_tcbs.signal_name.."]"
+ else
+ form = form .. "label[0.5,1.5;WARNING: Route does not end at a signal.]"
+ form = form .. "label[0.5,2 ;Routes should in most cases end at signals.]"
+ form = form .. "label[0.5,2.5;Cancel if you are unsure!]"
+ end
+ else
+ form = form .. "label[0.5,1.5;Route leads into]"
+ form = form .. "label[0.5,2 ;non-interlocked area]"
+ end
+ form = form.."field[0.8,3.5;5.2,1;name;Enter Route Name;]"
+ form = form.."button_exit[0.5,4.5; 5,1;save;Save Route]"
+
+
+ minetest.show_formspec(pname, "at_il_routepf", form)
+end
+
+
+local function check_advance_valid(tcbpos, rp)
+ -- track circuit break, try to advance route over it
+ local lri = get_last_route_item(rp.origin, rp.route)
+ if not lri then
+ return false, false
+ end
+
+ local is_endpoint = false
+
+ local this_sigd, this_ts, adv_side
+
+ if vector.equals(lri.p, tcbpos) then
+ -- If the player just punched the last TCB again, it's of course possible to
+ -- finish the route here (although it can't be advanced by here.
+ -- Fun fact: you can now program routes that end exactly where they begin :)
+ is_endpoint = true
+ this_sigd = lri
+ else
+ -- else, we need to check whether this TS actually borders
+ local start_tcbs = advtrains.interlocking.db.get_tcbs(lri)
+ if not start_tcbs.ts_id then
+ return false, false
+ end
+
+ this_ts = advtrains.interlocking.db.get_ts(start_tcbs.ts_id)
+ for _,sigd in ipairs(this_ts.tc_breaks) do
+ if vector.equals(sigd.p, tcbpos) then
+ adv_side = otherside(sigd.s)
+ end
+ end
+ if not adv_side then
+ -- this TCB is not bordering to the section
+ return false, false
+ end
+ this_sigd = {p=tcbpos, s=adv_side}
+ end
+
+ -- check whether the ts at the other end is capable of "end over"
+ local adv_tcbs = advtrains.interlocking.db.get_tcbs(this_sigd)
+ local next_tsid = adv_tcbs.ts_id
+ local can_over, over_ts, next_tc_bs = false, nil, nil
+ local cannotover_rsn = "Next section is diverging (>2 TCBs)"
+ if next_tsid then
+ -- you may not advance over EOI. While this is technically possible,
+ -- in practise this just enters an unnecessary extra empty route item.
+ over_ts = advtrains.interlocking.db.get_ts(adv_tcbs.ts_id)
+ next_tc_bs = over_ts.tc_breaks
+ can_over = #next_tc_bs <= 2
+ else
+ cannotover_rsn = "End of interlocking"
+ end
+
+ local over_sigd = nil
+ if can_over then
+ if next_tc_bs and #next_tc_bs == 2 then
+ local sdt
+ if vector.equals(next_tc_bs[1].p, tcbpos) then
+ sdt = next_tc_bs[2]
+ end
+ if vector.equals(next_tc_bs[2].p, tcbpos) then
+ sdt = next_tc_bs[1]
+ end
+ if not sdt then
+ error("Inconsistency: "..dump(next_ts))
+ end
+ -- swap TCB direction
+ over_sigd = {p = sdt.p, s = otherside(sdt.s) }
+ end
+ end
+
+ return is_endpoint, true, this_sigd, this_ts, can_over, over_ts, over_sigd, cannotover_rsn
+end
+
+local function show_routing_form(pname, tcbpos, message)
+
+ local rp = player_rte_prog[pname]
+
+ if not rp then return end
+
+ local is_endpoint, advance_valid, this_sigd, this_ts, can_over, over_ts, over_sigd, cannotover_rsn = check_advance_valid(tcbpos, rp)
+
+ -- at this place, advance_valid shows whether the current route can be advanced
+ -- over this TCB.
+ -- If it can:
+ -- Advance over (continue programming)
+ -- End here
+ -- Advance and end (only <=2 TCBs, terminal signal needs to be known)
+ -- if not:
+ -- show nothing at all
+ -- In all cases, Discard and Backtrack buttons needed.
+
+ local form = "size[7,9.5]label[0.5,0.5;Advance/Complete Route]"
+ if message then
+ form = form .. "label[0.5,1;"..message.."]"
+ end
+
+ if advance_valid and not is_endpoint then
+ form = form.. "label[0.5,1.8;Advance to next route section]"
+ form = form.."image_button[0.5,2.2; 5,1;at_il_routep_advance.png;advance;]"
+
+ form = form.. "label[0.5,3.5;-------------------------]"
+ else
+ form = form.. "label[0.5,2.3;This TCB is not suitable as]"
+ form = form.. "label[0.5,2.8;route continuation.]"
+ end
+ if advance_valid or is_endpoint then
+ form = form.. "label[0.5,3.8;Finish route HERE]"
+ form = form.."image_button[0.5, 4.2; 5,1;at_il_routep_end_here.png;endhere;]"
+ if can_over then
+ form = form.. "label[0.5,5.3;Finish route at end of NEXT section]"
+ form = form.."image_button[0.5,5.7; 5,1;at_il_routep_end_over.png;endover;]"
+ else
+ form = form.. "label[0.5,5.3;Advancing over next section is]"
+ form = form.. "label[0.5,5.8;impossible at this place.]"
+ if cannotover_rsn then
+ form = form.. "label[0.5,6.3;"..cannotover_rsn.."]"
+ end
+ end
+ end
+
+ form = form.. "label[0.5,7;-------------------------]"
+ if #rp.route > 0 then
+ form = form.."button[0.5,7.4; 5,1;retract;Step back one section]"
+ end
+ form = form.."button[0.5,8.4; 5,1;cancel;Cancel route programming]"
+
+ minetest.show_formspec(pname, "at_il_rprog_"..minetest.pos_to_string(tcbpos), form)
+end
+
+minetest.register_on_player_receive_fields(function(player, formname, fields)
+ local pname = player:get_player_name()
+
+ local tcbpts = string.match(formname, "^at_il_rprog_([^_]+)$")
+ local tcbpos
+ if tcbpts then
+ tcbpos = minetest.string_to_pos(tcbpts)
+ end
+ if tcbpos then
+ -- RPROG form
+ local rp = player_rte_prog[pname]
+ if not rp then
+ minetest.close_formspec(pname, formname)
+ return
+ end
+
+ local is_endpoint, advance_valid, this_sigd, this_ts, can_over, over_ts, over_sigd = check_advance_valid(tcbpos, rp)
+
+ if advance_valid then
+ if fields.advance then
+ -- advance route
+ if not is_endpoint then
+ do_advance_route(pname, rp, this_sigd, this_ts.name)
+ end
+ end
+ if fields.endhere then
+ if not is_endpoint then
+ do_advance_route(pname, rp, this_sigd, this_ts.name)
+ end
+ finishrpform(pname)
+ end
+ if can_over and fields.endover then
+ if not is_endpoint then
+ do_advance_route(pname, rp, this_sigd, this_ts.name)
+ end
+ do_advance_route(pname, rp, over_sigd, over_ts and over_ts.name or "--EOI--")
+ finishrpform(pname)
+ end
+ end
+ if fields.retract then
+ if #rp.route <= 0 then
+ minetest.close_formspec(pname, formname)
+ return
+ end
+ rp.tmp_locks = rp.route[#rp.route].locks
+ rp.route[#rp.route] = nil
+ chat(pname, "Route section "..(#rp.route+1).." removed.")
+ end
+ if fields.cancel then
+ player_rte_prog[pname] = nil
+ advtrains.interlocking.clear_visu_context("prog_"..pname)
+ chat(pname, "Route discarded.")
+ minetest.close_formspec(pname, formname)
+ return
+ end
+
+ advtrains.interlocking.visualize_route(rp.origin, rp.route, "prog_"..pname, rp.tmp_lcks, pname)
+ minetest.close_formspec(pname, formname)
+ return
+ end
+
+ if formname == "at_il_routepf" then
+ if not fields.save or not fields.name then return end
+ if fields.name == "" then
+ -- show form again
+ finishrpform(pname)
+ return
+ end
+
+ local rp = player_rte_prog[pname]
+ if rp then
+ if #rp.route <= 0 then
+ chat(pname, "Cannot program route without a target")
+ return
+ end
+
+ local tcbs = advtrains.interlocking.db.get_tcbs(rp.origin)
+ if not tcbs then
+ chat(pname, "The origin TCB has become unknown during programming. Try again.")
+ return
+ end
+
+ local terminal = get_last_route_item(rp.origin, rp.route)
+ rp.route.terminal = terminal
+ rp.route.name = fields.name
+
+ table.insert(tcbs.routes, rp.route)
+
+ advtrains.interlocking.clear_visu_context("prog_"..pname)
+ player_rte_prog[pname] = nil
+ chat(pname, "Successfully programmed route.")
+
+ advtrains.interlocking.show_route_edit_form(pname, rp.origin, #tcbs.routes)
+ return
+ end
+ end
+end)
+
+
+-- Central route programming punch callback
+minetest.register_on_punchnode(function(pos, node, player, pointed_thing)
+ local pname = player:get_player_name()
+ if not minetest.check_player_privs(pname, "interlocking") then
+ return
+ end
+ local rp = player_rte_prog[pname]
+ if rp then
+ -- determine what the punched node is
+ if minetest.get_item_group(node.name, "at_il_track_circuit_break") >= 1 then
+ -- get position of the assigned tcb
+ local meta = minetest.get_meta(pos)
+ local tcbpts = meta:get_string("tcb_pos")
+ if tcbpts == "" then
+ chat(pname, "This TCB is unconfigured, you first need to assign it to a rail")
+ return
+ end
+ local tcbpos = minetest.string_to_pos(tcbpts)
+
+ -- show formspec
+
+ show_routing_form(pname, tcbpos)
+
+ advtrains.interlocking.visualize_route(rp.origin, rp.route, "prog_"..pname, rp.tmp_lcks, pname)
+
+ return
+ end
+ if advtrains.is_passive(pos) then
+ local pts = advtrains.roundfloorpts(pos)
+ if rp.tmp_lcks[pts] then
+ clear_lock(rp.tmp_lcks, pname, pts)
+ else
+ local state = advtrains.getstate(pos)
+ rp.tmp_lcks[pts] = state
+ chat(pname, pts.." is held in "..state.." position when this route is set and freed ")
+ end
+ advtrains.interlocking.visualize_route(rp.origin, rp.route, "prog_"..pname, rp.tmp_lcks, pname)
+ return
+ end
+
+ end
+end)
+
+
+--TODO on route setting
+-- routes should end at signals. complete route setting by punching a signal, and command as exceptional route completion
+-- Create simpler way to advance a route to the next tcb/signal on simple sections without turnouts
diff --git a/advtrains_interlocking/route_ui.lua b/advtrains_interlocking/route_ui.lua
new file mode 100644
index 0000000..1999941
--- /dev/null
+++ b/advtrains_interlocking/route_ui.lua
@@ -0,0 +1,153 @@
+-- route_ui.lua
+-- User interface for showing and editing routes
+
+local atil = advtrains.interlocking
+local ildb = atil.db
+
+-- TODO duplicate
+local lntrans = { "A", "B" }
+local function sigd_to_string(sigd)
+ return minetest.pos_to_string(sigd.p).." / "..lntrans[sigd.s]
+end
+
+
+
+function atil.show_route_edit_form(pname, sigd, routeid)
+
+ if not minetest.check_player_privs(pname, {train_operator=true, interlocking=true}) then
+ minetest.chat_send_player(pname, "Insufficient privileges to use this!")
+ return
+ end
+
+ local tcbs = atil.db.get_tcbs(sigd)
+ if not tcbs then return end
+ local route = tcbs.routes[routeid]
+ if not route then return end
+
+ local form = "size[9,10]label[0.5,0.2;Route overview]"
+ form = form.."field[0.8,1.2;6.5,1;name;Route name;"..minetest.formspec_escape(route.name).."]"
+ form = form.."button[7.0,0.9;1.5,1;setname;Set]"
+
+ -- construct textlist for route information
+ local tab = {}
+ local function itab(t)
+ tab[#tab+1] = minetest.formspec_escape(string.gsub(t, ",", " "))
+ end
+ itab("TCB "..sigd_to_string(sigd).." ("..tcbs.signal_name..") Route #"..routeid)
+
+ -- this code is partially copy-pasted from routesetting.lua
+ -- we start at the tc designated by signal
+ local c_sigd = sigd
+ local i = 1
+ local c_tcbs, c_ts_id, c_ts, c_rseg, c_lckp
+ while c_sigd and i<=#route do
+ c_tcbs = ildb.get_tcbs(c_sigd)
+ if not c_tcbs then
+ itab("-!- No TCBS at "..sigd_to_string(c_sigd)..". Please reconfigure route!")
+ break
+ end
+ c_ts_id = c_tcbs.ts_id
+ if not c_ts_id then
+ itab("-!- No track section adjacent to "..sigd_to_string(c_sigd)..". Please reconfigure route!")
+ break
+ end
+ c_ts = ildb.get_ts(c_ts_id)
+
+ c_rseg = route[i]
+ c_lckp = {}
+
+ itab(""..i.." Entry "..sigd_to_string(c_sigd).." -> Sec. "..(c_ts and c_ts.name or "-").." -> Exit "..(c_rseg.next and sigd_to_string(c_rseg.next) or "END"))
+
+ if c_rseg.locks then
+ for pts, state in pairs(c_rseg.locks) do
+
+ local pos = minetest.string_to_pos(pts)
+ itab(" Lock: "..pts.." -> "..state)
+ if not advtrains.is_passive(pos) then
+ itab("-!- No passive component at "..pts..". Please reconfigure route!")
+ break
+ end
+ end
+ end
+ -- advance
+ c_sigd = c_rseg.next
+ i = i + 1
+ end
+ if c_sigd then
+ local e_tcbs = ildb.get_tcbs(c_sigd)
+ itab("Route end: "..sigd_to_string(c_sigd).." ("..(e_tcbs and e_tcbs.signal_name or "-")..")")
+ else
+ itab("Route ends on dead-end")
+ end
+
+ form = form.."textlist[0.5,2;7.75,3.9;rtelog;"..table.concat(tab, ",").."]"
+
+ form = form.."button[0.5,6;3,1;back;<<< Back to signal]"
+ form = form.."button[4.5,6;2,1;aspect;Signal Aspect]"
+ form = form.."button[6.5,6;2,1;delete;Delete Route]"
+
+ --atdebug(route.ars)
+ form = form.."style[ars;font=mono]"
+ form = form.."textarea[0.8,7.3;5,3;ars;ARS Rule List;"..atil.ars_to_text(route.ars).."]"
+ form = form.."button[5.5,7.23;3,1;savears;Save ARS List]"
+
+ minetest.show_formspec(pname, "at_il_routeedit_"..minetest.pos_to_string(sigd.p).."_"..sigd.s.."_"..routeid, form)
+
+end
+
+
+minetest.register_on_player_receive_fields(function(player, formname, fields)
+ local pname = player:get_player_name()
+ if not minetest.check_player_privs(pname, {train_operator=true, interlocking=true}) then
+ return
+ end
+
+ local pts, connids, routeids = string.match(formname, "^at_il_routeedit_([^_]+)_(%d)_(%d+)$")
+ local pos, connid, routeid
+ if pts then
+ pos = minetest.string_to_pos(pts)
+ connid = tonumber(connids)
+ routeid = tonumber(routeids)
+ if not connid or connid<1 or connid>2 then return end
+ if not routeid then return end
+ end
+ if pos and connid and routeid and not fields.quit then
+ local sigd = {p=pos, s=connid}
+ local tcbs = ildb.get_tcbs(sigd)
+ if not tcbs then return end
+ local route = tcbs.routes[routeid]
+ if not route then return end
+
+ if fields.setname and fields.name then
+ route.name = fields.name
+ end
+
+ if fields.aspect then
+ local suppasp = advtrains.interlocking.signal_get_supported_aspects(tcbs.signal)
+
+ local callback = function(pname, asp)
+ route.aspect = asp
+ advtrains.interlocking.show_route_edit_form(pname, sigd, routeid)
+ end
+
+ advtrains.interlocking.show_signal_aspect_selector(pname, suppasp, route.name, callback, route.aspect or advtrains.interlocking.GENERIC_FREE)
+ return
+ end
+ if fields.delete then
+ -- if something set the route in the meantime, make sure this doesn't break.
+ atil.route.update_route(sigd, tcbs, nil, true)
+ table.remove(tcbs.routes, routeid)
+ advtrains.interlocking.show_signalling_form(sigd, pname)
+ end
+
+ if fields.ars and fields.savears then
+ route.ars = atil.text_to_ars(fields.ars)
+ --atdebug(route.ars)
+ end
+
+ if fields.back then
+ advtrains.interlocking.show_signalling_form(sigd, pname)
+ end
+
+ end
+end)
diff --git a/advtrains_interlocking/routesetting.lua b/advtrains_interlocking/routesetting.lua
new file mode 100644
index 0000000..67efaea
--- /dev/null
+++ b/advtrains_interlocking/routesetting.lua
@@ -0,0 +1,342 @@
+-- Setting and clearing routes
+
+-- TODO duplicate
+local lntrans = { "A", "B" }
+local function sigd_to_string(sigd)
+ return minetest.pos_to_string(sigd.p).." / "..lntrans[sigd.s]
+end
+
+local 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 component
+function ilrs.set_route(signal, route, try)
+ if not try then
+ local tsuc, trsn, cbts, cblk = ilrs.set_route(signal, route, true)
+ if not tsuc then
+ return false, trsn, cbts, cblk
+ end
+ end
+
+
+ -- we start at the tc designated by signal
+ local c_sigd = signal
+ local first = true
+ local i = 1
+ local rtename = route.name
+ local signalname = ildb.get_tcbs(signal).signal_name
+ local c_tcbs, c_ts_id, c_ts, c_rseg, c_lckp
+ while c_sigd and i<=#route do
+ c_tcbs = ildb.get_tcbs(c_sigd)
+ if not c_tcbs then
+ if not try then atwarn("Did not find TCBS",c_sigd,"while setting route",rtename,"of",signal) end
+ return false, "No TCB found at "..sigd_to_string(c_sigd)..". Please reconfigure route!"
+ end
+ c_ts_id = c_tcbs.ts_id
+ if not c_ts_id then
+ if not try then atwarn("Encountered End-Of-Interlocking while setting route",rtename,"of",signal) end
+ return 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 c_ts.route then
+ if not try then atwarn("Encountered ts lock during a real run of routesetting routine, at ts=",c_ts_id,"while setting route",rtename,"of",signal) end
+ return false, "Section '"..c_ts.name.."' already has route set from "..sigd_to_string(c_ts.route.origin)..":\n"..c_ts.route.rsn, c_ts_id, nil
+ end
+ if c_ts.trains and #c_ts.trains>0 then
+ if not try then atwarn("Encountered ts occupied during a real run of routesetting routine, at ts=",c_ts_id,"while setting route",rtename,"of",signal) end
+ return false, "Section '"..c_ts.name.."' is occupied!", c_ts_id, nil
+ end
+
+ for pts, state in pairs(c_rseg.locks) do
+ local confl = ilrs.has_route_lock(pts, state)
+
+ local pos = minetest.string_to_pos(pts)
+ if advtrains.is_passive(pos) then
+ local cstate = advtrains.getstate(pos)
+ if cstate ~= state then
+ local confl = ilrs.has_route_lock(pts)
+ if confl then
+ if not try then atwarn("Encountered route lock while a real run of routesetting routine, at position",pts,"while setting route",rtename,"of",signal) end
+ return false, "Lock conflict at "..pts..", Held locked by:\n"..confl, nil, pts
+ elseif not try then
+ advtrains.setstate(pos, state)
+ end
+ end
+ if not try then
+ ilrs.add_route_lock(pts, c_ts_id, "Route '"..rtename.."' from signal '"..signalname.."'", signal)
+ c_lckp[#c_lckp+1] = pts
+ end
+ else
+ if not try then atwarn("Encountered route lock misconfiguration (no passive component) while a real run of routesetting routine, at position",pts,"while setting route",rtename,"of",signal) end
+ return false, "No passive component at "..pts..". Please reconfigure route!"
+ end
+ end
+ -- reserve ts and write locks
+ if not try then
+ local nvar = c_rseg.next
+ if 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 = nil
+ end
+ 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.aspect = route.aspect or advtrains.interlocking.GENERIC_FREE
+ c_tcbs.route_origin = signal
+ advtrains.interlocking.update_signal_aspect(c_tcbs)
+ end
+ end
+ -- advance
+ first = nil
+ c_sigd = c_rseg.next
+ i = i + 1
+ end
+
+ return true
+end
+
+-- Checks whether there is a route lock that prohibits setting the component
+-- to the wanted state. returns string with reasons on conflict
+function ilrs.has_route_lock(pts)
+ -- look this up
+ local e = ilrs.rte_locks[pts]
+ if not e then return nil
+ elseif #e==0 then
+ ilrs.rte_locks[pts] = nil
+ return nil
+ end
+ local txts = {}
+ for _, ent in ipairs(e) do
+ txts[#txts+1] = ent.rsn
+ end
+ return table.concat(txts, "\n")
+end
+
+-- adds route lock for position
+function 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 }
+ else
+ table.insert(ilrs.rte_locks[pts], elm)
+ end
+end
+
+-- adds route lock for position
+function ilrs.add_manual_route_lock(pts, rsn)
+ local elm = {rsn=rsn}
+ if not ilrs.rte_locks[pts] then
+ ilrs.rte_locks[pts] = { elm }
+ else
+ table.insert(ilrs.rte_locks[pts], elm)
+ end
+end
+
+-- frees route locking for all points (components) that were set by this ts
+function ilrs.free_route_locks(ts, lcks, nocallbacks)
+ for _,pts in pairs(lcks) do
+ ilrs.free_route_locks_indiv(pts, ts, nocallbacks)
+ end
+end
+
+function ilrs.free_route_locks_indiv(pts, ts, nocallbacks)
+ local e = ilrs.rte_locks[pts]
+ if not e then return nil
+ elseif #e==0 then
+ ilrs.rte_locks[pts] = nil
+ return nil
+ end
+ local i = 1
+ while i <= #e do
+ if 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 + 1
+ end
+ end
+ -- 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, minetest.string_to_pos(pts))
+ end
+end
+-- frees all route locks, even manual ones set with the tool, at a specific position
+function 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)
+ end
+end
+
+
+-- 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's
+function ilrs.cancel_route_from(sigd)
+ -- we start at the tc designated by signal
+ local 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 then
+ atwarn("Failed to cancel route, no TCBS at",c_sigd)
+ return false
+ end
+
+ --atdebug("cancelling",c_ts.route.rsn)
+ -- clear signal aspect and routesetting state
+ c_tcbs.route_committed = nil
+ c_tcbs.aspect = nil
+ c_tcbs.routeset = nil
+ c_tcbs.route_auto = nil
+ c_tcbs.route_origin = nil
+
+ advtrains.interlocking.update_signal_aspect(c_tcbs)
+
+ c_ts_id = c_tcbs.ts_id
+ if not c_tcbs then
+ atwarn("Failed to cancel route, end of interlocking at",c_sigd)
+ return false
+ end
+ c_ts = ildb.get_ts(c_ts_id)
+
+ if not c_ts
+ or not c_ts.route
+ or not sigd_equal(c_ts.route.entry, c_sigd) then
+ --atdebug("cancel_route_from: abort (eoi/no route):")
+ return false
+ end
+
+ c_ts.route = nil
+
+ if 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.next
+ else
+ c_sigd = nil
+ end
+ 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 true
+end
+
+-- 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). 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 = false
+ if tcbs.route_origin and not sigd_equal(tcbs.route_origin, sigd) then
+ --atdebug("Signal not in control, held by",tcbs.signal_name)
+ return
+ end
+ if (newrte and tcbs.routeset and tcbs.routeset ~= newrte) or cancel then
+ if tcbs.route_committed then
+ --atdebug("Cancelling:",tcbs.routeset)
+ advtrains.interlocking.route.cancel_route_from(sigd)
+ end
+ tcbs.route_committed = nil
+ tcbs.aspect = nil
+ has_changed_aspect = true
+ tcbs.routeset = nil
+ tcbs.route_auto = nil
+ tcbs.route_rsn = nil
+ end
+ if newrte or tcbs.routeset then
+ if tcbs.route_committed then
+ return
+ end
+ if newrte then tcbs.routeset = newrte end
+ --atdebug("Setting:",tcbs.routeset)
+ local succ, rsn, cbts, cblk = ilrs.set_route(sigd, tcbs.routes[tcbs.routeset])
+ if not succ then
+ tcbs.route_rsn = rsn
+ --atdebug("Routesetting failed:",rsn)
+ -- add cbts or cblk to callback table
+ if 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)
+ end
+ if 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)
+ end
+ else
+ --atdebug("Committed Route:",tcbs.routeset)
+ has_changed_aspect = true
+ end
+ end
+ if 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.update_signal_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 respectively
+function ilrs.update_waiting(sys, key)
+ --atdebug("update_waiting:",sys,".",key)
+ local t = ilrs.rte_callbacks[sys][key]
+ ilrs.rte_callbacks[sys][key] = nil
+ if t then
+ for _,sigd in ipairs(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)
+ end
+ end
+ end
+end
+
+advtrains.interlocking.route = ilrs
+
diff --git a/advtrains_interlocking/settingtypes.txt b/advtrains_interlocking/settingtypes.txt
new file mode 100644
index 0000000..f1c22b0
--- /dev/null
+++ b/advtrains_interlocking/settingtypes.txt
@@ -0,0 +1,4 @@
+# Stop trains forcibly in front of signal when about to run over an LZB 0 restriction, instead of setting emergency halt for manual resolving
+# This prevents the need to manually restart trains that overran red signals, but is unrealistic.
+# This is a workaround to circumvent system breakages due to bugs in LZB braking curves
+at_il_force_lzb_halt (Force LZB Halt) bool true
diff --git a/advtrains_interlocking/signal_api.lua b/advtrains_interlocking/signal_api.lua
new file mode 100644
index 0000000..a44eda6
--- /dev/null
+++ b/advtrains_interlocking/signal_api.lua
@@ -0,0 +1,515 @@
+-- Signal API implementation
+
+
+--[[
+Signal aspect table:
+Note: All speeds are measured in m/s, aka the number of + signs in the HUD.
+asp = {
+ main = <int speed>,
+ -- Main signal aspect, tells state and permitted speed of next section
+ -- 0 = section is blocked
+ -- >0 = section is free, speed limit is this value
+ -- -1 = section is free, maximum speed permitted
+ -- false/nil = Signal doesn't provide main signal information, retain current speed limit.
+ shunt = <boolean>,
+ -- Whether train may proceed as shunt move, on sight
+ -- main aspect takes precedence over this
+ -- When main==0, train switches to shunt move and is restricted to speed 6
+ proceed_as_main = <boolean>,
+ -- If an approaching train is a shunt move and 'shunt' is false,
+ -- the train may proceed as a train move under the "main" aspect
+ -- if the main aspect permits it (i.e. main!=0)
+ -- If this is not set, shunt moves are NOT allowed to switch to
+ -- a train move, and must stop even if "main" would permit passing.
+ -- This is intended to be used for "Halt for shunt moves" signs.
+
+ dst = <int speed>,
+ -- Distant signal aspect, tells state and permitted speed of the section after next section
+ -- The character of these information is purely informational
+ -- At this time, this field is not actively used
+ -- 0 = section is blocked
+ -- >0 = section is free, speed limit is this value
+ -- -1 = section is free, maximum speed permitted
+ -- false/nil = Signal doesn't provide distant signal information.
+
+ -- the character of call_on and dead_end is purely informative
+ call_on = <boolean>, -- Call-on route, expect train in track ahead (not implemented yet)
+ dead_end = <boolean>, -- Route ends on a dead end (e.g. bumper) (not implemented yet)
+
+ w_speed = <integer>,
+ -- "Warning speed restriction". Supposed for short-term speed
+ -- restrictions which always override any other restrictions
+ -- imposed by "speed" fields, until lifted by a value of -1
+ -- (Example: german Langsamfahrstellen-Signale)
+ }
+}
+
+== How signals actually work in here ==
+Each signal (in the advtrains universe) is some node that has at least the
+following things:
+- An "influence point" that is set somewhere on a rail
+- An aspect which trains that pass the "influence point" have to obey
+
+There can be static and dynamic signals. Static signals are, roughly
+spoken, signs, while dynamic signals are "real" signals which can display
+different things.
+
+The node definition of a signal node should contain those fields:
+groups = {
+ advtrains_signal = 2,
+ save_in_at_nodedb = 1,
+}
+advtrains = {
+ set_aspect = function(pos, node, asp)
+ -- This function gets called whenever the signal should display
+ -- a new or changed signal aspect. It is not required that
+ -- the signal actually displays the exact same aspect, since
+ -- some signals can not do this by design. However, it must
+ -- display an aspect that is at least as restrictive as the passed
+ -- aspect as far as it is capable of doing so.
+ -- Examples:
+ -- - pure shunt signals can not display a "main" aspect
+ -- and have no effect on train moves, so they will only ever
+ -- honor the shunt.free field for their aspect.
+ -- - the german Hl system can only signal speeds of 40, 60
+ -- and 100 km/h, a speed of 80km/h should then be signalled
+ -- as 60 km/h instead.
+ -- In turn, it is not guaranteed that the aspect will fulfill the
+ -- criteria put down in supported_aspects.
+ -- If set_aspect is present, supported_aspects should also be declared.
+
+ -- The aspect passed in here can always be queried using the
+ -- advtrains.interlocking.signal_get_supposed_aspect(pos) function.
+ -- It is always DANGER when the signal is not used as route signal.
+
+ -- For static signals, this function should be completely omitted
+ -- If this function is omitted, it won't be possible to use
+ -- route setting on this signal.
+ end,
+ supported_aspects = {
+ -- A table which tells which different types of aspects this signal
+ -- is able to display. It is used to construct the "aspect editing"
+ -- formspec for route programming (and others) It should always be
+ -- present alongside with set_aspect. If this is not specified but
+ -- set_aspect is, the user will be allowed to select any aspect.
+ -- Any of the fields marked with <boolean/nil> support 3 types of values:
+ nil: if this signal can switch between free/blocked
+ false: always shows "blocked", unchangable
+ true: always shows "free", unchangable
+ -- Any of the "speed" fields should contain a list of possible values
+ -- to be set as restriction. If omitted, the value of the described
+ -- field is always assumed to be false (no information)
+ -- A speed of 0 means that the signal can show a "blocked" aspect
+ -- (which is probably the case for most signals)
+ -- If the signal can signal "no information" on one of the fields
+ -- (thus false is an acceptable value), include false in the list
+ -- If your signal can only display a single speed (may it be -1),
+ -- always enclose that single value into a list. (such as {-1})
+ main = {<speed1>, ..., <speedn>} or nil,
+ dst = {<speed1>, ..., <speedn>} or nil,
+ shunt = <boolean/nil>,
+
+ call_on = <boolean/nil>,
+ dead_end = <boolean/nil>,
+ w_speed = {<speed1>, ..., <speedn>} or nil,
+
+ },
+ Example for supported_aspects:
+ supported_aspects = {
+ main = {0, 6, -1}, -- can show either "Section blocked", "Proceed at speed 6" or "Proceed at maximum speed"
+ dst = {0, false}, -- can show only if next signal shows "blocked", no other information.
+ shunt = false, -- shunting by this signal is never allowed.
+
+ call_on = false,
+ dead_end = false,
+ w_speed = nil,
+ -- none of the information can be shown by the signal
+
+ },
+
+ get_aspect = function(pos, node)
+ -- This function gets called by the train safety system. It
+ should return the aspect that this signal actually displays,
+ not preferably the input of set_aspect.
+ -- For regular, full-featured light signals, they will probably
+ honor all entries in the original aspect, however, e.g.
+ simple shunt signals always return main=false regardless of
+ the set_aspect input because they can not signal "Halt" to
+ train moves.
+ -- advtrains.interlocking.DANGER contains a default "all-danger" aspect.
+ -- If your signal does not cover certain sub-tables of the aspect,
+ the following reasonable defaults are automatically assumed:
+ main = false (unchanged)
+ dst = false (unchanged)
+ shunt = false (shunting not allowed)
+ info = {} (no further information)
+ end,
+}
+on_rightclick = advtrains.interlocking.signal_rc_handler
+can_dig = advtrains.interlocking.signal_can_dig
+after_dig_node = advtrains.interlocking.signal_after_dig
+
+(If you need to specify custom can_dig or after_dig_node callbacks,
+please call those functions anyway!)
+
+Important note: If your signal should support external ways to set its
+aspect (e.g. via mesecons), there are some things that need to be considered:
+- advtrains.interlocking.signal_get_supposed_aspect(pos) won't respect this
+- Whenever you change the signal aspect, and that aspect change
+did not happen through a call to
+advtrains.interlocking.signal_set_aspect(pos, asp), you are
+*required* to call this function:
+advtrains.interlocking.signal_on_aspect_changed(pos)
+in order to notify trains about the aspect change.
+This function will query get_aspect to retrieve the new aspect.
+
+]]--
+
+local DANGER = {
+ main = 0,
+ dst = false,
+ shunt = false,
+}
+advtrains.interlocking.DANGER = DANGER
+
+advtrains.interlocking.GENERIC_FREE = {
+ main = -1,
+ shunt = false,
+ dst = false,
+}
+
+local function convert_aspect_if_necessary(asp)
+ if type(asp.main) == "table" then
+ local newasp = {}
+ if asp.main.free then
+ newasp.main = asp.main.speed
+ else
+ newasp.main = 0
+ end
+ if asp.dst and asp.dst.free then
+ newasp.dst = asp.dst.speed
+ else
+ newasp.dst = 0
+ end
+ newasp.proceed_as_main = asp.shunt.proceed_as_main
+ newasp.shunt = asp.shunt.free
+ -- Note: info table not transferred, it's not used right now
+ return newasp
+ end
+ return asp
+end
+
+function advtrains.interlocking.update_signal_aspect(tcbs)
+ if tcbs.signal then
+ local asp = tcbs.aspect or DANGER
+ advtrains.interlocking.signal_set_aspect(tcbs.signal, asp)
+ end
+end
+
+function advtrains.interlocking.signal_can_dig(pos)
+ return not advtrains.interlocking.db.get_sigd_for_signal(pos)
+end
+
+function advtrains.interlocking.signal_after_dig(pos)
+ -- clear influence point
+ advtrains.interlocking.db.clear_ip_by_signalpos(pos)
+end
+
+function advtrains.interlocking.signal_set_aspect(pos, asp)
+ asp = convert_aspect_if_necessary(asp)
+ local node=advtrains.ndb.get_node(pos)
+ local ndef=minetest.registered_nodes[node.name]
+ if ndef and ndef.advtrains and ndef.advtrains.set_aspect then
+ ndef.advtrains.set_aspect(pos, node, asp)
+ advtrains.interlocking.signal_on_aspect_changed(pos)
+ end
+end
+
+-- should be called when aspect has changed on this signal.
+function advtrains.interlocking.signal_on_aspect_changed(pos)
+ local ipts, iconn = advtrains.interlocking.db.get_ip_by_signalpos(pos)
+ if not ipts then return end
+ local ipos = minetest.string_to_pos(ipts)
+
+ advtrains.invalidate_all_paths_ahead(ipos)
+end
+
+function advtrains.interlocking.signal_rc_handler(pos, node, player, itemstack, pointed_thing)
+ local pname = player:get_player_name()
+ local control = player:get_player_control()
+ if control.aux1 then
+ advtrains.interlocking.show_ip_form(pos, pname)
+ return
+ end
+
+ local sigd = advtrains.interlocking.db.get_sigd_for_signal(pos)
+ if sigd then
+ advtrains.interlocking.show_signalling_form(sigd, pname)
+ else
+ local ndef = minetest.registered_nodes[node.name]
+ if ndef.advtrains and ndef.advtrains.set_aspect then
+ -- permit to set aspect manually
+ local function callback(pname, aspect)
+ advtrains.interlocking.signal_set_aspect(pos, aspect)
+ end
+ local isasp = ndef.advtrains.get_aspect(pos, node)
+
+ advtrains.interlocking.show_signal_aspect_selector(
+ pname,
+ ndef.advtrains.supported_aspects,
+ "Set aspect manually", callback,
+ isasp)
+ else
+ --static signal - only IP
+ advtrains.interlocking.show_ip_form(pos, pname)
+ end
+ end
+end
+
+-- Returns the aspect the signal at pos is supposed to show
+function advtrains.interlocking.signal_get_supposed_aspect(pos)
+ local sigd = advtrains.interlocking.db.get_sigd_for_signal(pos)
+ if sigd then
+ local tcbs = advtrains.interlocking.db.get_tcbs(sigd)
+ if tcbs.aspect then
+ return convert_aspect_if_necessary(tcbs.aspect)
+ end
+ end
+ return DANGER;
+end
+
+-- Returns the actual aspect of the signal at position, as returned by the nodedef.
+-- returns nil when there's no signal at the position
+function advtrains.interlocking.signal_get_aspect(pos)
+ local node=advtrains.ndb.get_node(pos)
+ local ndef=minetest.registered_nodes[node.name]
+ if ndef and ndef.advtrains and ndef.advtrains.get_aspect then
+ local asp = ndef.advtrains.get_aspect(pos, node)
+ if not asp then asp = DANGER end
+ return convert_aspect_if_necessary(asp)
+ end
+ return nil
+end
+
+-- Returns the "supported_aspects" of the signal at position, as returned by the nodedef.
+-- returns nil when there's no signal at the position
+function advtrains.interlocking.signal_get_supported_aspects(pos)
+ local node=advtrains.ndb.get_node(pos)
+ local ndef=minetest.registered_nodes[node.name]
+ if ndef and ndef.advtrains and ndef.advtrains.supported_aspects then
+ local asp = ndef.advtrains.supported_aspects
+ return asp
+ end
+ return nil
+end
+
+local players_assign_ip = {}
+
+local function ipmarker(ipos, connid)
+ local node_ok, conns, rhe = advtrains.get_rail_info_at(ipos, advtrains.all_tracktypes)
+ if not node_ok then return end
+ local yaw = advtrains.dir_to_angle(conns[connid].c)
+
+ -- using tcbmarker here
+ local obj = minetest.add_entity(vector.add(ipos, {x=0, y=0.2, z=0}), "advtrains_interlocking:tcbmarker")
+ if not obj then return end
+ obj:set_yaw(yaw)
+ obj:set_properties({
+ textures = { "at_il_signal_ip.png" },
+ })
+end
+
+-- shows small info form for signal IP state/assignment
+-- only_notset: show only if it is not set yet (used by signal tcb assignment)
+function advtrains.interlocking.show_ip_form(pos, pname, only_notset)
+ if not minetest.check_player_privs(pname, "interlocking") then
+ return
+ end
+ local form = "size[7,5]label[0.5,0.5;Signal at "..minetest.pos_to_string(pos).."]"
+ local pts, connid = advtrains.interlocking.db.get_ip_by_signalpos(pos)
+ if pts then
+ form = form.."label[0.5,1.5;Influence point is set at "..pts.."/"..connid.."]"
+ form = form.."button_exit[0.5,2.5; 5,1;set;Move]"
+ form = form.."button_exit[0.5,3.5; 5,1;clear;Clear]"
+ local ipos = minetest.string_to_pos(pts)
+ ipmarker(ipos, connid)
+ else
+ form = form.."label[0.5,1.5;Influence point is not set.]"
+ form = form.."label[0.5,2.0;It is recommended to set an influence point.]"
+ form = form.."label[0.5,2.5;This is the point where trains will obey the signal.]"
+
+ form = form.."button_exit[0.5,3.5; 5,1;set;Set]"
+ end
+ if not only_notset or not pts then
+ minetest.show_formspec(pname, "at_il_ipassign_"..minetest.pos_to_string(pos), form)
+ end
+end
+
+minetest.register_on_player_receive_fields(function(player, formname, fields)
+ local pname = player:get_player_name()
+ if not minetest.check_player_privs(pname, {train_operator=true, interlocking=true}) then
+ return
+ end
+ local pts = string.match(formname, "^at_il_ipassign_([^_]+)$")
+ local pos
+ if pts then
+ pos = minetest.string_to_pos(pts)
+ end
+ if pos then
+ if fields.set then
+ advtrains.interlocking.signal_init_ip_assign(pos, pname)
+ elseif fields.clear then
+ advtrains.interlocking.db.clear_ip_by_signalpos(pos)
+ end
+ end
+end)
+
+-- inits the signal IP assignment process
+function advtrains.interlocking.signal_init_ip_assign(pos, pname)
+ if not minetest.check_player_privs(pname, "interlocking") then
+ minetest.chat_send_player(pname, "Insufficient privileges to use this!")
+ return
+ end
+ --remove old IP
+ --advtrains.interlocking.db.clear_ip_by_signalpos(pos)
+ minetest.chat_send_player(pname, "Configuring Signal: Please look in train's driving direction and punch rail to set influence point.")
+
+ players_assign_ip[pname] = pos
+end
+
+minetest.register_on_punchnode(function(pos, node, player, pointed_thing)
+ local pname = player:get_player_name()
+ if not minetest.check_player_privs(pname, "interlocking") then
+ return
+ end
+ -- IP assignment
+ local signalpos = players_assign_ip[pname]
+ if signalpos then
+ if vector.distance(pos, signalpos)<=50 then
+ local node_ok, conns, rhe = advtrains.get_rail_info_at(pos, advtrains.all_tracktypes)
+ if node_ok and #conns == 2 then
+
+ local yaw = player:get_look_horizontal()
+ local plconnid = advtrains.yawToClosestConn(yaw, conns)
+
+ -- add assignment if not already present.
+ local pts = advtrains.roundfloorpts(pos)
+ if not advtrains.interlocking.db.get_ip_signal_asp(pts, plconnid) then
+ advtrains.interlocking.db.set_ip_signal(pts, plconnid, signalpos)
+ ipmarker(pos, plconnid)
+ minetest.chat_send_player(pname, "Configuring Signal: Successfully set influence point")
+ else
+ minetest.chat_send_player(pname, "Configuring Signal: Influence point of another signal is already present!")
+ end
+ else
+ minetest.chat_send_player(pname, "Configuring Signal: This is not a normal two-connection rail! Aborted.")
+ end
+ else
+ minetest.chat_send_player(pname, "Configuring Signal: Node is too far away. Aborted.")
+ end
+ players_assign_ip[pname] = nil
+ end
+end)
+
+
+--== aspect selector ==--
+
+local players_aspsel = {}
+
+--[[
+suppasp: "supported_aspects" table
+purpose: form title string
+callback: func(pname, aspect) called on form submit
+isasp: aspect currently set
+]]
+function advtrains.interlocking.show_signal_aspect_selector(pname, p_suppasp, p_purpose, callback, isasp)
+ local suppasp = p_suppasp or {
+ main = {0, -1}, dst = {false}, shunt = false, info = {},
+ }
+ local purpose = p_purpose or ""
+
+ local form = "size[7,5]label[0.5,0.5;Select Signal Aspect:]"
+ form = form.."label[0.5,1;"..purpose.."]"
+
+ form = form.."label[0.5,1.5;== Main Signal ==]"
+ local selid = 1
+ local entries = {}
+ for idx, spv in ipairs(suppasp.main) do
+ local entry
+ if spv == 0 then
+ entry = "Halt"
+ elseif spv == -1 then
+ entry = "Continue at maximum speed"
+ elseif not spv then
+ entry = "Continue\\, speed limit unchanged (no info)"
+ else
+ entry = "Continue at speed of "..spv
+ end
+ -- hack: the crappy formspec system returns the label, not the index. save the index in it.
+ entries[idx] = idx.."| "..entry
+ if isasp and spv == (isasp.main or false) then
+ selid = idx
+ end
+ end
+ form = form.."dropdown[0.5,2;6;main;"..table.concat(entries, ",")..";"..selid.."]"
+
+
+ form = form.."label[0.5,3;== Shunting ==]"
+ if suppasp.shunt == nil then
+ local st = 1
+ if isasp and isasp.shunt then st=2 end
+ form = form.."dropdown[0.5,3.5;6;shunt_free;---,allowed;"..st.."]"
+ end
+
+ form = form.."button_exit[0.5,4.5; 5,1;save;OK]"
+
+ local token = advtrains.random_id()
+
+ minetest.show_formspec(pname, "at_il_sigaspdia_"..token, form)
+
+ minetest.after(1, function()
+ players_aspsel[pname] = {
+ suppasp = suppasp,
+ callback = callback,
+ token = token,
+ }
+ end)
+end
+
+local function usebool(sup, val, free)
+ if sup == nil then
+ return val==free
+ else
+ return sup
+ end
+end
+
+-- other side of hack: extract the index
+local function ddindex(val)
+ return tonumber(string.match(val, "^(%d+)|"))
+end
+
+-- TODO use non-hacky way to parse outputs
+
+minetest.register_on_player_receive_fields(function(player, formname, fields)
+ local pname = player:get_player_name()
+ local psl = players_aspsel[pname]
+ if psl then
+ if formname == "at_il_sigaspdia_"..psl.token then
+ if fields.save then
+ local maini = ddindex(fields.main)
+ if not maini then return end
+ local asp = {
+ main = psl.suppasp.main[maini],
+ dst = false,
+ shunt = usebool(psl.suppasp.shunt, fields.shunt_free, "allowed"),
+ info = {}
+ }
+ psl.callback(pname, asp)
+ end
+ else
+ players_aspsel[pname] = nil
+ end
+ end
+
+end)
diff --git a/advtrains_interlocking/tcb_ts_ui.lua b/advtrains_interlocking/tcb_ts_ui.lua
new file mode 100755
index 0000000..34fbf7f
--- /dev/null
+++ b/advtrains_interlocking/tcb_ts_ui.lua
@@ -0,0 +1,830 @@
+-- Track Circuit Breaks and Track Sections - Player interaction
+
+local players_assign_tcb = {}
+local players_assign_signal = {}
+local players_link_ts = {}
+
+local ildb = advtrains.interlocking.db
+local ilrs = advtrains.interlocking.route
+
+local sigd_equal = advtrains.interlocking.sigd_equal
+
+local lntrans = { "A", "B" }
+
+local function sigd_to_string(sigd)
+ return minetest.pos_to_string(sigd.p).." / "..lntrans[sigd.s]
+end
+
+minetest.register_node("advtrains_interlocking:tcb_node", {
+ drawtype = "mesh",
+ paramtype="light",
+ paramtype2="facedir",
+ walkable = false,
+ selection_box = {
+ type = "fixed",
+ fixed = {-1/6, -1/2, -1/6, 1/6, 1/4, 1/6},
+ },
+ mesh = "at_il_tcb_node.obj",
+ tiles = {"at_il_tcb_node.png"},
+ description="Track Circuit Break",
+ sunlight_propagates=true,
+ groups = {
+ cracky=3,
+ not_blocking_trains=1,
+ --save_in_at_nodedb=2,
+ at_il_track_circuit_break = 1,
+ },
+ after_place_node = function(pos, node, player)
+ local meta = minetest.get_meta(pos)
+ meta:set_string("infotext", "Unconfigured Track Circuit Break, right-click to assign.")
+ end,
+ on_rightclick = function(pos, node, player)
+ local pname = player:get_player_name()
+ if not minetest.check_player_privs(pname, "interlocking") then
+ minetest.chat_send_player(pname, "Insufficient privileges to use this!")
+ return
+ end
+
+ local meta = minetest.get_meta(pos)
+ local tcbpts = meta:get_string("tcb_pos")
+ if tcbpts ~= "" then
+ local tcbpos = minetest.string_to_pos(tcbpts)
+ local tcb = ildb.get_tcb(tcbpos)
+ if tcb then
+ advtrains.interlocking.show_tcb_form(tcbpos, pname)
+ else
+ minetest.chat_send_player(pname, "This TCB has been removed. Please dig marker.")
+ end
+ else
+ --unconfigured
+ minetest.chat_send_player(pname, "Configuring TCB: Please punch the rail you want to assign this TCB to.")
+
+ players_assign_tcb[pname] = pos
+ end
+ end,
+ --on_punch = function(pos, node, player)
+ -- local meta = minetest.get_meta(pos)
+ -- local tcbpts = meta:get_string("tcb_pos")
+ -- if tcbpts ~= "" then
+ -- local tcbpos = minetest.string_to_pos(tcbpts)
+ -- advtrains.interlocking.show_tcb_marker(tcbpos)
+ -- end
+ --end,
+ can_dig = function(pos, player)
+ if player == nil then return false end
+
+ local pname = player:get_player_name()
+
+ -- Those markers can only be dug when all adjacent TS's are set
+ -- as EOI.
+ local meta = minetest.get_meta(pos)
+ local tcbpts = meta:get_string("tcb_pos")
+ if tcbpts ~= "" then
+ if not minetest.check_player_privs(pname, "interlocking") then
+ minetest.chat_send_player(pname, "Insufficient privileges to use this!")
+ return
+ end
+ local tcbpos = minetest.string_to_pos(tcbpts)
+ local tcb = ildb.get_tcb(tcbpos)
+ if not tcb then return true end
+ for connid=1,2 do
+ if tcb[connid].ts_id or tcb[connid].signal then
+ minetest.chat_send_player(pname, "Can't remove TCB: Both sides must have no track section and no signal assigned!")
+ return false
+ end
+ if not ildb.may_modify_tcbs(tcb[connid]) then
+ minetest.chat_send_player(pname, "Can't remove TCB: Side "..connid.." forbids modification (shouldn't happen).")
+ return false
+ end
+ end
+ end
+ return true
+ end,
+ after_dig_node = function(pos, oldnode, oldmetadata, player)
+ if not oldmetadata or not oldmetadata.fields then return end
+ local tcbpts = oldmetadata.fields.tcb_pos
+ if tcbpts and tcbpts ~= "" then
+ local tcbpos = minetest.string_to_pos(tcbpts)
+ local success = ildb.remove_tcb(tcbpos)
+ if success and player then
+ minetest.chat_send_player(player:get_player_name(), "TCB has been removed.")
+ else
+ minetest.chat_send_player(player:get_player_name(), "Failed to remove TCB!")
+ minetest.set_node(pos, oldnode)
+ local meta = minetest.get_meta(pos)
+ meta:set_string("tcb_pos", minetest.pos_to_string(tcbpos))
+ end
+ end
+ end,
+})
+
+
+-- Crafting
+
+-- set some fallbacks
+local tcb_core = "default:mese_crystal"
+local tcb_secondary = "default:mese_crystal_fragment"
+
+--alternative recipe items
+--core
+if minetest.get_modpath("basic_materials") then
+ tcb_core = "basic_materials:ic"
+elseif minetest.get_modpath("technic") then
+ tcb_core = "technic:control_logic_unit"
+end
+--print("TCB Core: "..tcb_core)
+--secondary
+if minetest.get_modpath("mesecons") then
+ tcb_secondary = 'mesecons:wire_00000000_off'
+end
+--print("TCB Secondary: "..tcb_secondary)
+
+minetest.register_craft({
+ output = 'advtrains_interlocking:tcb_node 4',
+ recipe = {
+ {tcb_secondary,tcb_core,tcb_secondary},
+ {'advtrains:dtrack_placer','','advtrains:dtrack_placer'}
+ },
+ --actually use track in the tcb recipe
+ replacements = {
+ {"advtrains:dtrack_placer","advtrains:dtrack_placer"},
+ {"advtrains:dtrack_placer","advtrains:dtrack_placer"},
+ }
+})
+
+--nil the temp crafting variables
+tcb_core= nil
+tcb_secondary = nil
+
+minetest.register_on_punchnode(function(pos, node, player, pointed_thing)
+ local pname = player:get_player_name()
+ if not minetest.check_player_privs(pname, "interlocking") then
+ return
+ end
+ -- TCB assignment
+ local tcbnpos = players_assign_tcb[pname]
+ if tcbnpos then
+ if vector.distance(pos, tcbnpos)<=20 then
+ local node_ok, conns, rhe = advtrains.get_rail_info_at(pos, advtrains.all_tracktypes)
+ if node_ok and #conns == 2 then
+ local ok = ildb.create_tcb(pos)
+
+ if not ok then
+ minetest.chat_send_player(pname, "Configuring TCB: TCB already exists at this position! It has now been re-assigned.")
+ end
+
+ ildb.sync_tcb_neighbors(pos, 1)
+ ildb.sync_tcb_neighbors(pos, 2)
+
+ local meta = minetest.get_meta(tcbnpos)
+ meta:set_string("tcb_pos", minetest.pos_to_string(pos))
+ meta:set_string("infotext", "TCB assigned to "..minetest.pos_to_string(pos))
+ minetest.chat_send_player(pname, "Configuring TCB: Successfully configured TCB")
+ else
+ minetest.chat_send_player(pname, "Configuring TCB: This is not a normal two-connection rail! Aborted.")
+ end
+ else
+ minetest.chat_send_player(pname, "Configuring TCB: Node is too far away. Aborted.")
+ end
+ players_assign_tcb[pname] = nil
+ end
+
+ -- Signal assignment
+ local sigd = players_assign_signal[pname]
+ if sigd then
+ if vector.distance(pos, sigd.p)<=50 then
+ local is_signal = minetest.get_item_group(node.name, "advtrains_signal") >= 2
+ if is_signal then
+ local ndef = minetest.registered_nodes[node.name]
+ if ndef and ndef.advtrains and ndef.advtrains.set_aspect then
+ local tcbs = ildb.get_tcbs(sigd)
+ if tcbs then
+ tcbs.signal = pos
+ if not tcbs.signal_name then
+ tcbs.signal_name = "Signal at "..minetest.pos_to_string(sigd.p)
+ end
+ if not tcbs.routes then
+ tcbs.routes = {}
+ end
+ ildb.set_sigd_for_signal(pos, sigd)
+ minetest.chat_send_player(pname, "Configuring TCB: Successfully assigned signal.")
+ advtrains.interlocking.show_ip_form(pos, pname, true)
+ else
+ minetest.chat_send_player(pname, "Configuring TCB: Internal error, TCBS doesn't exist. Aborted.")
+ end
+ else
+ minetest.chat_send_player(pname, "Configuring TCB: Cannot use static signals for routesetting. Aborted.")
+ end
+ else
+ minetest.chat_send_player(pname, "Configuring TCB: Not a compatible signal. Aborted.")
+ end
+ else
+ minetest.chat_send_player(pname, "Configuring TCB: Node is too far away. Aborted.")
+ end
+ players_assign_signal[pname] = nil
+ end
+end)
+
+-- TCB Form
+
+local function mktcbformspec(tcbs, btnpref, offset, pname)
+ local form = ""
+ local ts
+ if tcbs.ts_id then
+ ts = ildb.get_ts(tcbs.ts_id)
+ end
+ if ts then
+ form = form.."label[0.5,"..offset..";Side "..btnpref..": "..minetest.formspec_escape(ts.name).."]"
+ form = form.."button[0.5,"..(offset+0.5)..";5,1;"..btnpref.."_gotots;Show track section]"
+ if ildb.may_modify_tcbs(tcbs) then
+ -- Note: the security check to prohibit those actions is located in database.lua in the corresponding functions.
+ form = form.."button[0.5,"..(offset+1.5)..";2.5,1;"..btnpref.."_update;Update near TCBs]"
+ form = form.."button[3 ,"..(offset+1.5)..";2.5,1;"..btnpref.."_remove;Remove from section]"
+ end
+ else
+ tcbs.ts_id = nil
+ form = form.."label[0.5,"..offset..";Side "..btnpref..": ".."End of interlocking]"
+ form = form.."button[0.5,"..(offset+0.5)..";5,1;"..btnpref.."_makeil;Create Interlocked Track Section]"
+ --if tcbs.section_free then
+ --form = form.."button[0.5,"..(offset+1.5)..";5,1;"..btnpref.."_setlocked;Section is free]"
+ --else
+ --form = form.."button[0.5,"..(offset+1.5)..";5,1;"..btnpref.."_setfree;Section is blocked]"
+ --end
+ end
+ if tcbs.signal then
+ form = form.."button[0.5,"..(offset+2.5)..";5,1;"..btnpref.."_sigdia;Signalling]"
+ else
+ form = form.."button[0.5,"..(offset+2.5)..";5,1;"..btnpref.."_asnsig;Assign a signal]"
+ end
+ return form
+end
+
+
+function advtrains.interlocking.show_tcb_form(pos, pname)
+ if not minetest.check_player_privs(pname, "interlocking") then
+ minetest.chat_send_player(pname, "Insufficient privileges to use this!")
+ return
+ end
+ local tcb = ildb.get_tcb(pos)
+ if not tcb then return end
+
+ local form = "size[6,9] label[0.5,0.5;Track Circuit Break Configuration]"
+ form = form .. mktcbformspec(tcb[1], "A", 1, pname)
+ form = form .. mktcbformspec(tcb[2], "B", 5, pname)
+
+ minetest.show_formspec(pname, "at_il_tcbconfig_"..minetest.pos_to_string(pos), form)
+ advtrains.interlocking.show_tcb_marker(pos)
+end
+
+--helper: length of nil table is 0
+local function nlen(t)
+ if not t then return 0 end
+ return #t
+end
+
+
+minetest.register_on_player_receive_fields(function(player, formname, fields)
+ local pname = player:get_player_name()
+ if not minetest.check_player_privs(pname, "interlocking") then
+ return
+ end
+ local pts = string.match(formname, "^at_il_tcbconfig_(.+)$")
+ local pos
+ if pts then
+ pos = minetest.string_to_pos(pts)
+ end
+ if pos and not fields.quit then
+ local tcb = ildb.get_tcb(pos)
+ if not tcb then return end
+ local f_gotots = {fields.A_gotots, fields.B_gotots}
+ local f_update = {fields.A_update, fields.B_update}
+ local f_remove = {fields.A_remove, fields.B_remove}
+ local f_makeil = {fields.A_makeil, fields.B_makeil}
+ local f_setlocked = {fields.A_setlocked, fields.B_setlocked}
+ local f_setfree = {fields.A_setfree, fields.B_setfree}
+ local f_asnsig = {fields.A_asnsig, fields.B_asnsig}
+ local f_sigdia = {fields.A_sigdia, fields.B_sigdia}
+
+ for connid=1,2 do
+ local tcbs = tcb[connid]
+ if tcbs.ts_id then
+ if f_gotots[connid] then
+ advtrains.interlocking.show_ts_form(tcbs.ts_id, pname)
+ return
+ end
+ if f_update[connid] then
+ ildb.sync_tcb_neighbors(pos, connid)
+ end
+ if f_remove[connid] then
+ ildb.remove_from_interlocking({p=pos, s=connid})
+ end
+ else
+ if f_makeil[connid] then
+ -- try sinc_tcb_neighbors first
+ ildb.sync_tcb_neighbors(pos, connid)
+ -- if that didn't work, create new section
+ if not tcbs.ts_id then
+ ildb.create_ts({p=pos, s=connid})
+ ildb.sync_tcb_neighbors(pos, connid)
+ end
+ end
+ -- non-interlocked
+ if f_setfree[connid] then
+ tcbs.section_free = true
+ end
+ if f_setlocked[connid] then
+ tcbs.section_free = nil
+ end
+ end
+ if f_asnsig[connid] and not tcbs.signal then
+ minetest.chat_send_player(pname, "Configuring TCB: Please punch the signal to assign.")
+ players_assign_signal[pname] = {p=pos, s=connid}
+ minetest.close_formspec(pname, formname)
+ return
+ end
+ if f_sigdia[connid] and tcbs.signal then
+ advtrains.interlocking.show_signalling_form({p=pos, s=connid}, pname)
+ return
+ end
+
+ end
+ advtrains.interlocking.show_tcb_form(pos, pname)
+ end
+
+end)
+
+
+
+-- TS Formspec
+
+-- textlist selection temporary storage
+local ts_pselidx = {}
+
+function advtrains.interlocking.show_ts_form(ts_id, pname, sel_tcb)
+ if not minetest.check_player_privs(pname, "interlocking") then
+ minetest.chat_send_player(pname, "Insufficient privileges to use this!")
+ return
+ end
+ local ts = ildb.get_ts(ts_id)
+ if not ts_id then return end
+
+ local form = "size[10,10]label[0.5,0.5;Track Section Detail - "..ts_id.."]"
+ form = form.."field[0.8,2;5.2,1;name;Section name;"..minetest.formspec_escape(ts.name).."]"
+ form = form.."button[5.5,1.7;1,1;setname;Set]"
+ local hint
+
+ local strtab = {}
+ for idx, sigd in ipairs(ts.tc_breaks) do
+ strtab[#strtab+1] = minetest.formspec_escape(sigd_to_string(sigd))
+ advtrains.interlocking.show_tcb_marker(sigd.p)
+ end
+
+ form = form.."textlist[0.5,3;5,3;tcblist;"..table.concat(strtab, ",").."]"
+
+ if ildb.may_modify_ts(ts) then
+
+ if players_link_ts[pname] then
+ local other_id = players_link_ts[pname]
+ local other_ts = ildb.get_ts(other_id)
+ if other_ts then
+ if ildb.may_modify_ts(other_ts) then
+ form = form.."button[5.5,3;3.5,1;mklink;Join with "..minetest.formspec_escape(other_ts.name).."]"
+ form = form.."button[9 ,3;0.5,1;cancellink;X]"
+ end
+ end
+ else
+ form = form.."button[5.5,3;4,1;link;Join into other section]"
+ hint = 1
+ end
+ form = form.."button[5.5,4;4,1;dissolve;Dissolve Section]"
+ form = form.."tooltip[dissolve;This will remove the track section and set all its end points to End Of Interlocking]"
+ if sel_tcb then
+ form = form.."button[5.5,5;4,1;del_tcb;Unlink selected TCB]"
+ hint = 2
+ end
+ else
+ hint=3
+ end
+
+ if ts.route then
+ form = form.."label[0.5,6.1;Route is set: "..ts.route.rsn.."]"
+ elseif ts.route_post then
+ form = form.."label[0.5,6.1;Section holds "..#(ts.route_post.lcks or {}).." route locks.]"
+ end
+ -- occupying trains
+ if ts.trains and #ts.trains>0 then
+ form = form.."label[0.5,7.1;Trains on this section:]"
+ form = form.."textlist[0.5,7.7;3,2;trnlist;"..table.concat(ts.trains, ",").."]"
+ else
+ form = form.."label[0.5,7.1;No trains on this section.]"
+ end
+
+ form = form.."button[5.5,7;4,1;reset;Reset section state]"
+
+ if hint == 1 then
+ form = form.."label[0.5,0.75;Use the 'Join' button to designate rail crosses and link not listed far-away TCBs]"
+ elseif hint == 2 then
+ form = form.."label[0.5,0.75;Unlinking a TCB will set it to non-interlocked mode.]"
+ elseif hint == 3 then
+ form = form.."label[0.5,0.75;You cannot modify track sections when a route is set or a train is on the section.]"
+ --form = form.."label[0.5,1;Trying to unlink a TCB directly connected to this track will not work.]"
+ end
+
+ ts_pselidx[pname]=sel_tcb
+ minetest.show_formspec(pname, "at_il_tsconfig_"..ts_id, form)
+
+end
+
+
+minetest.register_on_player_receive_fields(function(player, formname, fields)
+ local pname = player:get_player_name()
+ if not minetest.check_player_privs(pname, "interlocking") then
+ return
+ end
+ -- independent of the formspec, clear this whenever some formspec event happens
+ local tpsi = ts_pselidx[pname]
+ ts_pselidx[pname] = nil
+
+ local ts_id = string.match(formname, "^at_il_tsconfig_(.+)$")
+ if ts_id and not fields.quit then
+ local ts = ildb.get_ts(ts_id)
+ if not ts then return end
+
+ local sel_tcb
+ if fields.tcblist then
+ local tev = minetest.explode_textlist_event(fields.tcblist)
+ sel_tcb = tev.index
+ ts_pselidx[pname] = sel_tcb
+ elseif tpsi then
+ sel_tcb = tpsi
+ end
+
+ if ildb.may_modify_ts(ts) then
+ if players_link_ts[pname] then
+ if fields.cancellink then
+ players_link_ts[pname] = nil
+ elseif fields.mklink then
+ ildb.link_track_sections(players_link_ts[pname], ts_id)
+ players_link_ts[pname] = nil
+ end
+ end
+
+ if fields.del_tcb and sel_tcb and sel_tcb > 0 and sel_tcb <= #ts.tc_breaks then
+ if not ildb.remove_from_interlocking(ts.tc_breaks[sel_tcb]) then
+ minetest.chat_send_player(pname, "Please unassign signal first!")
+ end
+ sel_tcb = nil
+ end
+
+ if fields.link then
+ players_link_ts[pname] = ts_id
+ end
+ if fields.dissolve then
+ ildb.dissolve_ts(ts_id)
+ minetest.close_formspec(pname, formname)
+ return
+ end
+ end
+
+ if fields.setname then
+ ts.name = fields.name
+ if ts.name == "" then
+ ts.name = "Section "..ts_id
+ end
+ end
+
+ if fields.reset then
+ -- User requested resetting the section
+ -- Show him what this means...
+ local form = "size[7,5]label[0.5,0.5;Reset track section]"
+ form = form.."label[0.5,1;This will clear the list of trains\nand the routesetting status of this section.\nAre you sure?]"
+ form = form.."button_exit[0.5,2.5; 5,1;reset;Yes]"
+ form = form.."button_exit[0.5,3.5; 5,1;cancel;Cancel]"
+ minetest.show_formspec(pname, "at_il_tsreset_"..ts_id, form)
+ return
+ end
+
+ advtrains.interlocking.show_ts_form(ts_id, pname, sel_tcb)
+ return
+ end
+
+ ts_id = string.match(formname, "^at_il_tsreset_(.+)$")
+ if ts_id and fields.reset then
+ local ts = ildb.get_ts(ts_id)
+ if not ts then return end
+ ts.trains = {}
+ if ts.route_post then
+ advtrains.interlocking.route.free_route_locks(ts_id, ts.route_post.locks)
+ end
+ ts.route_post = nil
+ ts.route = nil
+ for _, sigd in ipairs(ts.tc_breaks) do
+ local tcbs = ildb.get_tcbs(sigd)
+ advtrains.interlocking.update_signal_aspect(tcbs)
+ end
+ minetest.chat_send_player(pname, "Reset track section "..ts_id.."!")
+ end
+end)
+
+-- TCB marker entities
+
+-- table with objectRefs
+local markerent = {}
+
+minetest.register_entity("advtrains_interlocking:tcbmarker", {
+ visual = "mesh",
+ mesh = "trackplane.b3d",
+ textures = {"at_il_tcb_marker.png"},
+ collisionbox = {-1,-0.5,-1, 1,-0.4,1},
+ visual_size = {x=10, y=10},
+ on_punch = function(self)
+ self.object:remove()
+ end,
+ on_rightclick = function(self, player)
+ if self.tcbpos and player then
+ advtrains.interlocking.show_tcb_form(self.tcbpos, player:get_player_name())
+ end
+ end,
+ get_staticdata = function() return "STATIC" end,
+ on_activate = function(self, sdata) if sdata=="STATIC" then self.object:remove() end end,
+ static_save = false,
+})
+
+function advtrains.interlocking.show_tcb_marker(pos)
+ --atdebug("showing tcb marker",pos)
+ local tcb = ildb.get_tcb(pos)
+ if not tcb then return end
+ local node_ok, conns, rhe = advtrains.get_rail_info_at(pos, advtrains.all_tracktypes)
+ if not node_ok then return end
+ local yaw = advtrains.conn_angle_median(conns[2].c, conns[1].c)
+
+ local itex = {}
+ for connid=1,2 do
+ local tcbs = tcb[connid]
+ local ts
+ if tcbs.ts_id then
+ ts = ildb.get_ts(tcbs.ts_id)
+ end
+ if ts then
+ itex[connid] = ts.name
+ else
+ itex[connid] = "--EOI--"
+ end
+ end
+
+ local pts = advtrains.roundfloorpts(pos)
+ if markerent[pts] then
+ markerent[pts]:remove()
+ end
+
+ local obj = minetest.add_entity(pos, "advtrains_interlocking:tcbmarker")
+ if not obj then return end
+ obj:set_yaw(yaw)
+ obj:set_properties({
+ infotext = "A = "..itex[1].."\nB = "..itex[2]
+ })
+ local le = obj:get_luaentity()
+ if le then le.tcbpos = pos end
+
+ markerent[pts] = obj
+end
+
+-- Signalling formspec - set routes a.s.o
+
+-- textlist selection temporary storage
+local sig_pselidx = {}
+-- Players having a signalling form open
+local p_open_sig_form = {}
+
+function advtrains.interlocking.show_signalling_form(sigd, pname, sel_rte, called_from_form_update)
+ if not minetest.check_player_privs(pname, "train_operator") then
+ minetest.chat_send_player(pname, "Insufficient privileges to use this!")
+ return
+ end
+ local hasprivs = minetest.check_player_privs(pname, "interlocking")
+ local tcbs = ildb.get_tcbs(sigd)
+
+ if not tcbs.signal then return end
+ if not tcbs.signal_name then tcbs.signal_name = "Signal at "..minetest.pos_to_string(sigd.p) end
+ if not tcbs.routes then tcbs.routes = {} end
+
+ local form = "size[7,10]label[0.5,0.5;Signal at "..minetest.pos_to_string(sigd.p).."]"
+ form = form.."field[0.8,1.5;5.2,1;name;Signal name;"..minetest.formspec_escape(tcbs.signal_name).."]"
+ form = form.."button[5.5,1.2;1,1;setname;Set]"
+
+ if tcbs.routeset then
+ local rte = tcbs.routes[tcbs.routeset]
+ if not rte then
+ atwarn("Unknown route set from signal!")
+ tcbs.routeset = nil
+ return
+ end
+ form = form.."label[0.5,2.5;A route is requested from this signal:]"
+ form = form.."label[0.5,3.0;"..minetest.formspec_escape(rte.name).."]"
+ if tcbs.route_committed then
+ form = form.."label[0.5,3.5;Route has been set.]"
+ else
+ form = form.."label[0.5,3.5;Waiting for route to be set...]"
+ if tcbs.route_rsn then
+ form = form.."label[0.5,4;"..minetest.formspec_escape(tcbs.route_rsn).."]"
+ end
+ end
+ if not tcbs.route_auto then
+ form = form.."button[0.5,7; 5,1;auto;Enable Automatic Working]"
+ else
+ form = form.."label[0.5,7 ;Automatic Working is active.]"
+ form = form.."label[0.5,7.3;Route is re-set when a train passed.]"
+ form = form.."button[0.5,7.7; 5,1;noauto;Disable Automatic Working]"
+ end
+
+ form = form.."button[0.5,6; 5,1;cancelroute;Cancel Route]"
+ else
+ if not tcbs.route_origin then
+ local strtab = {}
+ for idx, route in ipairs(tcbs.routes) do
+ local clr = ""
+ if route.ars then
+ clr = "#FF5555"
+ if route.ars.default then
+ clr = "#55FF55"
+ end
+ end
+ strtab[#strtab+1] = clr .. minetest.formspec_escape(route.name)
+ end
+ form = form.."label[0.5,2.5;Routes:]"
+ form = form.."textlist[0.5,3;5,3;rtelist;"..table.concat(strtab, ",").."]"
+ if sel_rte then
+ form = form.."button[0.5,6; 5,1;setroute;Set Route]"
+ form = form.."button[0.5,7;2,1;dsproute;Show]"
+ if hasprivs then
+ form = form.."button[3.5,7;2,1;editroute;Edit]"
+ end
+ else
+ if tcbs.ars_disabled then
+ form = form.."label[0.5,6 ;NOTE: ARS is disabled.]"
+ form = form.."label[0.5,6.5;Routes are not automatically set.]"
+ end
+ end
+ if hasprivs then
+ form = form.."button[0.5,8;2.5,1;newroute;New Route]"
+ form = form.."button[ 3,8;2.5,1;unassign;Unassign Signal]"
+ form = form.."button[ 3,9;2.5,1;influp;Influence Point]"
+ end
+ if tcbs.ars_disabled then
+ form = form.."button[0.5,9;2.5,1;arsenable;Enable ARS]"
+ else
+ form = form.."button[0.5,9;2.5,1;arsdisable;Disable ARS]"
+ end
+ elseif sigd_equal(tcbs.route_origin, sigd) then
+ -- something has gone wrong: tcbs.routeset should have been set...
+ form = form.."label[0.5,2.5;Inconsistent state: route_origin is same TCBS but no route set. Try again.]"
+ ilrs.cancel_route_from(sigd)
+ else
+ form = form.."label[0.5,2.5;Route is set over this signal by:\n"..sigd_to_string(tcbs.route_origin).."]"
+ form = form.."label[0.5,4;Wait for this route to be cancelled in order to do anything here.]"
+ end
+ end
+ sig_pselidx[pname] = sel_rte
+ minetest.show_formspec(pname, "at_il_signalling_"..minetest.pos_to_string(sigd.p).."_"..sigd.s, form)
+ p_open_sig_form[pname] = sigd
+
+ -- always a good idea to update the signal aspect
+ if not called_from_form_update then
+ -- FIX prevent a callback loop
+ advtrains.interlocking.update_signal_aspect(tcbs)
+ end
+end
+
+function advtrains.interlocking.update_player_forms(sigd)
+ for pname, tsigd in pairs(p_open_sig_form) do
+ if advtrains.interlocking.sigd_equal(sigd, tsigd) then
+ advtrains.interlocking.show_signalling_form(sigd, pname, nil)
+ end
+ end
+end
+
+
+minetest.register_on_player_receive_fields(function(player, formname, fields)
+ local pname = player:get_player_name()
+ if not minetest.check_player_privs(pname, "train_operator") then
+ return
+ end
+ local hasprivs = minetest.check_player_privs(pname, "interlocking")
+
+ -- independent of the formspec, clear this whenever some formspec event happens
+ local tpsi = sig_pselidx[pname]
+ sig_pselidx[pname] = nil
+ p_open_sig_form[pname] = nil
+
+ local pts, connids = string.match(formname, "^at_il_signalling_([^_]+)_(%d)$")
+ local pos, connid
+ if pts then
+ pos = minetest.string_to_pos(pts)
+ connid = tonumber(connids)
+ if not connid or connid<1 or connid>2 then return end
+ end
+ if pos and connid and not fields.quit then
+ local sigd = {p=pos, s=connid}
+ local tcbs = ildb.get_tcbs(sigd)
+ if not tcbs then return end
+
+ local sel_rte
+ if fields.rtelist then
+ local tev = minetest.explode_textlist_event(fields.rtelist)
+ sel_rte = tev.index
+ elseif tpsi then
+ sel_rte = tpsi
+ end
+ if fields.setname and fields.name and hasprivs then
+ tcbs.signal_name = fields.name
+ end
+ if tcbs.routeset and fields.cancelroute then
+ if tcbs.routes[tcbs.routeset] and tcbs.routes[tcbs.routeset].ars then
+ tcbs.ars_disabled = true
+ end
+ -- if route committed, cancel route ts info
+ ilrs.update_route(sigd, tcbs, nil, true)
+ end
+ if not tcbs.routeset then
+ if fields.newroute and hasprivs then
+ advtrains.interlocking.init_route_prog(pname, sigd)
+ minetest.close_formspec(pname, formname)
+ return
+ end
+ if sel_rte and tcbs.routes[sel_rte] then
+ if fields.setroute then
+ ilrs.update_route(sigd, tcbs, sel_rte)
+ end
+ if fields.dsproute then
+ local t = os.clock()
+ advtrains.interlocking.visualize_route(sigd, tcbs.routes[sel_rte], "disp_"..t)
+ minetest.after(10, function() advtrains.interlocking.clear_visu_context("disp_"..t) end)
+ end
+ if fields.editroute and hasprivs then
+ advtrains.interlocking.show_route_edit_form(pname, sigd, sel_rte)
+ --local rte = tcbs.routes[sel_rte]
+ --minetest.show_formspec(pname, formname.."_renroute_"..sel_rte, "field[name;Enter new route name;"..rte.name.."]")
+ return
+ end
+ end
+ end
+
+ if fields.unassign and hasprivs then
+ -- unassigning the signal from the tcbs
+ -- only when no route is set.
+ -- Routes and name remain saved, in case the player wants to reassign a new signal
+ if not tcbs.routeset then
+ local signal_pos = tcbs.signal
+ ildb.set_sigd_for_signal(signal_pos, nil)
+ tcbs.signal = nil
+ tcbs.aspect = nil
+ minetest.close_formspec(pname, formname)
+ minetest.chat_send_player(pname, "Signal has been unassigned. Name and routes are kept for reuse.")
+ return
+ else
+ minetest.chat_send_player(pname, "Please cancel route first!")
+ end
+ end
+ if fields.influp and hasprivs then
+ advtrains.interlocking.show_ip_form(tcbs.signal, pname)
+ return
+ end
+
+ if tcbs.ars_disabled and fields.arsenable then
+ tcbs.ars_disabled = nil
+ end
+ if not tcbs.ars_disabled and fields.arsdisable then
+ tcbs.ars_disabled = true
+ end
+
+ if fields.auto then
+ tcbs.route_auto = true
+ end
+ if fields.noauto then
+ tcbs.route_auto = false
+ end
+
+ advtrains.interlocking.show_signalling_form(sigd, pname, sel_rte, true)
+ return
+ end
+
+
+ if not hasprivs then return end
+ -- rename route
+ local rind, rte_id
+ pts, connids, rind = string.match(formname, "^at_il_signalling_([^_]+)_(%d)_renroute_(%d+)$")
+ if pts then
+ pos = minetest.string_to_pos(pts)
+ connid = tonumber(connids)
+ rte_id = tonumber(rind)
+ if not connid or connid<1 or connid>2 then return end
+ end
+ if pos and connid and rind and fields.name then
+ local sigd = {p=pos, s=connid}
+ local tcbs = ildb.get_tcbs(sigd)
+ if tcbs.routes[rte_id] then
+ tcbs.routes[rte_id].name = fields.name
+ advtrains.interlocking.show_signalling_form(sigd, pname)
+ end
+ end
+end)
diff --git a/advtrains_interlocking/textures/advtrains_dtrack_npr_placer.png b/advtrains_interlocking/textures/advtrains_dtrack_npr_placer.png
new file mode 100644
index 0000000..0d1c769
--- /dev/null
+++ b/advtrains_interlocking/textures/advtrains_dtrack_npr_placer.png
Binary files differ
diff --git a/advtrains_interlocking/textures/advtrains_dtrack_shared_npr.png b/advtrains_interlocking/textures/advtrains_dtrack_shared_npr.png
new file mode 100644
index 0000000..0116c27
--- /dev/null
+++ b/advtrains_interlocking/textures/advtrains_dtrack_shared_npr.png
Binary files differ
diff --git a/advtrains_interlocking/textures/at_il_route_end.png b/advtrains_interlocking/textures/at_il_route_end.png
new file mode 100644
index 0000000..1433f0c
--- /dev/null
+++ b/advtrains_interlocking/textures/at_il_route_end.png
Binary files differ
diff --git a/advtrains_interlocking/textures/at_il_route_lock.png b/advtrains_interlocking/textures/at_il_route_lock.png
new file mode 100644
index 0000000..6a5269b
--- /dev/null
+++ b/advtrains_interlocking/textures/at_il_route_lock.png
Binary files differ
diff --git a/advtrains_interlocking/textures/at_il_route_lock_edit.png b/advtrains_interlocking/textures/at_il_route_lock_edit.png
new file mode 100644
index 0000000..df5f923
--- /dev/null
+++ b/advtrains_interlocking/textures/at_il_route_lock_edit.png
Binary files differ
diff --git a/advtrains_interlocking/textures/at_il_route_set.png b/advtrains_interlocking/textures/at_il_route_set.png
new file mode 100644
index 0000000..3531420
--- /dev/null
+++ b/advtrains_interlocking/textures/at_il_route_set.png
Binary files differ
diff --git a/advtrains_interlocking/textures/at_il_route_start.png b/advtrains_interlocking/textures/at_il_route_start.png
new file mode 100644
index 0000000..dcb5160
--- /dev/null
+++ b/advtrains_interlocking/textures/at_il_route_start.png
Binary files differ
diff --git a/advtrains_interlocking/textures/at_il_routep_advance.png b/advtrains_interlocking/textures/at_il_routep_advance.png
new file mode 100644
index 0000000..d971e85
--- /dev/null
+++ b/advtrains_interlocking/textures/at_il_routep_advance.png
Binary files differ
diff --git a/advtrains_interlocking/textures/at_il_routep_end_here.png b/advtrains_interlocking/textures/at_il_routep_end_here.png
new file mode 100644
index 0000000..9dd3088
--- /dev/null
+++ b/advtrains_interlocking/textures/at_il_routep_end_here.png
Binary files differ
diff --git a/advtrains_interlocking/textures/at_il_routep_end_over.png b/advtrains_interlocking/textures/at_il_routep_end_over.png
new file mode 100644
index 0000000..e03198b
--- /dev/null
+++ b/advtrains_interlocking/textures/at_il_routep_end_over.png
Binary files differ
diff --git a/advtrains_interlocking/textures/at_il_routep_end_over_last.png b/advtrains_interlocking/textures/at_il_routep_end_over_last.png
new file mode 100644
index 0000000..f4fb1aa
--- /dev/null
+++ b/advtrains_interlocking/textures/at_il_routep_end_over_last.png
Binary files differ
diff --git a/advtrains_interlocking/textures/at_il_signal_asp_danger.png b/advtrains_interlocking/textures/at_il_signal_asp_danger.png
new file mode 100644
index 0000000..fca786d
--- /dev/null
+++ b/advtrains_interlocking/textures/at_il_signal_asp_danger.png
Binary files differ
diff --git a/advtrains_interlocking/textures/at_il_signal_asp_free.png b/advtrains_interlocking/textures/at_il_signal_asp_free.png
new file mode 100644
index 0000000..e9d6e9c
--- /dev/null
+++ b/advtrains_interlocking/textures/at_il_signal_asp_free.png
Binary files differ
diff --git a/advtrains_interlocking/textures/at_il_signal_asp_slow.png b/advtrains_interlocking/textures/at_il_signal_asp_slow.png
new file mode 100644
index 0000000..9242bb3
--- /dev/null
+++ b/advtrains_interlocking/textures/at_il_signal_asp_slow.png
Binary files differ
diff --git a/advtrains_interlocking/textures/at_il_signal_ip.png b/advtrains_interlocking/textures/at_il_signal_ip.png
new file mode 100644
index 0000000..bf1618a
--- /dev/null
+++ b/advtrains_interlocking/textures/at_il_signal_ip.png
Binary files differ
diff --git a/advtrains_interlocking/textures/at_il_signal_off.png b/advtrains_interlocking/textures/at_il_signal_off.png
new file mode 100644
index 0000000..f9b1f79
--- /dev/null
+++ b/advtrains_interlocking/textures/at_il_signal_off.png
Binary files differ
diff --git a/advtrains_interlocking/textures/at_il_tcb_marker.png b/advtrains_interlocking/textures/at_il_tcb_marker.png
new file mode 100644
index 0000000..3efc38a
--- /dev/null
+++ b/advtrains_interlocking/textures/at_il_tcb_marker.png
Binary files differ
diff --git a/advtrains_interlocking/textures/at_il_tcb_node.png b/advtrains_interlocking/textures/at_il_tcb_node.png
new file mode 100644
index 0000000..d5f615f
--- /dev/null
+++ b/advtrains_interlocking/textures/at_il_tcb_node.png
Binary files differ
diff --git a/advtrains_interlocking/textures/at_il_tool.png b/advtrains_interlocking/textures/at_il_tool.png
new file mode 100644
index 0000000..f6ce1cc
--- /dev/null
+++ b/advtrains_interlocking/textures/at_il_tool.png
Binary files differ
diff --git a/advtrains_interlocking/textures/at_il_turnout_cr_l.png b/advtrains_interlocking/textures/at_il_turnout_cr_l.png
new file mode 100644
index 0000000..fb79e3d
--- /dev/null
+++ b/advtrains_interlocking/textures/at_il_turnout_cr_l.png
Binary files differ
diff --git a/advtrains_interlocking/textures/at_il_turnout_cr_r.png b/advtrains_interlocking/textures/at_il_turnout_cr_r.png
new file mode 100644
index 0000000..e04dfbd
--- /dev/null
+++ b/advtrains_interlocking/textures/at_il_turnout_cr_r.png
Binary files differ
diff --git a/advtrains_interlocking/textures/at_il_turnout_free.png b/advtrains_interlocking/textures/at_il_turnout_free.png
new file mode 100644
index 0000000..5c83193
--- /dev/null
+++ b/advtrains_interlocking/textures/at_il_turnout_free.png
Binary files differ
diff --git a/advtrains_interlocking/textures/at_il_turnout_st.png b/advtrains_interlocking/textures/at_il_turnout_st.png
new file mode 100644
index 0000000..50d5ad5
--- /dev/null
+++ b/advtrains_interlocking/textures/at_il_turnout_st.png
Binary files differ
diff --git a/advtrains_interlocking/tool.lua b/advtrains_interlocking/tool.lua
new file mode 100644
index 0000000..5d38b3a
--- /dev/null
+++ b/advtrains_interlocking/tool.lua
@@ -0,0 +1,66 @@
+-- tool.lua
+-- Interlocking tool
+
+local ilrs = advtrains.interlocking.route
+
+minetest.register_craftitem("advtrains_interlocking:tool",{
+ description = "Interlocking tool\nright-click turnouts to inspect route locks",
+ groups = {cracky=1}, -- key=name, value=rating; rating=1..3.
+ inventory_image = "at_il_tool.png",
+ wield_image = "at_il_tool.png",
+ stack_max = 1,
+ on_place = function(itemstack, placer, pointed_thing)
+ local pname = placer:get_player_name()
+ if not pname then
+ return
+ end
+ if not minetest.check_player_privs(pname, {interlocking=true}) then
+ minetest.chat_send_player(pname, "Insufficient privileges to use this!")
+ return
+ end
+ if pointed_thing.type=="node" then
+ local pos=pointed_thing.under
+ if advtrains.is_passive(pos) then
+ local form = "size[7,5]label[0.5,0.5;Route lock inspector]"
+ local pts = minetest.pos_to_string(pos)
+
+ local rtl = ilrs.has_route_lock(pts)
+
+ if rtl then
+ form = form.."label[0.5,1;Route locks currently put:\n"..rtl.."]"
+ form = form.."button_exit[0.5,3.5; 5,1;clear;Clear]"
+ else
+ form = form.."label[0.5,1;No route locks set]"
+ form = form.."button_exit[0.5,3.5; 5,1;emplace;Emplace manual lock]"
+ end
+
+ minetest.show_formspec(pname, "at_il_rtool_"..pts, form)
+ else
+ minetest.chat_send_player(pname, "Cannot use this here.")
+ return
+ end
+ end
+ end,
+})
+
+minetest.register_on_player_receive_fields(function(player, formname, fields)
+ local pname = player:get_player_name()
+ if not minetest.check_player_privs(pname, "interlocking") then
+ return
+ end
+ local pos
+ local pts = string.match(formname, "^at_il_rtool_(.+)$")
+ if pts then
+ pos = minetest.string_to_pos(pts)
+ end
+ if pos then
+ if advtrains.is_passive(pos) then
+ if fields.clear then
+ ilrs.remove_route_locks(pts)
+ end
+ if fields.emplace then
+ ilrs.add_manual_route_lock(pts, "Manual lock ("..pname..")")
+ end
+ end
+ end
+end)
diff --git a/advtrains_interlocking/train_sections.lua b/advtrains_interlocking/train_sections.lua
new file mode 100644
index 0000000..757f36a
--- /dev/null
+++ b/advtrains_interlocking/train_sections.lua
@@ -0,0 +1,199 @@
+-- train_related.lua
+-- Occupation of track sections - mainly implementation of train callbacks
+
+--[[
+Track section occupation is saved as follows
+
+In train:
+train.il_sections = {
+ [n] = {ts_id = <...> (origin = <sigd>)}
+}
+-- "origin" is the TCB (signal describer) the train initially entered this section
+
+In track section
+ts.trains = {
+ [n] = <train_id>
+}
+
+When any inconsistency is detected, we will assume the most restrictive setup.
+It will be possible to indicate a section "free" via the GUI.
+]]
+
+local ildb = advtrains.interlocking.db
+
+local sigd_equal = advtrains.interlocking.sigd_equal
+
+local function itexist(tbl, com)
+ for _,item in ipairs(tbl) do
+ if (item==com) then
+ return true
+ end
+ end
+ return false
+end
+local function itkexist(tbl, ikey, com)
+ for _,item in ipairs(tbl) do
+ if item[ikey] == com then
+ return true
+ end
+ end
+ return false
+end
+
+local function itremove(tbl, com)
+ local i=1
+ while i <= #tbl do
+ if tbl[i] == com then
+ table.remove(tbl, i)
+ else
+ i = i + 1
+ end
+ end
+end
+local function itkremove(tbl, ikey, com)
+ local i=1
+ while i <= #tbl do
+ if tbl[i][ikey] == com then
+ table.remove(tbl, i)
+ else
+ i = i + 1
+ end
+ end
+end
+
+local function setsection(tid, train, ts_id, ts, sigd)
+ -- train
+ if not train.il_sections then train.il_sections = {} end
+ if not itkexist(train.il_sections, "ts_id", ts_id) then
+ table.insert(train.il_sections, {ts_id = ts_id, origin = sigd})
+ end
+
+ -- ts
+ if not ts.trains then ts.trains = {} end
+ if not itexist(ts.trains, tid) then
+ table.insert(ts.trains, tid)
+ end
+
+ -- routes
+ local tcbs = advtrains.interlocking.db.get_tcbs(sigd)
+
+ -- route setting - clear route state
+ if ts.route then
+ --atdebug(tid,"enters",ts_id,"examining Routestate",ts.route)
+ if not sigd_equal(ts.route.entry, sigd) then
+ -- Train entered not from the route. Locate origin and cancel route!
+ atwarn("Train",tid,"hit route",ts.route.rsn,"!")
+ advtrains.interlocking.route.cancel_route_from(ts.route.origin)
+ atwarn("Route was cancelled.")
+ else
+ -- train entered route regularily. Reset route and signal
+ tcbs.route_committed = nil
+ tcbs.route_comitted = nil -- TODO compatibility cleanup
+ tcbs.aspect = nil
+ tcbs.route_origin = nil
+ advtrains.interlocking.update_signal_aspect(tcbs)
+ if tcbs.signal and sigd_equal(ts.route.entry, ts.route.origin) then
+ if tcbs.route_auto and tcbs.routeset then
+ --atdebug("Resetting route (",ts.route.origin,")")
+ advtrains.interlocking.route.update_route(ts.route.origin, tcbs)
+ else
+ tcbs.routeset = nil
+ end
+ end
+ end
+ ts.route = nil
+ end
+ if tcbs.signal then
+ advtrains.interlocking.route.update_route(sigd, tcbs)
+ end
+end
+
+local function freesection(tid, train, ts_id, ts)
+ -- train
+ if not train.il_sections then train.il_sections = {} end
+ itkremove(train.il_sections, "ts_id", ts_id)
+
+ -- ts
+ if not ts.trains then ts.trains = {} end
+ itremove(ts.trains, tid)
+
+ if ts.route_post then
+ advtrains.interlocking.route.free_route_locks(ts_id, ts.route_post.locks)
+ if ts.route_post.next then
+ --this does nothing when the train went the right way, because
+ -- "route" info is already cleared.
+ advtrains.interlocking.route.cancel_route_from(ts.route_post.next)
+ end
+ ts.route_post = nil
+ end
+ -- This must be delayed, because this code is executed in-between a train step
+ -- TODO use luaautomation timers?
+ minetest.after(0, advtrains.interlocking.route.update_waiting, "ts", ts_id)
+end
+
+
+-- This is regular operation
+-- The train is on a track and drives back and forth
+
+-- This sets the section for both directions, to be failsafe
+advtrains.tnc_register_on_enter(function(pos, id, train, index)
+ local tcb = ildb.get_tcb(pos)
+ if tcb then
+ for connid=1,2 do
+ local ts = tcb[connid].ts_id and ildb.get_ts(tcb[connid].ts_id)
+ if ts then
+ setsection(id, train, tcb[connid].ts_id, ts, {p=pos, s=connid})
+ end
+ end
+ end
+end)
+
+
+-- this time, of course, only clear the backside (cp connid)
+advtrains.tnc_register_on_leave(function(pos, id, train, index)
+ local tcb = ildb.get_tcb(pos)
+ if tcb and train.path_cp[index] then
+ local connid = train.path_cp[index]
+ local ts = tcb[connid].ts_id and ildb.get_ts(tcb[connid].ts_id)
+ if ts then
+ freesection(id, train, tcb[connid].ts_id, ts)
+ end
+ end
+end)
+
+-- those callbacks are needed to account for created and removed trains (also regarding coupling)
+
+advtrains.te_register_on_create(function(id, train)
+ -- let's see what track sections we find here
+ local index = atround(train.index)
+ local pos = advtrains.path_get(train, index)
+ local ts_id, origin = ildb.get_ts_at_pos(pos)
+ if ts_id then
+ local ts = ildb.get_ts(ts_id)
+ if ts then
+ setsection(id, train, ts_id, ts, origin)
+ else
+ atwarn("ILDB corruption: TCB",origin," has invalid TS reference")
+ end
+ -- Make train a shunt move
+ train.is_shunt = true
+ elseif ts_id==nil then
+ atlog("Train",id,": Unable to determine whether to block a track section!")
+ else
+ --atdebug("Train",id,": Outside of interlocked area!")
+ end
+end)
+
+advtrains.te_register_on_remove(function(id, train)
+ if train.il_sections then
+ for idx, item in ipairs(train.il_sections) do
+
+ local ts = item.ts_id and ildb.get_ts(item.ts_id)
+
+ if ts and ts.trains then
+ itremove(ts.trains, id)
+ end
+ end
+ train.il_sections = nil
+ end
+end)
diff --git a/advtrains_interlocking/tsr_rail.lua b/advtrains_interlocking/tsr_rail.lua
new file mode 100644
index 0000000..f302540
--- /dev/null
+++ b/advtrains_interlocking/tsr_rail.lua
@@ -0,0 +1,66 @@
+-- tsr_rail.lua
+-- Point speed restriction rails
+-- Simple rail whose only purpose is to place a TSR on the position, as a temporary solution until the timetable system covers everything.
+-- This code resembles the code in lines/stoprail.lua
+
+local function updateform(pos)
+ local meta = minetest.get_meta(pos)
+ local pe = advtrains.encode_pos(pos)
+ local npr = advtrains.interlocking.npr_rails[pe] or 2
+
+ meta:set_string("infotext", "Point speed restriction: "..npr)
+ meta:set_string("formspec", "field[npr;Set point speed restriction:;"..npr.."]")
+end
+
+
+local adefunc = function(def, preset, suffix, rotation)
+ return {
+ after_place_node=function(pos)
+ updateform(pos)
+ end,
+ after_dig_node=function(pos)
+ local pe = advtrains.encode_pos(pos)
+ advtrains.interlocking.npr_rails[pe] = nil
+ end,
+ on_receive_fields = function(pos, formname, fields, player)
+ local pname = player:get_player_name()
+ if not minetest.check_player_privs(pname, {interlocking=true}) then
+ minetest.chat_send_player(pname, "Interlocking privilege required!")
+ return
+ end
+ if minetest.is_protected(pos, pname) then
+ minetest.chat_send_player(pname, "This rail is protected!")
+ minetest.record_protection_violation(pos, pname)
+ return
+ end
+ if fields.npr then
+ local pe = advtrains.encode_pos(pos)
+ advtrains.interlocking.npr_rails[pe] = tonumber(fields.npr)
+ updateform(pos)
+ end
+ end,
+ advtrains = {
+ on_train_approach = function(pos,train_id, train, index)
+ if train.path_cn[index] == 1 then
+ local pe = advtrains.encode_pos(pos)
+ local npr = advtrains.interlocking.npr_rails[pe] or 2
+ advtrains.lzb_add_checkpoint(train, index, npr, nil)
+ end
+ end,
+ },
+ }
+end
+
+
+if minetest.get_modpath("advtrains_train_track") ~= nil then
+ advtrains.register_tracks("default", {
+ nodename_prefix="advtrains_interlocking:dtrack_npr",
+ texture_prefix="advtrains_dtrack_npr",
+ models_prefix="advtrains_dtrack",
+ models_suffix=".b3d",
+ shared_texture="advtrains_dtrack_shared_npr.png",
+ description="Point Speed Restriction Rail",
+ formats={},
+ get_additional_definiton = adefunc,
+ }, advtrains.trackpresets.t_30deg_straightonly)
+end