From 6a5540878f334e97b78ef1430698a8bf8b3faa99 Mon Sep 17 00:00:00 2001 From: orwell96 Date: Sun, 19 Mar 2023 15:20:03 +0100 Subject: Auto-Repair Track Sections/TCBs (automatically when adding/removing or triggered by interlocking tool) --- advtrains/helpers.lua | 50 +- advtrains_interlocking/database.lua | 567 +++++++++++++-------- advtrains_interlocking/tcb_ts_ui.lua | 182 +++---- .../textures/at_il_ts_highlight_particle.png | Bin 0 -> 7164 bytes advtrains_interlocking/tool.lua | 90 +++- 5 files changed, 509 insertions(+), 380 deletions(-) create mode 100644 advtrains_interlocking/textures/at_il_ts_highlight_particle.png diff --git a/advtrains/helpers.lua b/advtrains/helpers.lua index bef4903..6d22bc5 100644 --- a/advtrains/helpers.lua +++ b/advtrains/helpers.lua @@ -342,9 +342,10 @@ function advtrains.get_matching_conn(conn, nconns) return connlku[nconns][conn] end -function advtrains.random_id() +function advtrains.random_id(lenp) local idst="" - for i=0,5 do + local len = lenp or 6 + for i=1,len do idst=idst..(math.random(0,9)) end return idst @@ -476,6 +477,14 @@ end -- Metatable: local trackiter_mt = { + -- Internal State: + -- branches: A list of {pos, connid, limit} for where to restart + -- pos: The *next* position that the track iterator will return + -- bconnid: The connid of the connection of the rail at pos that points backward + -- tconns: The connections of the rail at pos + -- limit: the current limit + -- visited: a key-boolean table of already visited rails + -- get whether there are still unprocessed branches has_next_branch = function(self) return #self.branches > 0 @@ -491,13 +500,18 @@ local trackiter_mt = { self.tconns = adj_conns self.limit = br.limit - 1 self.visited[advtrains.encode_pos(br.pos)] = true + self.last_track_already_visited = false return br.pos, br.connid end, -- get the next track along the current branch, -- potentially adding branching tracks to the unprocessed branches list - -- returns status, track_pos, track_connid - -- status is true(ok), false(track has ended), nil(traversing limit has been reached) (when status~=true, track_pos and track_connid are nil) + -- returns track_pos, track_connid, track_backwards_connid + -- On error, returns nil, reason; reason is one of "track_end", "limit_hit", "already_visited" next_track = function(self) + if self.last_track_already_visited then + -- see comment below + return nil, "already_visited" + end local pos = self.pos if not pos then -- last run found track end. Return false @@ -507,12 +521,17 @@ local trackiter_mt = { if self.limit <= 0 then return nil, "limit_hit" end - if self.visited[advtrains.encode_pos(pos)] then - -- node was already seen. do not continue - return nil, "already_visited" - end -- select next conn (main conn to follow is the associated connection) + local old_bconnid = self.bconnid local mconnid = advtrains.get_matching_conn(self.bconnid, #self.tconns) + if self.visited[advtrains.encode_pos(pos)] then + -- node was already seen + -- Due to special requirements for the track section updater, return this first already visited track once + -- but do not process any further rails on this branch + -- The next call will then throw already_visited error + self.last_track_already_visited = true + return pos, mconnid, old_bconnid + end -- If there are more connections, add these to branches for nconnid,_ in ipairs(self.tconns) do if nconnid~=mconnid and nconnid~=self.bconnid then @@ -526,7 +545,16 @@ local trackiter_mt = { self.tconns = adj_conns self.limit = self.limit - 1 self.visited[advtrains.encode_pos(pos)] = true - return pos, mconnid + self.last_track_already_visited = false + return pos, mconnid, old_bconnid + end, + + add_branch = function(self, pos, connid) + table.insert(self.branches, {pos = pos, connid = connid, limit=self.limit}) + end, + + is_visited = function(self, pos) + return self.visited[advtrains.encode_pos(pos)] end, } @@ -569,8 +597,8 @@ while ti:has_next_branch() do repeat if then break end --for example, when traversing should stop at TCBs this can check if there is a tcb here - ok, pos, connid = ti:next_track() - until not ok -- this stops the loop when either the track end is reached or the limit is hit + pos, connid = ti:next_track() + until not pos -- this stops the loop when either the track end is reached or the limit is hit -- while loop continues with the next branch ( diverging branch of one of the switches/crossings) until no more are left end diff --git a/advtrains_interlocking/database.lua b/advtrains_interlocking/database.lua index 5f163e4..38fad77 100644 --- a/advtrains_interlocking/database.lua +++ b/advtrains_interlocking/database.lua @@ -37,6 +37,22 @@ Another case of this: 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. + + +== Terminology / Variable Names == + +"tcb" : A TCB table (as in track_circuit_breaks) +"tcbs" : One side of a tcb (that is tcb == {[1] = tcbs, [2] = tcbs}) +"sigd" : A table of format {p=, s=} by which a "tcbs" is uniqely identified. + +== Section Autorepair & Turnout Cache == + +As fundamental part of reworked route programming mechanism, Track Section objects become weak now. They are created and destroyed on demand. +ildb.repair_tcb automatically checks all nearby sections for issues and repairs them automatically. + +Also the database now holds a cache of the turnouts in the section and their position for all possible driving paths. +Every time a repair operation takes place, and on every track edit operation, the affected sections need to have their cache updated. + ]]-- local TRAVERSER_LIMIT = 1000 @@ -59,7 +75,22 @@ advtrains.interlocking.npr_rails = {} function ildb.load(data) if not data then return end if data.tcbs then - track_circuit_breaks = data.tcbs + if data.tcbpts_conversion_applied then + track_circuit_breaks = data.tcbs + else + -- Convert legacy pos_to_string tcbs to new advtrains.encode_pos position strings + for pts, tcb in pairs(data.tcbs) do + local pos = minetest.string_to_pos(pts) + if pos then + -- that was a pos_to_string + local epos = advtrains.encode_pos(pos) + track_circuit_breaks[epos] = tcb + else + -- keep entry, it is already new + track_circuit_breaks[pts] = tcb + end + end + end end if data.ts then track_sections = data.ts @@ -121,6 +152,7 @@ function ildb.save() rs_callbacks = advtrains.interlocking.route.rte_callbacks, influence_points = influence_points, npr_rails = advtrains.interlocking.npr_rails, + tcbpts_conversion_applied = true, -- remark that legacy pos conversion has taken place } end @@ -147,8 +179,6 @@ TCB data structure -- 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 = , --this can be set by an exit node via mesecons or atlatc, - -- or from the tc formspec. } } @@ -156,7 +186,11 @@ Track section [id] = { name = "Some human-readable name" tc_breaks = { ,... } -- Bounding TC's (signal specifiers) - -- Can be direct ends (auto-detected), conflicting routes or TCBs that are too far away from each other + rs_cache = { [-] = { [] = "state" } } + -- Saves the turnout states that need to be locked when a route is set from tcb#x to tcb#y + -- e.g. "1-2" = { "800080008000" = "st" } + -- Recalculated on every change via update_ts_cache + route = { origin = , -- route origin entry = , -- supposed train entry point @@ -172,7 +206,9 @@ Track section -- 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 = {, ...} -- Set whenever a train (or more) reside in this TC + -- Note: The same train ID may be contained in this mapping multiple times, when it has entered the section in two different places. } @@ -185,24 +221,13 @@ signal_assignments = { } ]] +-- Maximum scan length for track iterator +local TS_MAX_SCAN = 1000 --- -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 +-- basic functions function ildb.get_tcb(pos) - local pts = advtrains.roundfloorpts(pos) + local pts = advtrains.encode_pos(pos) return track_circuit_breaks[pts] end @@ -212,227 +237,347 @@ function ildb.get_tcbs(sigd) 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 +-- retrieve full tables. Please use only read-only! +function ildb.get_all_tcb() + return track_circuit_breaks +end +function ildb.get_all_ts() + return track_sections +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 +-- Checks the consistency of the track section at the given position +-- This method attempts to autorepair track sections if they are inconsistent +-- @param pos: the position to start from +-- Returns: +-- ts_id - the track section that was found +-- nil - No track section exists +function ildb.check_and_repair_ts_at_pos(pos) + atdebug("check_and_repair_ts_at_pos", pos) + -- STEP 1: Ensure that only one section is at this place + -- get all TCBs adjacent to this + local all_tcbs = ildb.get_all_tcbs_adjacent(pos, nil) + local first_ts = true + local ts_id + for _,sigd in ipairs(all_tcbs) do + ildb.tcbs_ensure_ts_ref_exists(sigd) + local tcbs_ts_id = sigd.tcbs.ts_id + if first_ts then + -- this one determines + ts_id = tcbs_ts_id + first_ts = false + else + -- these must be the same as the first + if ts_id ~= tcbs_ts_id then + -- inconsistency is found, repair it + atdebug("check_and_repair_ts_at_pos: Inconsistency is found!") + return ildb.repair_ts_merge_all(all_tcbs) + -- Step2 check is no longer necessary since we just created that new section + end 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) + -- only one found (it is either nil or a ts id) + atdebug("check_and_repair_ts_at_pos: TS consistent id=",ts_id,"") + if not ts_id then return + -- All TCBs agreed that there is no section here. 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 + + local ts = ildb.get_ts(ts_id) + if not ts then + -- This branch may never be reached, because ildb.tcbs_ensure_ts_ref_exists(sigd) is already supposed to clear out missing sections + error("check_and_repair_ts_at_pos: Resolved to nonexisting section although ildb.tcbs_ensure_ts_ref_exists(sigd) was supposed to prevent this. Panic!") + end + ildb.purge_ts_tcb_refs(ts_id) + -- STEP 2: Ensure that all_tcbs is equal to the track section's TCB list. If there are extra TCBs then the section should be split + -- ildb.tcbs_ensure_ts_ref_exists(sigd) has already make sure that all tcbs are found in the ts's tc_breaks list + -- That means it is sufficient to compare the LENGTHS of both lists, if one is longer then it is inconsistent + if #ts.tc_breaks ~= #all_tcbs then + atdebug("check_and_repair_ts_at_pos: Partition is found!") + return ildb.repair_ts_merge_all(all_tcbs) + end + return ts_id +end + +-- Helper function to prevent duplicates +local function insert_sigd_if_not_present(tab, sigd) + local found = false + for _, ssigd in ipairs(tab) do + if vector.equals(sigd.p, ssigd.p) and sigd.s==ssigd.s then + found = true end end - -- recursion abort condition - if count > TRAVERSER_LIMIT then - --atdebug("Traverser hit counter at",adj_pos, adj_connid) - return true + if not found then + table.insert(tab, sigd) 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 + return not found +end + +-- Starting from a position, search all TCBs that can be reached from this position. +-- In a non-faulty setup, all of these should have the same track section assigned. +-- This function does not trigger a repair. +-- @param inipos: the initial position +-- @param inidir: the initial direction, or nil to search in all directions +-- Returns: a list of sigd's describing the TCBs found (sigd's point inward): +-- {p=, s=, tcbs=} +function ildb.get_all_tcbs_adjacent(inipos, inidir) + atdebug("get_all_tcbs_adjacent: inipos",inipos,"inidir",inidir,"") + local found_sigd = {} + local ti = advtrains.get_track_iterator(inipos, inidir, TS_MAX_SCAN, true) + local pos, connid, bconnid, tcb + while ti:has_next_branch() do + pos, connid = ti:next_branch() + --atdebug("get_all_tcbs_adjacent: BRANCH: ",pos, connid) + bconnid = nil + repeat + tcb = ildb.get_tcb(pos) + if tcb then + -- found a tcb + if not bconnid then + -- A branch start point cannot be a TCB, as that would imply that it was a turnout/crossing (illegal) + -- Only exception where this can happen is if the start point is a TCB, then we'd need to add the forward side of it to our list + if pos.x==inipos.x and pos.y==inipos.y and pos.z==inipos.z then + -- Note "connid" instead of "bconnid" + atdebug("get_all_tcbs_adjacent: Found Startpoint TCB: ",pos, connid, "ts=", tcb[connid].ts_id) + insert_sigd_if_not_present(found_sigd, {p=pos, s=connid, tcbs=tcb[connid]}) + else + -- this may not happend + error("Found TCB at TrackIterator new branch which is not the start point, this is illegal! pos="..minetest.pos_to_string(pos)) + end + + else + -- add the sigd of this tcb and a reference to the tcb table in it + atdebug("get_all_tcbs_adjacent: Found TCB: ",pos, bconnid, "ts=", tcb[bconnid].ts_id) + insert_sigd_if_not_present(found_sigd, {p=pos, s=bconnid, tcbs=tcb[bconnid]}) + break + end end - end + pos, connid, bconnid = ti:next_track() + --atdebug("get_all_tcbs_adjacent: TRACK: ",pos, connid, bconnid) + until not pos end - return counter_hit + return found_sigd 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 +-- Called by frontend functions when multiple tcbs's that logically belong to one section have been determined to have different sections +-- Parameter is the output of ildb.get_all_tcbs_adjacent(pos) +-- Returns the ID of the track section that results after the merge +function ildb.repair_ts_merge_all(all_tcbs, force_create) + atdebug("repair_ts_merge_all: Instructed to merge sections of following TCBs:") + -- The first loop does the following for each TCBS: + -- a) Store the TS ID in the set of TS to update + -- b) Set the TS ID to nil, so that the TCBS gets removed from the section + local ts_to_update = {} + local ts_name_repo = {} + local any_ts = false + for _,sigd in ipairs(all_tcbs) do + local ts_id = sigd.tcbs.ts_id + atdebug(sigd, "ts=", ts_id) + if ts_id then + local ts = track_sections[ts_id] + if ts then + any_ts = true + ts_to_update[ts_id] = true + -- if nonstandard name, store this + if ts.name and not string.match(ts.name, "^Section") then + ts_name_repo[#ts_name_repo+1] = ts.name + end + end end - advtrains.interlocking.show_tcb_marker(msigd.p) + sigd.tcbs.ts_id = nil 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] + if not any_ts and not force_create then + -- nothing to do at all, just no interlocking. Why were we even called + atdebug("repair_ts_merge_all: No track section present, will not create a new one") + return nil + end + -- Purge every TS in turn. TS's that are now empty will be deleted. TS's that still have TCBs will be kept + for ts_id, _ in pairs(ts_to_update) do + local remain_ts = ildb.purge_ts_tcb_refs(ts_id) + end + -- Create a new fresh track section with all the TCBs we have in our collection + local new_ts_id, new_ts = ildb.create_ts_from_tcb_list(all_tcbs) + return new_ts_id 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 +-- For the specified TS, go through the list of TCBs and purge all TCBs that have no corresponding backreference in their TCBS table. +-- If the track section ends up empty, it is deleted in this process. +-- Should the track section still exist after the purge operation, it is returned. +function ildb.purge_ts_tcb_refs(ts_id) + local ts = track_sections[ts_id] + if not ts then + return nil 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 has_changed = false + local i = 1 + while ts.tc_breaks[i] do + -- get TCBS + local sigd = ts.tc_breaks[i] 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) + if tcbs then + if tcbs.ts_id == ts_id then + -- this one is legit + i = i+1 else - --atdebug("Sync: put",sigd_to_string(sigd),"into list_ok") - ts_id = tcbs.ts_id - table.insert(list_ok, sigd) + -- this one is to be purged + atdebug("purge_ts_tcb_refs(",ts_id,"): purging",sigd,"(backreference = ",tcbs.ts_id,")") + table.remove(ts.tc_breaks, i) + has_changed = true 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) + else + -- if not tcbs: was anyway an orphan, remove it + atdebug("purge_ts_tcb_refs(",ts_id,"): purging",sigd,"(referred nonexisting TCB)") + table.remove(ts.tc_breaks, i) + has_changed = true 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) + if #ts.tc_breaks == 0 then + -- remove the section completely + atdebug("purge_ts_tcb_refs(",ts_id,"): after purging, the section is empty, is being deleted") + track_sections[ts_id] = nil + return nil + else + if has_changed then + -- needs to update route cache + ildb.update_ts_cache(ts_id) end + return ts end end -function ildb.link_track_sections(merge_id, root_id) - if merge_id == root_id then +-- For the specified TCBS, make sure that the track section referenced by it +-- (a) exists and +-- (b) has a backreference to this TCBS stored in its tc_breaks list +function ildb.tcbs_ensure_ts_ref_exists(sigd) + local tcbs = sigd.tcbs or ildb.get_tcbs(sigd) + if not tcbs or not tcbs.ts_id then return end + local ts = ildb.get_ts(tcbs.ts_id) + if not ts then + atdebug("tcbs_ensure_ts_ref_exists(",sigd,"): TS does not exist, setting to nil") + -- TS is deleted, clear own ts id + tcbs.ts_id = nil return end - merge_ts(root_id, merge_id) + local did_insert = insert_sigd_if_not_present(ts.tc_breaks, {p=sigd.p, s=sigd.s}) + if did_insert then + atdebug("tcbs_ensure_ts_ref_exists(",sigd,"): TCBS was missing reference in TS",tcbs.ts_id) + ildb.update_ts_cache(ts_id) + end end -function ildb.remove_from_interlocking(sigd) - local tcbs = ildb.get_tcbs(sigd) - if not ildb.may_modify_tcbs(tcbs) then return false end +function ildb.create_ts_from_tcb_list(sigd_list) + local id = advtrains.random_id(8) - 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 + while track_sections[id] do + id = advtrains.random_id(8) + end + atdebug("create_ts_from_tcb_list: sigd_list=",sigd_list, "new ID will be ",id) + + local tcbr = {} + -- makes a copy of the sigd list, for use in repair mechanisms where sigd may contain a tcbs field which we dont want + for _, sigd in ipairs(sigd_list) do + table.insert(tcbr, {p=sigd.p, s=sigd.s}) + local tcbs = sigd.tcbs or ildb.get_tcbs(sigd) + if tcbs.ts_id then + error("Trying to create TS with TCBS that is already assigned to other section") end + tcbs.ts_id = id end - advtrains.interlocking.show_tcb_marker(sigd.p) - if tcbs.signal then - return false + + local new_ts = { + tc_breaks = tcbr + } + track_sections[id] = new_ts + -- update the TCB markers + for _, sigd in ipairs(sigd_list) do + advtrains.interlocking.show_tcb_marker(sigd.p) end - return true + + + ildb.update_ts_cache(id) + return id, new_ts 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 + +-- Updates the turnout cache of the given track section +function ildb.update_ts_cache(ts_id) + local ts = ildb.get_ts(ts_id) + if not ts then + error("Update TS Cache called with nonexisting ts_id "..(ts_id or "nil")) end + local rscache = {} + -- start on every of the TS's TCBs, walk the track forward and store locks along the way + -- TODO: Need change in handling of switches + atdebug("update_ts_cache",ts_id,"TODO: implement") +end + +local lntrans = { "A", "B" } +local function sigd_to_string(sigd) + return minetest.pos_to_string(sigd.p).." / "..lntrans[sigd.s] +end + +-- Create a new TCB at the position and update/repair the adjoining sections +function ildb.create_tcb_at(pos) + atdebug("create_tcb_at",pos) + local pts = advtrains.encode_pos(pos) + track_circuit_breaks[pts] = {[1] = {}, [2] = {}} + local all_tcbs_1 = ildb.get_all_tcbs_adjacent(pos, 1) + atdebug("TCBs on A side",all_tcbs_1) + local all_tcbs_2 = ildb.get_all_tcbs_adjacent(pos, 2) + atdebug("TCBs on B side",all_tcbs_2) + -- perform TS repair + ildb.repair_ts_merge_all(all_tcbs_1) + ildb.repair_ts_merge_all(all_tcbs_2) +end + +-- Create a new TCB at the position and update/repair the now joined section +function ildb.remove_tcb_at(pos) + atdebug("remove_tcb_at",pos) + local pts = advtrains.encode_pos(pos) + local old_tcb = track_circuit_breaks[pts] track_circuit_breaks[pts] = nil + -- purge the track sections adjacent + if old_tcb[1].ts_id then + ildb.purge_ts_tcb_refs(old_tcb[1].ts_id) + end + if old_tcb[2].ts_id then + ildb.purge_ts_tcb_refs(old_tcb[2].ts_id) + end + advtrains.interlocking.remove_tcb_marker(pos) + -- If needed, merge the track sections here + ildb.check_and_repair_ts_at_pos(pos) 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) +function ildb.create_ts_from_tcbs(sigd) + atdebug("create_ts_from_tcbs",sigd) + local all_tcbs = ildb.get_all_tcbs_adjacent(sigd.p, sigd.s) + ildb.repair_ts_merge_all(all_tcbs, true) +end + +-- Remove the given track section, leaving its TCBs with no section assigned +function ildb.remove_ts(ts_id) + atdebug("remove_ts",ts_id) + local ts = track_sections[ts_id] + if not ts then + error("remove_ts: "..ts_id.." doesn't exist") end - -- Note: ts gets removed in the moment of the removal of the last TCB. - return true + while ts.tc_breaks[i] do + -- get TCBS + local sigd = ts.tc_breaks[i] + local tcbs = ildb.get_tcbs(sigd) + if tcbs then + atdebug("cleared TCB",sigd) + tcbs.ts_id = nil + else + atdebug("orphan TCB",sigd) + end + i = i+1 + end + track_sections[ts_id] = nil end -- Returns true if it is allowed to modify any property of a track section, such as @@ -457,38 +602,8 @@ function ildb.may_modify_tcbs(tcbs) 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 + +-- Signals/IP -- -- returns the sigd the signal at pos belongs to, if this is known diff --git a/advtrains_interlocking/tcb_ts_ui.lua b/advtrains_interlocking/tcb_ts_ui.lua index 0cc10da..97c28a8 100755 --- a/advtrains_interlocking/tcb_ts_ui.lua +++ b/advtrains_interlocking/tcb_ts_ui.lua @@ -88,12 +88,8 @@ minetest.register_node("advtrains_interlocking:tcb_node", { 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).") + if tcb[connid].signal then + minetest.chat_send_player(pname, "Can't remove TCB: Both sides must have no signal assigned!") return false end end @@ -105,15 +101,7 @@ minetest.register_node("advtrains_interlocking:tcb_node", { 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 + ildb.remove_tcb_at(tcbpos) end end, }) @@ -167,15 +155,13 @@ minetest.register_on_punchnode(function(pos, node, player, pointed_thing) 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.") + -- if there is already a tcb here, reassign it + if ildb.get_tcb(pos) then + minetest.chat_send_player(pname, "Configuring TCB: Already existed at this position, it is now linked to this TCB marker") + else + ildb.create_tcb_at(pos) 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)) @@ -234,22 +220,12 @@ local function mktcbformspec(tcbs, btnpref, offset, pname) 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.."label[0.5,"..offset..";Side "..btnpref..": "..minetest.formspec_escape(ts.name or tcbs.ts_id).."]" 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]" @@ -297,11 +273,7 @@ minetest.register_on_player_receive_fields(function(player, formname, fields) 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} @@ -312,29 +284,12 @@ minetest.register_on_player_receive_fields(function(player, formname, fields) 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) + ildb.create_ts_from_tcbs({p=pos, s=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.") @@ -357,10 +312,7 @@ end) -- TS Formspec --- textlist selection temporary storage -local ts_pselidx = {} - -function advtrains.interlocking.show_ts_form(ts_id, pname, sel_tcb) +function advtrains.interlocking.show_ts_form(ts_id, pname) if not minetest.check_player_privs(pname, "interlocking") then minetest.chat_send_player(pname, "Insufficient privileges to use this!") return @@ -369,7 +321,7 @@ function advtrains.interlocking.show_ts_form(ts_id, pname, sel_tcb) 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.."field[0.8,2;5.2,1;name;Section name;"..minetest.formspec_escape(ts.name or "").."]" form = form.."button[5.5,1.7;1,1;setname;Set]" local hint @@ -382,26 +334,8 @@ function advtrains.interlocking.show_ts_form(ts_id, pname, sel_tcb) 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.."button[5.5,4;4,1;remove;Remove 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 @@ -420,17 +354,12 @@ function advtrains.interlocking.show_ts_form(ts_id, pname, sel_tcb) 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 + + if 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 @@ -442,45 +371,15 @@ minetest.register_on_player_receive_fields(function(player, formname, fields) 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) + if fields.remove then + ildb.remove_ts(ts_id) minetest.close_formspec(pname, formname) return end @@ -489,7 +388,7 @@ minetest.register_on_player_receive_fields(function(player, formname, fields) if fields.setname then ts.name = fields.name if ts.name == "" then - ts.name = "Section "..ts_id + ts.name = nil end end @@ -566,7 +465,7 @@ function advtrains.interlocking.show_tcb_marker(pos) ts = ildb.get_ts(tcbs.ts_id) end if ts then - itex[connid] = ts.name + itex[connid] = ts.name or tcbs.ts_id or "???" else itex[connid] = "--EOI--" end @@ -589,6 +488,47 @@ function advtrains.interlocking.show_tcb_marker(pos) markerent[pts] = obj end +function advtrains.interlocking.remove_tcb_marker(pos) + local pts = advtrains.roundfloorpts(pos) + if markerent[pts] then + markerent[pts]:remove() + end + markerent[pts] = nil +end + +-- Spawns particles to highlight the clicked track section +-- TODO: Adapt behavior to not dumb-walk anymore +function advtrains.interlocking.highlight_track_section(pos) + local ti = advtrains.get_track_iterator(pos, nil, 100, true) + local pos, connid, bconnid, tcb + while ti:has_next_branch() do + pos, connid = ti:next_branch() + --atdebug("highlight_track_section: BRANCH: ",pos, connid) + bconnid = nil + repeat + -- spawn particles + minetest.add_particle({ + pos = pos, + velocity = {x=0, y=0, z=0}, + acceleration = {x=0, y=0, z=0}, + expirationtime = 10, + size = 7, + vertical = true, + texture = "at_il_ts_highlight_particle.png", + glow = 6, + }) + -- abort if TCB is found + tcb = ildb.get_tcb(pos) + if tcb then + advtrains.interlocking.show_tcb_marker(pos) + break + end + pos, connid, bconnid = ti:next_track() + --atdebug("highlight_track_section: TRACK: ",pos, connid, bconnid) + until not pos + end +end + -- Signalling formspec - set routes a.s.o -- textlist selection temporary storage diff --git a/advtrains_interlocking/textures/at_il_ts_highlight_particle.png b/advtrains_interlocking/textures/at_il_ts_highlight_particle.png new file mode 100644 index 0000000..4ba3622 Binary files /dev/null and b/advtrains_interlocking/textures/at_il_ts_highlight_particle.png differ diff --git a/advtrains_interlocking/tool.lua b/advtrains_interlocking/tool.lua index 5d38b3a..4b701b4 100644 --- a/advtrains_interlocking/tool.lua +++ b/advtrains_interlocking/tool.lua @@ -3,14 +3,64 @@ local ilrs = advtrains.interlocking.route +local function node_right_click(pos, pname) + 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) + return + end + + -- If not a turnout, check the track section and show a form + local node_ok, conns, rail_y=advtrains.get_rail_info_at(pos) + if not node_ok then + minetest.chat_send_player(pname, "Node is not a track!") + return + end + + local ts_id = advtrains.interlocking.db.check_and_repair_ts_at_pos(pos) + if ts_id then + advtrains.interlocking.show_ts_form(ts_id, pname) + else + minetest.chat_send_player(pname, "No track section at this location!") + end +end + +local function node_left_click(pos, pname) + local node_ok, conns, rail_y=advtrains.get_rail_info_at(pos) + if not node_ok then + minetest.chat_send_player(pname, "Node is not a track!") + return + end + + local ts_id = advtrains.interlocking.db.check_and_repair_ts_at_pos(pos) + if ts_id then + advtrains.interlocking.highlight_track_section(pos) + else + minetest.chat_send_player(pname, "No track section at this location!") + end +end + + minetest.register_craftitem("advtrains_interlocking:tool",{ - description = "Interlocking tool\nright-click turnouts to inspect route locks", + description = "Interlocking tool\nPunch: Highlight track section\nPlace: check route locks/show track section info", 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() + on_place = function(itemstack, player, pointed_thing) + local pname = player:get_player_name() if not pname then return end @@ -20,27 +70,23 @@ minetest.register_craftitem("advtrains_interlocking:tool",{ 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 + node_right_click(pos, pname) end end, + on_use = function(itemstack, player, pointed_thing) + local pname = player: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 + node_left_click(pos, pname) + end + end }) minetest.register_on_player_receive_fields(function(player, formname, fields) -- cgit v1.2.3