From 5992618ee837351124f59c1c0289391a9d501ade Mon Sep 17 00:00:00 2001 From: orwell96 Date: Wed, 20 Jun 2018 20:13:12 +0200 Subject: [PATCH] Add Track Circuit Breaks (TCBs), Database and Track Circuit Setup Does not get saved yet. --- advtrains/helpers.lua | 4 + advtrains_interlocking/database.lua | 192 ++++++++++++++++++++++++++++ advtrains_interlocking/init.lua | 1 + advtrains_interlocking/tcb.lua | 180 ++++++++++++++++++++++++++ 4 files changed, 377 insertions(+) create mode 100644 advtrains_interlocking/tcb.lua diff --git a/advtrains/helpers.lua b/advtrains/helpers.lua index ce059de..2160444 100644 --- a/advtrains/helpers.lua +++ b/advtrains/helpers.lua @@ -334,4 +334,8 @@ function advtrains.random_id() end return idst end +-- Shorthand for pos_to_string and round_vector_floor_y +function advtrains.roundfloorpts(pos) + return minetest.pos_to_string(advtrains.round_vector_floor_y(pos)) +end diff --git a/advtrains_interlocking/database.lua b/advtrains_interlocking/database.lua index 61bf5ad..e318dd2 100644 --- a/advtrains_interlocking/database.lua +++ b/advtrains_interlocking/database.lua @@ -32,6 +32,9 @@ Route setting fails whenever any TC that we want to set ROUTE to is already set 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 @@ -50,7 +53,196 @@ CALL_ON_ALLOWED - Whether this TC being blocked (TRAIN or ROUTE) does not preven == 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 +== 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 = 100 + + +local ildb = {} + +local track_circuit_breaks = {} + +function ildb.load(data) + +end + +function ildb.save() + return {} +end + +-- +--[[ +TCB data structure +{ +[1] = { -- Variant: with adjacent TCs. + == Synchronized properties == Apply to the whole TC + adjacent = { ,... } -- Adjacent TCBs, forms a TC with these + conflict = { ,... } -- Conflicting TC's (chosen as a representative TCB member) + -- Used e.g. for crossing rails that do not have nodes in common (like it's currently done) + incomplete = -- Set when the recursion counter hit during traverse. Probably needs to add + -- another tcb at some far-away place + route = {origin = , in_dir = } + -- Set whenever a route has been set through this TC. It saves the origin tcb id and side + -- (=the origin signal). in_dir is set when the train will enter the TC from this side + + == Unsynchronized properties == Apply only to this side of the TC + signal = -- optional: when set, routes can be set from this tcb/direction and signal + -- aspect will be set accordingly. + routetar = -- 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 = -- 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 +}, +[2] = { -- Variant: end of track-circuited area (initial state of TC) + end_of_interlocking = true, + section_free = , --this can be set by an exit node via mesecons or atlatc, + -- or from the tc formspec. +} +} +Signal specifier (a pair of TCB/Side): +{p = , s = <1/2>} +]] + + +-- +function ildb.create_tcb(pos) + local new_tcb = { + [1] = {end_of_interlocking = true}, + [2] = {end_of_interlocking = true}, + } + local pts = advtrains.roundfloorpts(pos) + track_circuit_breaks[pts] = new_tcb +end + +function ildb.get_tcb(pos) + local pts = advtrains.roundfloorpts(pos) + return track_circuit_breaks[pts] +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) + 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 + -- end of track + 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 + table.insert(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,"found tcb's:",found_tcbs) + 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, hit_counter) + end + end + return counter_hit +end + +local function sigd_equal(sigd, cmp) + return vector.equals(sigd.p, cmp.p) and sigd.s==cmp.s +end + + + + + +-- Updates the neighbors of this TCB using the traverser function (see comments above) +-- returns true if the traverser hit the counter, which means that there could be another +-- TCB outside of the traversed range. +function ildb.update_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 + error("update_tcb_neighbors but node is NOK: "..minetest.pos_to_string(pos)) + end + + local counter_hit = traverser(found_tcbs, pos, conns, connid, 0, hit_counter) + + for idx, sigd in pairs(found_tcbs) do + local tcb = ildb.get_tcb(sigd.p) + local tcbs = tcb[sigd.s] + + tcbs.end_of_interlocking = nil + tcbs.incomplete = counter_hit + tcbs.adjacent = {} + + for idx2, other_sigd in pairs(found_tcbs) do + if idx~=idx2 then + ildb.add_adjacent(tcbs, sigd.p, sigd.s, other_sigd) + end + end + end + + return hit_counter +end + +-- Add the adjacency entry into the tcbs, but without duplicating it +-- and without adding a self-reference +function ildb.add_adjacent(tcbs, this_pos, this_connid, sigd) + if sigd_equal(sigd, {p=this_pos, s=this_connid}) then + return + end + tcbs.end_of_interlocking = nil + if not tcbs.adjacent then + tcbs.adjacent = {} + end + for idx, cmp in pairs(tcbs.adjacent) do + if sigd_equal(sigd, cmp) then + return + end + end + table.insert(tcbs.adjacent, sigd) +end + +advtrains.interlocking.db = ildb + + diff --git a/advtrains_interlocking/init.lua b/advtrains_interlocking/init.lua index ab79573..e3be234 100644 --- a/advtrains_interlocking/init.lua +++ b/advtrains_interlocking/init.lua @@ -6,5 +6,6 @@ advtrains.interlocking = {} local modpath = minetest.get_modpath(minetest.get_current_modname()) .. DIR_DELIM dofile(modpath.."database.lua") +dofile(modpath.."tcb.lua") dofile(modpath.."signal_api.lua") dofile(modpath.."demosignals.lua") diff --git a/advtrains_interlocking/tcb.lua b/advtrains_interlocking/tcb.lua new file mode 100644 index 0000000..742bb62 --- /dev/null +++ b/advtrains_interlocking/tcb.lua @@ -0,0 +1,180 @@ +-- Track Circuit Breaks - Player interaction + +local players_assign_tcb = {} +local players_addfar_tcb = {} + +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 = true, + selection_box = { + type = "fixed", + fixed = {-1/4, -1/2, -1/4, 1/4, 1/2, 1/4}, + }, + 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, + }, + 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 meta = minetest.get_meta(pos) + local tcbpts = meta:get_string("tcb_pos") + local pname = player:get_player_name() + if tcbpts ~= "" then + local tcbpos = minetest.string_to_pos(tcbpts) + advtrains.interlocking.show_tcb_form(tcbpos, pname) + else + --unconfigured + --TODO security + 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) + atwarn("Would show tcb marker.") + -- TODO TCB-Marker anzeigen + end, +}) + +minetest.register_on_punchnode(function(pos, node, player, pointed_thing) + local pname = player:get_player_name() + 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 + advtrains.interlocking.db.create_tcb(pos) + + advtrains.interlocking.db.update_tcb_neighbors(pos, 1) + advtrains.interlocking.db.update_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 +end) + + +local function mkformspec(tcbs, btnpref, offset, pname) + local form = "label[0.5,"..offset..";Side "..btnpref..": "..(tcbs.end_of_interlocking and "End of interlocking" or "Track Circuit").."]" + if tcbs.end_of_interlocking then + form = form.."button[0.5,"..(offset+1)..";3,1;"..btnpref.."_clearadj;Activate Interlocking]" + if tcbs.section_free then + form = form.."button[4.5,"..(offset+1)..";3,1;"..btnpref.."_setlocked;Section is free]" + else + form = form.."button[4.5,"..(offset+1)..";3,1;"..btnpref.."_setfree;Section is blocked]" + end + else + local strtab = {} + for idx, sigd in ipairs(tcbs.adjacent) do + strtab[idx] = minetest.formspec_escape(sigd_to_string(sigd)) + end + form = form.."textlist[0.5,"..(offset+1)..";5,3;"..btnpref.."_adjlist;"..table.concat(strtab, ",").."]" + if players_addfar_tcb[pname] then + local sigd = players_addfar_tcb[pname] + form = form.."button[5.5,"..(offset+2)..";2.5,1;"..btnpref.."_addadj;Link TCB to "..sigd_to_string(sigd).."]" + form = form.."button[8,"..(offset+2)..";0.5,1;"..btnpref.."_canceladdadj;X]" + else + form = form.."button[5.5,"..(offset+2)..";3,1;"..btnpref.."_addadj;Add far TCB]" + end + form = form.."button[5.5,"..(offset+1)..";3,1;"..btnpref.."_clearadj;Clear&Update]" + form = form.."button[5.5,"..(offset+3)..";3,1;"..btnpref.."_mknonint;Make non-interlocked]" + if tcbs.incomplete then + form = form.."label[0.5,"..(offset+0.5)..";Warning: You possibly need to add TCBs manually!]" + end + end + return form +end + + + +function advtrains.interlocking.show_tcb_form(pos, pname) + local tcb = advtrains.interlocking.db.get_tcb(pos) + if not tcb then return end + + local form = "size[10,10] label[0.5,0.5;Track Circuit Break Configuration]" + form = form .. mkformspec(tcb[1], "A", 1, pname) + form = form .. mkformspec(tcb[2], "B", 6, pname) + + minetest.show_formspec(pname, "at_il_tcbconfig_"..minetest.pos_to_string(pos), form) +end + + +minetest.register_on_player_receive_fields(function(player, formname, fields) + local pname = player:get_player_name() + 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 = advtrains.interlocking.db.get_tcb(pos) + if not tcb then return end + local f_clearadj = {fields.A_clearadj, fields.B_clearadj} + local f_addadj = {fields.A_addadj, fields.B_addadj} + local f_canceladdadj = {fields.A_canceladdadj, fields.B_canceladdadj} + local f_setlocked = {fields.A_setlocked, fields.B_setlocked} + local f_setfree = {fields.A_setfree, fields.B_setfree} + local f_mknonint = {fields.A_mknonint, fields.B_mknonint} + + for connid=1,2 do + if f_clearadj[connid] then + advtrains.interlocking.db.update_tcb_neighbors(pos, connid) + end + if f_mknonint[connid] then + --TODO: remove this from the other tcb's + tcb[connid].end_of_interlocking = true + end + if f_addadj[connid] then + if players_addfar_tcb[pname] then + local sigd = players_addfar_tcb[pname] + advtrains.interlocking.db.add_adjacent(tcb[connid], pos, connid, sigd) + players_addfar_tcb[pname] = nil + else + players_addfar_tcb[pname] = {p = pos, s = connid} + end + end + if f_canceladdadj[connid] then + players_addfar_tcb[pname] = nil + end + if f_setfree[connid] then + tcb[connid].section_free = true + end + if f_setlocked[connid] then + tcb[connid].section_free = nil + end + end + advtrains.interlocking.show_tcb_form(pos, pname) + end + +end) + + + + +