diff options
Diffstat (limited to 'advtrains_interlocking')
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 Binary files differnew file mode 100644 index 0000000..0d1c769 --- /dev/null +++ b/advtrains_interlocking/textures/advtrains_dtrack_npr_placer.png diff --git a/advtrains_interlocking/textures/advtrains_dtrack_shared_npr.png b/advtrains_interlocking/textures/advtrains_dtrack_shared_npr.png Binary files differnew file mode 100644 index 0000000..0116c27 --- /dev/null +++ b/advtrains_interlocking/textures/advtrains_dtrack_shared_npr.png diff --git a/advtrains_interlocking/textures/at_il_route_end.png b/advtrains_interlocking/textures/at_il_route_end.png Binary files differnew file mode 100644 index 0000000..1433f0c --- /dev/null +++ b/advtrains_interlocking/textures/at_il_route_end.png diff --git a/advtrains_interlocking/textures/at_il_route_lock.png b/advtrains_interlocking/textures/at_il_route_lock.png Binary files differnew file mode 100644 index 0000000..6a5269b --- /dev/null +++ b/advtrains_interlocking/textures/at_il_route_lock.png diff --git a/advtrains_interlocking/textures/at_il_route_lock_edit.png b/advtrains_interlocking/textures/at_il_route_lock_edit.png Binary files differnew file mode 100644 index 0000000..df5f923 --- /dev/null +++ b/advtrains_interlocking/textures/at_il_route_lock_edit.png diff --git a/advtrains_interlocking/textures/at_il_route_set.png b/advtrains_interlocking/textures/at_il_route_set.png Binary files differnew file mode 100644 index 0000000..3531420 --- /dev/null +++ b/advtrains_interlocking/textures/at_il_route_set.png diff --git a/advtrains_interlocking/textures/at_il_route_start.png b/advtrains_interlocking/textures/at_il_route_start.png Binary files differnew file mode 100644 index 0000000..dcb5160 --- /dev/null +++ b/advtrains_interlocking/textures/at_il_route_start.png diff --git a/advtrains_interlocking/textures/at_il_routep_advance.png b/advtrains_interlocking/textures/at_il_routep_advance.png Binary files differnew file mode 100644 index 0000000..d971e85 --- /dev/null +++ b/advtrains_interlocking/textures/at_il_routep_advance.png diff --git a/advtrains_interlocking/textures/at_il_routep_end_here.png b/advtrains_interlocking/textures/at_il_routep_end_here.png Binary files differnew file mode 100644 index 0000000..9dd3088 --- /dev/null +++ b/advtrains_interlocking/textures/at_il_routep_end_here.png diff --git a/advtrains_interlocking/textures/at_il_routep_end_over.png b/advtrains_interlocking/textures/at_il_routep_end_over.png Binary files differnew file mode 100644 index 0000000..e03198b --- /dev/null +++ b/advtrains_interlocking/textures/at_il_routep_end_over.png diff --git a/advtrains_interlocking/textures/at_il_routep_end_over_last.png b/advtrains_interlocking/textures/at_il_routep_end_over_last.png Binary files differnew file mode 100644 index 0000000..f4fb1aa --- /dev/null +++ b/advtrains_interlocking/textures/at_il_routep_end_over_last.png diff --git a/advtrains_interlocking/textures/at_il_signal_asp_danger.png b/advtrains_interlocking/textures/at_il_signal_asp_danger.png Binary files differnew file mode 100644 index 0000000..fca786d --- /dev/null +++ b/advtrains_interlocking/textures/at_il_signal_asp_danger.png diff --git a/advtrains_interlocking/textures/at_il_signal_asp_free.png b/advtrains_interlocking/textures/at_il_signal_asp_free.png Binary files differnew file mode 100644 index 0000000..e9d6e9c --- /dev/null +++ b/advtrains_interlocking/textures/at_il_signal_asp_free.png diff --git a/advtrains_interlocking/textures/at_il_signal_asp_slow.png b/advtrains_interlocking/textures/at_il_signal_asp_slow.png Binary files differnew file mode 100644 index 0000000..9242bb3 --- /dev/null +++ b/advtrains_interlocking/textures/at_il_signal_asp_slow.png diff --git a/advtrains_interlocking/textures/at_il_signal_ip.png b/advtrains_interlocking/textures/at_il_signal_ip.png Binary files differnew file mode 100644 index 0000000..bf1618a --- /dev/null +++ b/advtrains_interlocking/textures/at_il_signal_ip.png diff --git a/advtrains_interlocking/textures/at_il_signal_off.png b/advtrains_interlocking/textures/at_il_signal_off.png Binary files differnew file mode 100644 index 0000000..f9b1f79 --- /dev/null +++ b/advtrains_interlocking/textures/at_il_signal_off.png diff --git a/advtrains_interlocking/textures/at_il_tcb_marker.png b/advtrains_interlocking/textures/at_il_tcb_marker.png Binary files differnew file mode 100644 index 0000000..3efc38a --- /dev/null +++ b/advtrains_interlocking/textures/at_il_tcb_marker.png diff --git a/advtrains_interlocking/textures/at_il_tcb_node.png b/advtrains_interlocking/textures/at_il_tcb_node.png Binary files differnew file mode 100644 index 0000000..d5f615f --- /dev/null +++ b/advtrains_interlocking/textures/at_il_tcb_node.png diff --git a/advtrains_interlocking/textures/at_il_tool.png b/advtrains_interlocking/textures/at_il_tool.png Binary files differnew file mode 100644 index 0000000..f6ce1cc --- /dev/null +++ b/advtrains_interlocking/textures/at_il_tool.png diff --git a/advtrains_interlocking/textures/at_il_turnout_cr_l.png b/advtrains_interlocking/textures/at_il_turnout_cr_l.png Binary files differnew file mode 100644 index 0000000..fb79e3d --- /dev/null +++ b/advtrains_interlocking/textures/at_il_turnout_cr_l.png diff --git a/advtrains_interlocking/textures/at_il_turnout_cr_r.png b/advtrains_interlocking/textures/at_il_turnout_cr_r.png Binary files differnew file mode 100644 index 0000000..e04dfbd --- /dev/null +++ b/advtrains_interlocking/textures/at_il_turnout_cr_r.png diff --git a/advtrains_interlocking/textures/at_il_turnout_free.png b/advtrains_interlocking/textures/at_il_turnout_free.png Binary files differnew file mode 100644 index 0000000..5c83193 --- /dev/null +++ b/advtrains_interlocking/textures/at_il_turnout_free.png diff --git a/advtrains_interlocking/textures/at_il_turnout_st.png b/advtrains_interlocking/textures/at_il_turnout_st.png Binary files differnew file mode 100644 index 0000000..50d5ad5 --- /dev/null +++ b/advtrains_interlocking/textures/at_il_turnout_st.png 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 |