diff options
Diffstat (limited to 'advtrains_interlocking/route_prog.lua')
-rw-r--r-- | advtrains_interlocking/route_prog.lua | 549 |
1 files changed, 549 insertions, 0 deletions
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 |