562 lines
18 KiB
Lua
562 lines
18 KiB
Lua
-- 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, default_route)
|
|
if not minetest.check_player_privs(pname, "interlocking") then
|
|
minetest.chat_send_player(pname, "Insufficient privileges to use this!")
|
|
return
|
|
end
|
|
local rp = {
|
|
origin = sigd,
|
|
}
|
|
if default_route then
|
|
rp.route = table.copy(default_route)
|
|
|
|
-- "Step back one section", but keeping turnouts
|
|
local last_route = rp.route[#rp.route]
|
|
if last_route then
|
|
rp.tmp_lcks = last_route.locks
|
|
rp.route[#rp.route] = nil
|
|
end
|
|
rp.route.name = "PROG["..pname.."]"
|
|
else
|
|
rp.route = {
|
|
name = "PROG["..pname.."]"
|
|
}
|
|
rp.tmp_lcks = {}
|
|
end
|
|
player_rte_prog[pname] = rp
|
|
advtrains.interlocking.visualize_route(sigd, rp.route, "prog_"..pname, rp.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
|