547 lines
17 KiB
Lua
547 lines
17 KiB
Lua
-- Signal API implementation
|
|
|
|
|
|
--[[
|
|
Signal aspect table:
|
|
asp = {
|
|
main = {
|
|
free = <boolean>,
|
|
speed = <int km/h>,
|
|
},
|
|
shunt = {
|
|
free = <boolean>,
|
|
-- Whether train may proceed as shunt move, on sight
|
|
-- main aspect takes precedence over this
|
|
proceed_as_main = <boolean>,
|
|
-- If an approaching train is a shunt move and "main.free" is set,
|
|
-- the train may proceed as a train move under the "main" aspect
|
|
-- If this is not set, shunt moves are NOT allowed to switch to
|
|
-- a train move, and must stop even if "main.free" is set.
|
|
-- This is intended to be used for "Halt for shunt moves" signs.
|
|
}
|
|
dst = {
|
|
free = <boolean>,
|
|
speed = <int km/h>,
|
|
}
|
|
info = {
|
|
call_on = <boolean>, -- Call-on route, expect train in track ahead (not implemented yet)
|
|
dead_end = <boolean>, -- Route ends on a dead end (e.g. bumper) (not implemented yet)
|
|
w_speed = <integer>,
|
|
-- "Warning speed restriction". Supposed for short-term speed
|
|
-- restrictions which always override any other restrictions
|
|
-- imposed by "speed" fields, until lifted by a value of -1
|
|
-- (Example: german Langsamfahrstellen-Signale)
|
|
}
|
|
}
|
|
-- For "speed" and "w_speed" fields, a value of -1 means that the
|
|
-- restriction is lifted. If they are omitted, the value imposed at
|
|
-- the last aspect received remains valid.
|
|
-- The "dst" subtable can be completely omitted when no explicit dst
|
|
-- aspect should be signalled to the train. In this case, the last
|
|
-- signalled dst aspect remains valid.
|
|
|
|
== How signals actually work in here ==
|
|
Each signal (in the advtrains universe) is some node that has at least the
|
|
following things:
|
|
- An "influence point" that is set somewhere on a rail
|
|
- An aspect which trains that pass the "influence point" have to obey
|
|
|
|
There can be static and dynamic signals. Static signals are, roughly
|
|
spoken, signs, while dynamic signals are "real" signals which can display
|
|
different things.
|
|
|
|
The node definition of a signal node should contain those fields:
|
|
groups = {
|
|
advtrains_signal = 2,
|
|
save_in_at_nodedb = 1,
|
|
}
|
|
advtrains = {
|
|
set_aspect = function(pos, node, asp)
|
|
-- This function gets called whenever the signal should display
|
|
-- a new or changed signal aspect. It is not required that
|
|
-- the signal actually displays the exact same aspect, since
|
|
-- some signals can not do this by design.
|
|
-- Example: pure shunt signals can not display a "main" aspect
|
|
-- and have no effect on train moves, so they will only ever
|
|
-- honor the shunt.free field for their aspect.
|
|
-- In turn, it is not guaranteed that the aspect will fulfill the
|
|
-- criteria put down in supported_aspects.
|
|
-- If set_aspect is present, supported_aspects should also be declared.
|
|
|
|
-- The aspect passed in here can always be queried using the
|
|
-- advtrains.interlocking.signal_get_supposed_aspect(pos) function.
|
|
-- It is always DANGER when the signal is not used as route signal.
|
|
|
|
-- For static signals, this function should be completely omitted
|
|
-- If this function is omitted, it won't be possible to use
|
|
-- route setting on this signal.
|
|
end,
|
|
supported_aspects = {
|
|
-- A table which tells which different types of aspects this signal
|
|
-- is able to display. It is used to construct the "aspect editing"
|
|
-- formspec for route programming (and others) It should always be
|
|
-- present alongside with set_aspect. If this is not specified but
|
|
-- set_aspect is, the user will be allowed to select any aspect.
|
|
-- Any of the fields marked with <boolean/nil> support 3 types of values:
|
|
nil: if this signal can switch between free/blocked
|
|
false: always shows "blocked", unchangable
|
|
true: always shows "free", unchangable
|
|
-- Any of the "speed" fields should contain a list of possible values
|
|
-- to be set as restriction. If omitted, this signal should never
|
|
-- set the corresponding "speed" field in the aspect, which means
|
|
-- that the previous speed limit stays valid
|
|
-- If your signal can only display a single speed (may it be -1),
|
|
-- always enclose that single value into a list. (such as {-1})
|
|
main = {
|
|
free = <boolean/nil>,
|
|
speed = {<speed1>, ..., <speedn>} or nil,
|
|
},
|
|
dst = {
|
|
free = <boolean/nil>,
|
|
speed = {<speed1>, ..., <speedn>} or nil,
|
|
},
|
|
shunt = {
|
|
free = <boolean/nil>,
|
|
},
|
|
info = {
|
|
call_on = <boolean/nil>,
|
|
dead_end = <boolean/nil>,
|
|
w_speed = {<speed1>, ..., <speedn>} or nil,
|
|
}
|
|
|
|
},
|
|
get_aspect = function(pos, node)
|
|
-- This function gets called by the train safety system. It
|
|
should return the aspect that this signal actually displays,
|
|
not preferably the input of set_aspect.
|
|
-- For regular, full-featured light signals, they will probably
|
|
honor all entries in the original aspect, however, e.g.
|
|
simple shunt signals always return main.free=true regardless of
|
|
the set_aspect input because they can not signal "Halt" to
|
|
train moves.
|
|
-- advtrains.interlocking.DANGER contains a default "all-danger" aspect.
|
|
-- If your signal does not cover certain sub-tables of the aspect,
|
|
the following reasonable defaults are automatically assumed:
|
|
main = {
|
|
free = true,
|
|
}
|
|
dst = {
|
|
free = true,
|
|
}
|
|
shunt = {
|
|
free = false,
|
|
proceed_as_main = false,
|
|
}
|
|
end,
|
|
}
|
|
on_rightclick = advtrains.interlocking.signal_rc_handler
|
|
can_dig = advtrains.interlocking.signal_can_dig
|
|
after_dig_node = advtrains.interlocking.signal_after_dig
|
|
|
|
(If you need to specify custom can_dig or after_dig_node callbacks,
|
|
please call those functions anyway!)
|
|
|
|
Important note: If your signal should support external ways to set its
|
|
aspect (e.g. via mesecons), there are some things that need to be considered:
|
|
- advtrains.interlocking.signal_get_supposed_aspect(pos) won't respect this
|
|
- Whenever you change the signal aspect, and that aspect change
|
|
did not happen through a call to
|
|
advtrains.interlocking.signal_set_aspect(pos, asp), you are
|
|
*required* to call this function:
|
|
advtrains.interlocking.signal_on_aspect_changed(pos)
|
|
in order to notify trains about the aspect change.
|
|
This function will query get_aspect to retrieve the new aspect.
|
|
|
|
]]--
|
|
|
|
local DANGER = {
|
|
main = {
|
|
free = false,
|
|
speed = 0,
|
|
},
|
|
shunt = {
|
|
free = false,
|
|
},
|
|
dst = {
|
|
free = false,
|
|
speed = 0,
|
|
},
|
|
info = {}
|
|
}
|
|
advtrains.interlocking.DANGER = DANGER
|
|
|
|
local function fillout_aspect(asp)
|
|
if not asp.main then
|
|
asp.main = {
|
|
free = true,
|
|
}
|
|
elseif type(asp.main) ~= "table" then
|
|
asp.main = {
|
|
free = asp.main~=0,
|
|
speed = asp.main,
|
|
}
|
|
end
|
|
if not asp.dst then
|
|
asp.dst = {
|
|
free = true,
|
|
}
|
|
end
|
|
if not asp.shunt then
|
|
asp.shunt = {
|
|
free = false,
|
|
proceed_as_main = false,
|
|
}
|
|
elseif type(asp.shunt) ~= "table" then
|
|
asp.shunt = {
|
|
free = asp.shunt,
|
|
proceed_as_main = asp.proceed_as_main,
|
|
}
|
|
end
|
|
if not asp.info then
|
|
asp.info = {}
|
|
end
|
|
end
|
|
|
|
function advtrains.interlocking.update_signal_aspect(tcbs)
|
|
if tcbs.signal then
|
|
local asp = tcbs.aspect or DANGER
|
|
advtrains.interlocking.signal_set_aspect(tcbs.signal, asp)
|
|
end
|
|
end
|
|
|
|
function advtrains.interlocking.signal_can_dig(pos)
|
|
return not advtrains.interlocking.db.get_sigd_for_signal(pos)
|
|
end
|
|
|
|
function advtrains.interlocking.signal_after_dig(pos)
|
|
-- clear influence point
|
|
advtrains.interlocking.db.clear_ip_by_signalpos(pos)
|
|
end
|
|
|
|
function advtrains.interlocking.signal_set_aspect(pos, asp)
|
|
fillout_aspect(asp)
|
|
local node=advtrains.ndb.get_node(pos)
|
|
local ndef=minetest.registered_nodes[node.name]
|
|
if ndef and ndef.advtrains and ndef.advtrains.set_aspect then
|
|
ndef.advtrains.set_aspect(pos, node, asp)
|
|
advtrains.interlocking.signal_on_aspect_changed(pos)
|
|
end
|
|
end
|
|
|
|
-- should be called when aspect has changed on this signal.
|
|
function advtrains.interlocking.signal_on_aspect_changed(pos)
|
|
local ipts, iconn = advtrains.interlocking.db.get_ip_by_signalpos(pos)
|
|
if not ipts then return end
|
|
local ipos = minetest.string_to_pos(ipts)
|
|
|
|
local tns = advtrains.occ.get_trains_over(ipos)
|
|
for id, sidx in pairs(tns) do
|
|
-- local train = advtrains.trains[id]
|
|
--if train.index <= sidx then
|
|
minetest.after(0, advtrains.invalidate_path, id)
|
|
--end
|
|
end
|
|
end
|
|
|
|
function advtrains.interlocking.signal_rc_handler(pos, node, player, itemstack, pointed_thing)
|
|
local pname = player:get_player_name()
|
|
local sigd = advtrains.interlocking.db.get_sigd_for_signal(pos)
|
|
if sigd then
|
|
advtrains.interlocking.show_signalling_form(sigd, pname)
|
|
else
|
|
local ndef = minetest.registered_nodes[node.name]
|
|
if ndef.advtrains and ndef.advtrains.set_aspect then
|
|
-- permit to set aspect manually
|
|
minetest.show_formspec(pname, "at_il_sigasp_"..minetest.pos_to_string(pos), "field[aspect;Set Aspect ('A' to assign IP);D0D0D]")
|
|
else
|
|
--static signal - only IP
|
|
advtrains.interlocking.show_ip_form(pos, pname)
|
|
end
|
|
end
|
|
end
|
|
|
|
minetest.register_on_player_receive_fields(function(player, formname, fields)
|
|
local pname = player:get_player_name()
|
|
local pts = string.match(formname, "^at_il_sigasp_(.+)$")
|
|
local pos
|
|
if pts then pos = minetest.string_to_pos(pts) end
|
|
if pos and fields.aspect then
|
|
if fields.aspect == "A" then
|
|
advtrains.interlocking.show_ip_form(pos, pname)
|
|
return
|
|
end
|
|
local mfs, msps, dfs, dsps, shs = string.match(fields.aspect, "^([FD])([-0-9]+)([FD])([-0-9]+)([FD])$")
|
|
local asp = {
|
|
main = {
|
|
free = mfs=="F",
|
|
speed = tonumber(msps),
|
|
},
|
|
shunt = {
|
|
free = shs=="F",
|
|
},
|
|
dst = {
|
|
free = dfs=="F",
|
|
speed = tonumber(dsps),
|
|
},
|
|
info = {
|
|
call_on = false, -- Call-on route, expect train in track ahead
|
|
dead_end = false, -- Route ends on a dead end (e.g. bumper)
|
|
}
|
|
}
|
|
advtrains.interlocking.signal_set_aspect(pos, asp)
|
|
end
|
|
end)
|
|
|
|
-- Returns the aspect the signal at pos is supposed to show
|
|
function advtrains.interlocking.signal_get_supposed_aspect(pos)
|
|
local sigd = advtrains.interlocking.db.get_sigd_for_signal(pos)
|
|
if sigd then
|
|
local tcbs = advtrains.interlocking.db.get_tcbs(sigd)
|
|
if tcbs.aspect then
|
|
return tcbs.aspect
|
|
end
|
|
end
|
|
return DANGER;
|
|
end
|
|
|
|
-- Returns the actual aspect of the signal at position, as returned by the nodedef.
|
|
-- returns nil when there's no signal at the position
|
|
function advtrains.interlocking.signal_get_aspect(pos)
|
|
local node=advtrains.ndb.get_node(pos)
|
|
local ndef=minetest.registered_nodes[node.name]
|
|
if ndef and ndef.advtrains and ndef.advtrains.get_aspect then
|
|
local asp = ndef.advtrains.get_aspect(pos, node)
|
|
if not asp then asp = DANGER end
|
|
fillout_aspect(asp)
|
|
return asp
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- Returns the "supported_aspects" of the signal at position, as returned by the nodedef.
|
|
-- returns nil when there's no signal at the position
|
|
function advtrains.interlocking.signal_get_supported_aspects(pos)
|
|
local node=advtrains.ndb.get_node(pos)
|
|
local ndef=minetest.registered_nodes[node.name]
|
|
if ndef and ndef.advtrains and ndef.advtrains.supported_aspects then
|
|
local asp = ndef.advtrains.supported_aspects
|
|
return asp
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local players_assign_ip = {}
|
|
|
|
local function ipmarker(ipos, connid)
|
|
local node_ok, conns, rhe = advtrains.get_rail_info_at(ipos, advtrains.all_tracktypes)
|
|
if not node_ok then return end
|
|
local yaw = advtrains.dir_to_angle(conns[connid].c)
|
|
|
|
-- using tcbmarker here
|
|
local obj = minetest.add_entity(vector.add(ipos, {x=0, y=0.2, z=0}), "advtrains_interlocking:tcbmarker")
|
|
if not obj then return end
|
|
obj:set_yaw(yaw)
|
|
obj:set_properties({
|
|
textures = { "at_il_signal_ip.png" },
|
|
})
|
|
end
|
|
|
|
-- shows small info form for signal IP state/assignment
|
|
-- only_notset: show only if it is not set yet (used by signal tcb assignment)
|
|
function advtrains.interlocking.show_ip_form(pos, pname, only_notset)
|
|
if not minetest.check_player_privs(pname, "interlocking") then
|
|
return
|
|
end
|
|
local form = "size[7,5]label[0.5,0.5;Signal at "..minetest.pos_to_string(pos).."]"
|
|
local pts, connid = advtrains.interlocking.db.get_ip_by_signalpos(pos)
|
|
if pts then
|
|
form = form.."label[0.5,1.5;Influence point is set at "..pts.."/"..connid.."]"
|
|
form = form.."button_exit[0.5,2.5; 5,1;set;Move]"
|
|
form = form.."button_exit[0.5,3.5; 5,1;clear;Clear]"
|
|
local ipos = minetest.string_to_pos(pts)
|
|
ipmarker(ipos, connid)
|
|
else
|
|
form = form.."label[0.5,1.5;Influence point is not set.]"
|
|
form = form.."label[0.5,2.0;It is recommended to set an influence point.]"
|
|
form = form.."label[0.5,2.5;This is the point where trains will obey the signal.]"
|
|
|
|
form = form.."button_exit[0.5,3.5; 5,1;set;Set]"
|
|
end
|
|
if not only_notset or not pts then
|
|
minetest.show_formspec(pname, "at_il_ipassign_"..minetest.pos_to_string(pos), form)
|
|
end
|
|
end
|
|
|
|
minetest.register_on_player_receive_fields(function(player, formname, fields)
|
|
local pname = player:get_player_name()
|
|
if not minetest.check_player_privs(pname, {train_operator=true, interlocking=true}) then
|
|
return
|
|
end
|
|
local pts = string.match(formname, "^at_il_ipassign_([^_]+)$")
|
|
local pos
|
|
if pts then
|
|
pos = minetest.string_to_pos(pts)
|
|
end
|
|
if pos then
|
|
if fields.set then
|
|
advtrains.interlocking.signal_init_ip_assign(pos, pname)
|
|
elseif fields.clear then
|
|
advtrains.interlocking.db.clear_ip_by_signalpos(pos)
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- inits the signal IP assignment process
|
|
function advtrains.interlocking.signal_init_ip_assign(pos, pname)
|
|
if not minetest.check_player_privs(pname, "interlocking") then
|
|
minetest.chat_send_player(pname, "Insufficient privileges to use this!")
|
|
return
|
|
end
|
|
--remove old IP
|
|
--advtrains.interlocking.db.clear_ip_by_signalpos(pos)
|
|
minetest.chat_send_player(pname, "Configuring Signal: Please look in train's driving direction and punch rail to set influence point.")
|
|
|
|
players_assign_ip[pname] = pos
|
|
end
|
|
|
|
minetest.register_on_punchnode(function(pos, node, player, pointed_thing)
|
|
local pname = player:get_player_name()
|
|
if not minetest.check_player_privs(pname, "interlocking") then
|
|
return
|
|
end
|
|
-- IP assignment
|
|
local signalpos = players_assign_ip[pname]
|
|
if signalpos then
|
|
if vector.distance(pos, signalpos)<=50 then
|
|
local node_ok, conns, rhe = advtrains.get_rail_info_at(pos, advtrains.all_tracktypes)
|
|
if node_ok and #conns == 2 then
|
|
|
|
local yaw = player:get_look_horizontal()
|
|
local plconnid = advtrains.yawToClosestConn(yaw, conns)
|
|
|
|
-- add assignment if not already present.
|
|
local pts = advtrains.roundfloorpts(pos)
|
|
if not advtrains.interlocking.db.get_ip_signal_asp(pts, plconnid) then
|
|
advtrains.interlocking.db.set_ip_signal(pts, plconnid, signalpos)
|
|
ipmarker(pos, plconnid)
|
|
minetest.chat_send_player(pname, "Configuring Signal: Successfully set influence point")
|
|
else
|
|
minetest.chat_send_player(pname, "Configuring Signal: Influence point of another signal is already present!")
|
|
end
|
|
else
|
|
minetest.chat_send_player(pname, "Configuring Signal: This is not a normal two-connection rail! Aborted.")
|
|
end
|
|
else
|
|
minetest.chat_send_player(pname, "Configuring Signal: Node is too far away. Aborted.")
|
|
end
|
|
players_assign_ip[pname] = nil
|
|
end
|
|
end)
|
|
|
|
|
|
--== aspect selector ==--
|
|
|
|
local players_aspsel = {}
|
|
|
|
--[[
|
|
suppasp: "supported_aspects" table
|
|
purpose: form title string
|
|
callback: func(pname, aspect) called on form submit
|
|
]]
|
|
function advtrains.interlocking.show_signal_aspect_selector(pname, p_suppasp, p_purpose, callback, p_isasp)
|
|
local suppasp = p_suppasp or {
|
|
main = {}, dst = {}, shunt = {}, info = {},
|
|
}
|
|
local purpose = p_purpose or ""
|
|
local isasp = p_isasp and fillout_aspect(p_isasp)
|
|
|
|
local form = "size[7,5]label[0.5,0.5;Select Signal Aspect:]"
|
|
form = form.."label[0.5,1;"..purpose.."]"
|
|
|
|
form = form.."label[0.5,1.5;== Main Signal ==]"
|
|
if suppasp.main.free == nil then
|
|
local st = 2
|
|
if isasp and not isasp.main.free then st=1 end
|
|
form = form.."dropdown[0.5,2;2;main_free;danger,free;"..st.."]"
|
|
end
|
|
if suppasp.main.speed then
|
|
local selid = 1
|
|
if isasp and isasp.main.speed then
|
|
for idx, spv in ipairs(suppasp.main.speed) do
|
|
if spv == isasp.main.speed then
|
|
selid = idx
|
|
break
|
|
end
|
|
end
|
|
end
|
|
form = form.."label[2.3,1;Speed:]"
|
|
form = form.."dropdown[3,2;2;main_speed;"..table.concat(suppasp.main.speed, ",")..";"..selid.."]"
|
|
end
|
|
|
|
form = form.."label[0.5,3;== Shunting ==]"
|
|
if suppasp.shunt.free == nil then
|
|
local st = 1
|
|
if isasp and isasp.shunt.free then st=2 end
|
|
form = form.."dropdown[0.5,3.5;2;shunt_free;---,allowed;"..st.."]"
|
|
end
|
|
|
|
form = form.."button_exit[0.5,4.5; 5,1;save;OK]"
|
|
|
|
local token = advtrains.random_id()
|
|
|
|
minetest.show_formspec(pname, "at_il_sigaspdia_"..token, form)
|
|
|
|
minetest.after(1, function()
|
|
players_aspsel[pname] = {
|
|
suppasp = suppasp,
|
|
callback = callback,
|
|
token = token,
|
|
}
|
|
end)
|
|
end
|
|
|
|
local function usebool(sup, val, free)
|
|
if sup == nil then
|
|
return val==free
|
|
else
|
|
return sup
|
|
end
|
|
end
|
|
local function usespeed(sup, val)
|
|
if sup then
|
|
return tonumber(val)
|
|
else
|
|
return nil
|
|
end
|
|
end
|
|
|
|
-- TODO use non-hacky way to parse outputs
|
|
|
|
minetest.register_on_player_receive_fields(function(player, formname, fields)
|
|
local pname = player:get_player_name()
|
|
local psl = players_aspsel[pname]
|
|
if psl then
|
|
if formname == "at_il_sigaspdia_"..psl.token then
|
|
if fields.save then
|
|
local asp = {
|
|
main = {
|
|
free = usebool(psl.suppasp.main.free, fields.main_free, "free"),
|
|
speed = usespeed(psl.suppasp.main.speed, fields.main_speed),
|
|
},
|
|
dst = {
|
|
free = true, speed = -1,
|
|
},
|
|
shunt = {
|
|
free = usebool(psl.suppasp.shunt.free, fields.shunt_free, "allowed"),
|
|
},
|
|
info = {}
|
|
}
|
|
psl.callback(pname, asp)
|
|
end
|
|
else
|
|
players_aspsel[pname] = nil
|
|
end
|
|
end
|
|
|
|
end)
|