aboutsummaryrefslogtreecommitdiff
path: root/advtrains_interlocking
diff options
context:
space:
mode:
Diffstat (limited to 'advtrains_interlocking')
-rw-r--r--advtrains_interlocking/README.md85
-rw-r--r--advtrains_interlocking/aspect.lua296
-rw-r--r--advtrains_interlocking/database.lua9
-rw-r--r--advtrains_interlocking/demosignals.lua97
-rw-r--r--advtrains_interlocking/distant_ui.lua141
-rw-r--r--advtrains_interlocking/init.lua8
-rw-r--r--advtrains_interlocking/route_ui.lua18
-rw-r--r--advtrains_interlocking/routesetting.lua62
-rw-r--r--advtrains_interlocking/signal_api.lua833
-rw-r--r--advtrains_interlocking/signal_aspect_ui.lua237
-rw-r--r--advtrains_interlocking/spec/basic_signalling_spec.lua106
l---------advtrains_interlocking/spec/fixtures/advtrains_helpers.lua1
-rw-r--r--advtrains_interlocking/spec/mineunit.conf0
-rw-r--r--advtrains_interlocking/spec/signal_group_spec.lua95
-rwxr-xr-xadvtrains_interlocking/tcb_ts_ui.lua30
-rw-r--r--advtrains_interlocking/train_sections.lua26
16 files changed, 1423 insertions, 621 deletions
diff --git a/advtrains_interlocking/README.md b/advtrains_interlocking/README.md
new file mode 100644
index 0000000..d4a2699
--- /dev/null
+++ b/advtrains_interlocking/README.md
@@ -0,0 +1,85 @@
+# Interlocking for Advtrains
+
+The `advtrains_interlocking` mod provides various interlocking and signaling features for Advtrains.
+
+## Signal aspect tables
+
+Signal aspects are represented using tables with the following (optional) fields:
+
+* `main`: The main signal aspect. It provides information on the permitted speed after passing the signal.
+* `dst`: The distant signal aspect. It provides information on the permitted speed after passing the next signal.
+* `shunt`: Whether the train may proceed in shunt mode and, if the main aspect is danger, proceed in shunt mode.
+* `proceed_as_main`: Whether the train should exit shunt mode when proceeding.
+* `group`: The name of the signal group.
+* `name`: The name of the signal aspect.
+
+The `main` and `dst` fields may be:
+
+* An positive number indicating the permitted speed,
+* The number 0, indicating that the train should expect to stop at the current signal (or, for the `dst` field, the next signal),
+* The number -1, indicating that the train can proceed (or, for the `dst` field, expect to proceed) at maximum speed,
+* The constant `false`, indicating no change to the speed restriction, or
+* The constant `nil`, indicating that the default value for the name aspect (if present) is used. If no valid signal aspect is named, or the signal aspect does not provide a default value, the value is assumed to be `false`.
+
+### Node definitions
+
+Signals should belong the following node groups:
+
+* `advtrains_signal`: `1` for static signals, `2` for signals with variable aspects.
+* `save_in_at_nodedb`: This should be set to `1` to make sure that Advtrains always has access to the signal.
+* `not_blocking_trains`: Setting this to `1` prevents trains from colliding with the signal. Setting this is not necessary, but recommended.
+
+The node definition should contain an `advtrains` field.
+
+The `advtrains` field of the node definition should contain a `supported_aspects` table for signals with variable aspects.
+
+The `supported_aspects` table should contain the following fields:
+
+* `main`: A list of values supported for the main aspect.
+* `dst`: A list of values supported for the distant aspect.
+* `shunt`: The value for the `shunt` field of the signal aspect or `nil` if the value is variable.
+* `proceed_as_main`: The value for the `proceed_as_main` field of the signal aspect.
+* `group`: The name of the signal group.
+* `name`: A list of supported (named) aspects.
+* `dst_shift`: The phase shift for distant/repeater signals. This field should not be set for main signals.
+
+The `advtrains` field of the node definition should contain a `get_aspect` function. This function is given the position of the signal and the node at the position. It should return the signal aspect table or, in the case of type 2 signals, the name of the signal aspect.
+
+For signals with variable aspects, a corresponding `set_aspect` function should also be defined. This function is given the position of the signal, the node at the position, and the new aspect. The new aspect is not guaranteed to be supported by the signal itself.
+
+Signals should also have the following callbacks set:
+
+* `on_rightclick` should be set to `advtrains.interlocking.signal_rc_handler`
+* `can_dig` should be set to `advtrains.interlocking.signal_can_dig`
+* `after_dig_node` should be set to `advtrains.interlocking.signal_after_dig`
+
+Alternatively, custom callbacks should call the respective functions.
+
+## Signal groups
+
+Signals may belong to signal groups are registered using `advtrains.interlocking.aspect.register_group`.
+
+Signal group definitions include the following fields:
+
+* `name`: The internal name of the signal group. It is recommended to use the mod name as a prefix to avoid name collisions.
+* `label`: The description of the signal group.
+* `aspects`: A table of signal aspects. Entries with string indices define the signal aspect with the name. Entries with numeric indices (starting from 1, counting upward) contain a list of corresponding aspect names (the first entry is preferred) and are mainly used for routing, where larger indices indicate that the signal with the aspect is closer to the signal with the "danger" (or similar) aspect.
+
+Each aspect in the signal group definition table should contain the following fields:
+
+* `label`: The description of the signal aspect.
+* `main`, `shunt`, `proceed_as_main`: The default values for the aspect. Note that the `dst` field has no default value as it is automatically adjusted.
+
+## Notes
+
+It is allowed to provide other methods of setting the signal aspect. However:
+
+* These changes are ignored by the routesetting system.
+* Please call `advtrains.interlocking.signal_readjust_aspect` after the signal aspect has changed.
+
+## Examples
+An example of speed signals can be found in `advtrains_signals_ks`, which provides a subset of German signals.
+
+An example of route signals can be found in `advtrains_signals_japan`, which provides a subset of Japanese signals.
+
+The mods mentioned above are also used for demonstation purposes and can also be used for testing.
diff --git a/advtrains_interlocking/aspect.lua b/advtrains_interlocking/aspect.lua
new file mode 100644
index 0000000..c7d5c81
--- /dev/null
+++ b/advtrains_interlocking/aspect.lua
@@ -0,0 +1,296 @@
+--- Signal aspect handling.
+-- @module advtrains.interlocking.aspect
+
+local registered_groups = {}
+
+local default_aspect = {
+ main = false,
+ dst = false,
+ shunt = true,
+ proceed_as_main = false,
+}
+
+local signal_aspect = {}
+
+local signal_aspect_metatable = {
+ __eq = function(asp1, asp2)
+ for _, k in pairs {"main", "dst", "shunt", "proceed_as_main"} do
+ local v1, v2 = (asp1[k] or false), (asp2[k] or false)
+ if v1 ~= v2 then
+ return false
+ end
+ end
+ if asp1.group and asp1.group == asp2.group then
+ return asp1.name == asp2.name
+ end
+ return true
+ end,
+ __index = function(asp, field)
+ local val = signal_aspect[field]
+ if val then
+ return val
+ end
+ val = default_aspect[field]
+ if val == nil then
+ return nil
+ end
+ local group = registered_groups[rawget(asp, "group")]
+ if group then
+ local aspdef = group.aspects[rawget(asp, "name")]
+ if aspdef[field] ~= nil then
+ val = aspdef[field]
+ end
+ end
+ return val
+ end,
+ __tostring = function(asp)
+ local st = {}
+ if asp.group and asp.name then
+ table.insert(st, ("%q in %q"):format(asp.name, asp.group))
+ end
+ if asp.main then
+ table.insert(st, ("current %d"):format(asp.main))
+ end
+ if asp.main ~= 0 then
+ if asp.dst then
+ table.insert(st, string.format("next %d", asp.dst))
+ end
+ end
+ if asp.main ~= 0 and asp.proceed_as_main then
+ table.insert(st, "proceed as main")
+ end
+ return ("[%s]"):format(table.concat(st, ", "))
+ end,
+}
+
+local function quicknew(t)
+ return setmetatable(t, signal_aspect_metatable)
+end
+
+--- Signal aspect class.
+-- @type signal_aspect
+
+--- Return a plain version of the signal aspect.
+-- @param[opt=false] raw Bypass metamethods when fetching signal aspects
+-- @return A plain copy of the signal aspect object.
+function signal_aspect:plain(raw)
+ local t = {}
+ for _, k in pairs {"main", "dst", "shunt", "proceed_as_main", "group", "name"} do
+ local v
+ if raw then
+ v = rawget(self, k)
+ else
+ v = self[k]
+ end
+ t[k] = v
+ end
+ return t
+end
+
+--- Create (or copy) a signal aspect object.
+-- Note that signal aspect objects can also be created by calling the `advtrains.interlocking.aspect` table.
+-- @return The newly created signal aspect object.
+function signal_aspect:new()
+ if type(self) ~= "table" then
+ return quicknew{}
+ end
+ local newasp = {}
+ for _, k in pairs {"main", "dst"} do
+ if type(self[k]) == "table" then
+ if self[k].free then
+ newasp[k] = self[k].speed
+ else
+ newasp[k] = 0
+ end
+ else
+ newasp[k] = self[k]
+ end
+ end
+ if type(self.shunt) == "table" then
+ newasp.shunt = self.shunt.free
+ newasp.proceed_as_main = self.shunt.proceed_as_main
+ else
+ newasp.shunt = self.shunt
+ end
+ for _, k in pairs {"group", "name"} do
+ newasp[k] = self[k]
+ end
+ return quicknew(newasp)
+end
+
+--- Modify the signal aspect in-place to fit in the specific signal group.
+-- @param group The signal group. The `nil` indicates a generic group.
+-- @return The (now modified) signal aspect itself.
+function signal_aspect:to_group(group)
+ local cg = self.group
+ local gdef = registered_groups[group]
+ if type(self.name) ~= "string" then
+ self.name = nil
+ end
+ if not gdef then
+ for k in pairs(default_aspect) do
+ rawset(self, k, self[k])
+ end
+ self.group = nil
+ self.name = nil
+ return self
+ elseif cg == group and gdef.aspects[self.name] then
+ return self
+ end
+ local newidx = 1
+ if self.main == 0 then
+ newidx = #gdef.aspects
+ end
+ local cgdef = registered_groups[cg]
+ if cgdef then
+ local idx = (cgdef.aspects[self.name] or {}).index
+ if idx then
+ if idx >= #cgdef.aspects then
+ idx = #gdef.aspects
+ elseif idx >= #gdef.aspects then
+ idx = #gdef.aspects-1
+ end
+ newidx = idx
+ end
+ end
+ self.group = group
+ self.name = gdef.aspects[newidx][1]
+ return self
+end
+
+--- Modify the signal aspect in-place to indicate a specific distant aspect.
+-- @param dst The distant aspect
+-- @param[opt=1] shift The phase shift of the current signal.
+-- @return The (now modified) signal aspect itself.
+function signal_aspect:adjust_distant(dst, shift)
+ if (shift or -1) < 0 then
+ shift = 1
+ end
+ if not dst then
+ self.dst = nil
+ return self
+ end
+ if self.main ~= 0 then
+ self.dst = dst.main
+ else
+ self.dst = nil
+ return self
+ end
+ local dgdef = registered_groups[dst.group]
+ if dgdef then
+ if self.group == dst.group and shift == 0 then
+ self.name = dst.name
+ else
+ local idx = (dgdef.aspects[dst.name] or {}).index
+ if idx then
+ idx = math.max(idx-shift, 1)
+ self.group = dst.group
+ self.name = dgdef.aspects[idx][1]
+ end
+ end
+ end
+ return self
+end
+
+--- Signal groups.
+-- @section signal_group
+
+--- Register a signal group.
+-- @function register_group
+-- @param def The definition table.
+local function register_group(def)
+ local t = {}
+ local name = def.name
+ if type(name) ~= "string" then
+ return error("Expected signal group name to be a string, got " .. type(name))
+ elseif registered_groups[name] then
+ return error(string.format("Attempt to redefine signal group %q, previously defined in %s", name, registered_groups[name].defined))
+ end
+ t.name = name
+
+ t.defined = debug.getinfo(2, "S").short_src or "[?]"
+
+ local label = def.label or name
+ if type(label) ~= "string" then
+ return error("Label is not a string")
+ end
+ t.label = label
+
+ local mainasps = {}
+ for idx, asp in pairs(def.aspects) do
+ local idxtp = type(idx)
+ if idxtp == "string" then
+ local t = {}
+ t.name = idx
+
+ local label = asp.label or idx
+ if type(label) ~= "string" then
+ return error("Aspect label is not a string")
+ end
+ t.label = label
+
+ for _, k in pairs{"main", "dst", "shunt"} do
+ t[k] = asp[k]
+ end
+
+ mainasps[idx] = t
+ end
+ end
+ if #def.aspects < 2 then
+ return error("Insufficient entries in signal aspect list")
+ end
+ for idx, asplist in ipairs(def.aspects) do
+ if type(asplist) ~= "table" then
+ asplist = {asplist}
+ else
+ asplist = table.copy(asplist)
+ end
+ if #asplist < 1 then
+ error("Invalid entry in signal aspect list")
+ end
+ for _, k in ipairs(asplist) do
+ if type(k) ~= "string" then
+ return error("Invalid signal aspect ID")
+ end
+ local asp = mainasps[k]
+ if not asp then
+ return error("Invalid signal aspect ID")
+ end
+ if asp.index ~= nil then
+ return error("Attempt to assign a signal aspect to multiple numeric indices")
+ end
+ asp.index = idx
+ end
+ mainasps[idx] = asplist
+ end
+ t.aspects = mainasps
+
+ registered_groups[name] = t
+end
+
+--- Get the definition of a signal group.
+-- @function get_group_definition
+-- @param name The name of the signal group.
+-- @return[1] The definition for the signal group (if present).
+-- @return[2] The nil constant (otherwise).
+local function get_group_definition(name)
+ local t = registered_groups[name]
+ if t then
+ return table.copy(t)
+ else
+ return nil
+ end
+end
+
+local lib = {
+ register_group = register_group,
+ get_group_definition = get_group_definition,
+}
+
+local libmt = {
+ __call = function(_, ...)
+ return signal_aspect.new(...)
+ end,
+}
+
+return setmetatable(lib, libmt)
diff --git a/advtrains_interlocking/database.lua b/advtrains_interlocking/database.lua
index 17a4199..49ca13d 100644
--- a/advtrains_interlocking/database.lua
+++ b/advtrains_interlocking/database.lua
@@ -150,6 +150,9 @@ function ildb.load(data)
advtrains.interlocking.npr_rails = data.npr_rails
end
+ -- let signal_api load data
+ advtrains.interlocking.signal.load(data)
+
--COMPATIBILITY to Signal aspect format
-- TODO remove in time...
for pts,tcb in pairs(track_circuit_breaks) do
@@ -183,7 +186,7 @@ function ildb.load(data)
end
function ildb.save()
- return {
+ local data = {
tcbs = track_circuit_breaks,
ts=track_sections,
signalass = signal_assignments,
@@ -193,6 +196,8 @@ function ildb.save()
npr_rails = advtrains.interlocking.npr_rails,
tcbpts_conversion_applied = true, -- remark that legacy pos conversion has taken place
}
+ advtrains.interlocking.signal.save(data)
+ return data
end
--
@@ -1001,7 +1006,7 @@ end
function ildb.get_ip_signal_asp(pts, connid)
local p = ildb.get_ip_signal(pts, connid)
if p then
- local asp = advtrains.interlocking.signal_get_aspect(p)
+ local asp = advtrains.interlocking.signal.get_aspect_info(p)
if not asp then
atlog("Clearing orphaned signal influence point", pts, "/", connid)
ildb.clear_ip_signal(pts, connid)
diff --git a/advtrains_interlocking/demosignals.lua b/advtrains_interlocking/demosignals.lua
deleted file mode 100644
index 1c1b8b2..0000000
--- a/advtrains_interlocking/demosignals.lua
+++ /dev/null
@@ -1,97 +0,0 @@
--- Demonstration signals
--- Those can display the 3 main aspects of Ks signals
-
--- Note that the group value of advtrains_signal is 2, which means "step 2 of signal capabilities"
--- advtrains_signal=1 is meant for signals that do not implement set_aspect.
-
-
-local setaspect = function(pos, node, asp)
- if asp.main == 0 then
- advtrains.ndb.swap_node(pos, {name="advtrains_interlocking:ds_danger"})
- else
- if asp.dst ~= 0 and asp.main == -1 then
- advtrains.ndb.swap_node(pos, {name="advtrains_interlocking:ds_free"})
- else
- advtrains.ndb.swap_node(pos, {name="advtrains_interlocking:ds_slow"})
- end
- end
- local meta = minetest.get_meta(pos)
- if meta then
- meta:set_string("infotext", minetest.serialize(asp))
- end
-end
-
-local suppasp = {
- main = {0, 6, -1},
- dst = {0, false},
- shunt = false,
- proceed_as_main = true,
- info = {
- call_on = false,
- dead_end = false,
- w_speed = nil,
- }
-}
-
-minetest.register_node("advtrains_interlocking:ds_danger", {
- description = "Demo signal at Danger",
- tiles = {"at_il_signal_asp_danger.png"},
- groups = {
- cracky = 3,
- advtrains_signal = 2,
- save_in_at_nodedb = 1,
- },
- advtrains = {
- set_aspect = setaspect,
- supported_aspects = suppasp,
- get_aspect = function(pos, node)
- return advtrains.interlocking.DANGER
- end,
- },
- on_rightclick = advtrains.interlocking.signal_rc_handler,
- can_dig = advtrains.interlocking.signal_can_dig,
- after_dig_node = advtrains.interlocking.signal_after_dig,
-})
-minetest.register_node("advtrains_interlocking:ds_free", {
- description = "Demo signal at Free",
- tiles = {"at_il_signal_asp_free.png"},
- groups = {
- cracky = 3,
- advtrains_signal = 2,
- save_in_at_nodedb = 1,
- },
- advtrains = {
- set_aspect = setaspect,
- supported_aspects = suppasp,
- get_aspect = function(pos, node)
- return {
- main = -1,
- }
- end,
- },
- on_rightclick = advtrains.interlocking.signal_rc_handler,
- can_dig = advtrains.interlocking.signal_can_dig,
- after_dig_node = advtrains.interlocking.signal_after_dig,
-})
-minetest.register_node("advtrains_interlocking:ds_slow", {
- description = "Demo signal at Slow",
- tiles = {"at_il_signal_asp_slow.png"},
- groups = {
- cracky = 3,
- advtrains_signal = 2,
- save_in_at_nodedb = 1,
- },
- advtrains = {
- set_aspect = setaspect,
- supported_aspects = suppasp,
- get_aspect = function(pos, node)
- return {
- main = 6,
- }
- end,
- },
- on_rightclick = advtrains.interlocking.signal_rc_handler,
- can_dig = advtrains.interlocking.signal_can_dig,
- after_dig_node = advtrains.interlocking.signal_after_dig,
-})
-
diff --git a/advtrains_interlocking/distant_ui.lua b/advtrains_interlocking/distant_ui.lua
new file mode 100644
index 0000000..bb66dc4
--- /dev/null
+++ b/advtrains_interlocking/distant_ui.lua
@@ -0,0 +1,141 @@
+local F = advtrains.formspec
+local D = advtrains.distant
+local I = advtrains.interlocking
+
+function I.make_short_dst_formspec_component(pos, x, y, w)
+ local main, set_by = D.get_main(pos)
+ if main then
+ local pts_main = minetest.pos_to_string(main)
+ local desc = attrans("The assignment is made with an unknown method.")
+ if set_by == "manual" then
+ desc = attrans("The assignment is made manually.")
+ elseif set_by == "routesetting" then
+ desc = attrans("The assignment is made by the routesetting system.")
+ end
+ return table.concat {
+ F.S_label(x, y, "This signal is a distant signal of @1.", pts_main),
+ F.label(x, y+0.5, desc),
+ F.S_button_exit(x, y+1, w/2-0.125, "dst_assign", "Reassign"),
+ F.S_button_exit(x+w/2+0.125, y+1, w/2-0.125, "dst_unassign", "Unassign"),
+ }
+ else
+ return table.concat {
+ F.S_label(x, y, "This signal is not assigned to a main signal."),
+ F.S_label(x, y+0.5, "The distant aspect of the signal is not used."),
+ F.S_button_exit(x, y+1, w, "dst_assign", "Assign")
+ }
+ end
+end
+
+function I.make_dst_list_formspec_component(pos, x, y, w, h)
+ local ymid = y+0.25+h/2
+ local dstlist = {}
+ for pos, _ in pairs(D.get_dst(pos)) do
+ table.insert(dstlist, minetest.pos_to_string(advtrains.decode_pos(pos)))
+ end
+ return table.concat {
+ F.S_label(x, y, "Distant signals:"),
+ F.textlist(x, y+0.5, w-1, h-0.5, "dstlist", dstlist),
+ F.image_button_exit(x+w-0.75, ymid-0.875, 0.75, 0.75, "cdb_add.png", "dst_add", ""),
+ F.image_button_exit(x+w-0.75, ymid+0.125, 0.75, 0.75, "cdb_clear.png", "dst_del", ""),
+ }
+end
+
+function I.make_dst_formspec_component(pos, x, y, w, h)
+ return I.make_short_dst_formspec_component(pos, x, y, w, h)
+ .. I.make_dst_list_formspec_component(pos, x, y+2, w, h-2)
+end
+
+function I.show_distant_signal_form(pos, pname)
+ return I.show_ip_form(pos, pname)
+end
+
+local signal_pos = {}
+local function init_signal_assignment(pname, pos)
+ if not minetest.check_player_privs(pname, "interlocking") then
+ minetest.chat_send_player(pname, attrans("This operation is not allowed without the @1 privilege.", "interlocking"))
+ return
+ end
+ if not D.appropriate_signal(pos) then
+ minetest.chat_send_player(pname, attrans("Incompatible signal."))
+ return
+ end
+ signal_pos[pname] = pos
+ minetest.chat_send_player(pname, attrans("Please punch the signal to use as the main signal."))
+end
+
+local distant_pos = {}
+local function init_distant_assignment(pname, pos)
+ if not minetest.check_player_privs(pname, "interlocking") then
+ minetest.send_chat_player(pname, attrans("This operation is now allowed without the @1 privilege.", "interlocking"))
+ return
+ end
+ if not D.appropriate_signal(pos) then
+ minetest.chat_send_player(pname, attrans("Incompatible signal."))
+ return
+ end
+ distant_pos[pname] = pos
+ minetest.chat_send_player(pname, attrans("Please punch the signal to use as the distant signal."))
+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
+ local spos = signal_pos[pname]
+ local distant = false
+ if not spos then
+ spos = distant_pos[pname]
+ if not spos then
+ return
+ end
+ distant = true
+ end
+ signal_pos[pname] = nil
+ distant_pos[pname] = nil
+ local is_signal = minetest.get_item_group(node.name, "advtrains_signal") >= 2
+ if not (is_signal and D.appropriate_signal(pos)) then
+ minetest.chat_send_player(pname, attrans("Incompatible signal."))
+ return
+ end
+ minetest.chat_send_player(pname, attrans("Successfully assigned signal."))
+ if distant then
+ D.assign(spos, pos, "manual")
+ else
+ D.assign(pos, spos, "manual")
+ end
+end)
+
+local dstsel = {}
+
+function advtrains.interlocking.handle_dst_formspec_fields(pname, pos, fields)
+ if not (pos and minetest.check_player_privs(pname, "interlocking")) then
+ return
+ end
+ if fields.dst_unassign then
+ D.unassign_dst(pos)
+ elseif fields.dst_assign then
+ init_signal_assignment(pname, pos)
+ elseif fields.dst_add then
+ init_distant_assignment(pname, pos)
+ elseif fields.dstlist then
+ dstsel[pname] = minetest.explode_textlist_event(fields.dstlist).index
+ elseif fields.dst_del then
+ local selid = dstsel[pname]
+ if selid then
+ local dsts = D.get_dst(pos)
+ local pos
+ for p, _ in pairs(dsts) do
+ selid = selid-1
+ if selid <= 0 then
+ pos = p
+ break
+ end
+ end
+ if pos then
+ D.unassign_dst(advtrains.decode_pos(pos))
+ end
+ end
+ end
+end
diff --git a/advtrains_interlocking/init.lua b/advtrains_interlocking/init.lua
index fe8b967..a4ddbad 100644
--- a/advtrains_interlocking/init.lua
+++ b/advtrains_interlocking/init.lua
@@ -1,5 +1,5 @@
--- Advtrains interlocking system
--- See database.lua for a detailed explanation
+--- Advtrains interlocking system.
+-- @module advtrains.interlocking
advtrains.interlocking = {}
@@ -12,9 +12,11 @@ end
local modpath = minetest.get_modpath(minetest.get_current_modname()) .. DIR_DELIM
+--advtrains.interlocking.aspect = dofile(modpath.."aspect.lua")
+
dofile(modpath.."database.lua")
dofile(modpath.."signal_api.lua")
-dofile(modpath.."demosignals.lua")
+dofile(modpath.."signal_aspect_ui.lua")
dofile(modpath.."train_sections.lua")
dofile(modpath.."route_prog.lua")
dofile(modpath.."routesetting.lua")
diff --git a/advtrains_interlocking/route_ui.lua b/advtrains_interlocking/route_ui.lua
index a1a331d..982c579 100644
--- a/advtrains_interlocking/route_ui.lua
+++ b/advtrains_interlocking/route_ui.lua
@@ -33,7 +33,7 @@ function atil.show_route_edit_form(pname, sigd, routeid)
local function itab(t)
tab[#tab+1] = minetest.formspec_escape(string.gsub(t, ",", " "))
end
- itab("TCB "..sigd_to_string(sigd).." ("..(tcbs.signal_name or "")..") Route #"..routeid)
+ itab("("..(tcbs.signal_name or "+")..") Route #"..routeid)
-- this code is partially copy-pasted from routesetting.lua
-- we start at the tc designated by signal
@@ -56,13 +56,14 @@ function atil.show_route_edit_form(pname, sigd, routeid)
c_rseg = route[i]
c_lckp = {}
- itab(""..i.." Entry "..sigd_to_string(c_sigd).." -> Sec. "..(c_ts and c_ts.name or "-").." -> Exit "..(c_rseg.next and sigd_to_string(c_rseg.next) or "END"))
+ itab(""..i.." "..sigd_to_string(c_sigd))
+ itab("= "..(c_ts and c_ts.name or "-").." =")
if c_rseg.locks then
for pts, state in pairs(c_rseg.locks) do
local pos = minetest.string_to_pos(pts)
- itab(" Lock: "..pts.." -> "..state)
+ itab("L "..pts.." -> "..state)
if not advtrains.is_passive(pos) then
itab("-!- No passive component at "..pts..". Please reconfigure route!")
break
@@ -75,16 +76,17 @@ function atil.show_route_edit_form(pname, sigd, routeid)
end
if c_sigd then
local e_tcbs = ildb.get_tcbs(c_sigd)
- itab("Route end: "..sigd_to_string(c_sigd).." ("..(e_tcbs and e_tcbs.signal_name or "-")..")")
+ local signame = "-"
+ if e_tcbs and e_tcbs.signal then signame = e_tcbs.signal_name or "+" end
+ itab("E "..sigd_to_string(c_sigd).." ("..signame..")")
else
- itab("Route ends on dead-end")
+ itab("E (none)")
end
- form = form.."textlist[0.5,2;7.75,3.9;rtelog;"..table.concat(tab, ",").."]"
+ form = form.."textlist[0.5,2;3,3.9;rtelog;"..table.concat(tab, ",").."]"
form = form.."button[0.5,6;3,1;back;<<< Back to signal]"
- form = form.."button[4.5,6;2,1;aspect;Signal Aspect]"
- form = form.."button[6.5,6;2,1;delete;Delete Route]"
+ form = form.."button[5.5,6;3,1;delete;Delete Route]"
--atdebug(route.ars)
form = form.."style[ars;font=mono]"
diff --git a/advtrains_interlocking/routesetting.lua b/advtrains_interlocking/routesetting.lua
index 64b8c25..d619aac 100644
--- a/advtrains_interlocking/routesetting.lua
+++ b/advtrains_interlocking/routesetting.lua
@@ -45,12 +45,18 @@ function ilrs.set_route(signal, route, try)
local rtename = route.name
local signalname = (ildb.get_tcbs(signal).signal_name or "") .. sigd_to_string(signal)
local c_tcbs, c_ts_id, c_ts, c_rseg, c_lckp
+ -- signals = { { pos = ., tcbs_ref = <tcbs>, role = "main_distant", masp_override = nil, dst_type = "next_main" or "none" }
+ local signals = {}
+ local nodst
while c_sigd and i<=#route do
c_tcbs = ildb.get_tcbs(c_sigd)
if not c_tcbs then
if not try then atwarn("Did not find TCBS",c_sigd,"while setting route",rtename,"of",signal) end
return false, "No TCB found at "..sigd_to_string(c_sigd)..". Please update or reconfigure route!"
end
+ if i == 1 then
+ nodst = c_tcbs.nodst
+ end
c_ts_id = c_tcbs.ts_id
if not c_ts_id then
if not try then atwarn("Encountered End-Of-Interlocking while setting route",rtename,"of",signal) end
@@ -62,11 +68,11 @@ function ilrs.set_route(signal, route, try)
if c_ts.route then
if not try then atwarn("Encountered ts lock during a real run of routesetting routine, at ts=",c_ts_id,"while setting route",rtename,"of",signal) end
- return false, "Section '"..c_ts.name.."' already has route set from "..sigd_to_string(c_ts.route.origin)..":\n"..c_ts.route.rsn, c_ts_id, nil
+ return false, "Section '"..(c_ts.name or c_ts_id).."' already has route set from "..sigd_to_string(c_ts.route.origin)..":\n"..c_ts.route.rsn, c_ts_id, nil
end
if c_ts.trains and #c_ts.trains>0 then
if not try then atwarn("Encountered ts occupied during a real run of routesetting routine, at ts=",c_ts_id,"while setting route",rtename,"of",signal) end
- return false, "Section '"..c_ts.name.."' is occupied!", c_ts_id, nil
+ return false, "Section '"..(c_ts.name or c_ts_id).."' is occupied!", c_ts_id, nil
end
-- collect locks from rs cache and from route def
@@ -133,9 +139,17 @@ function ilrs.set_route(signal, route, try)
}
if c_tcbs.signal then
c_tcbs.route_committed = true
- c_tcbs.aspect = route.aspect or advtrains.interlocking.GENERIC_FREE
c_tcbs.route_origin = signal
- advtrains.interlocking.update_signal_aspect(c_tcbs)
+ -- determine route role
+ local ndef = advtrains.ndb.get_ndef(c_tcbs.signal)
+ local sig_table = {
+ pos = c_tcbs.signal,
+ tcbs_ref = c_tcbs,
+ role = ndef and ndef.advtrains and ndef.advtrains.route_role,
+ masp_override = c_rseg.masp_override, --TODO implement masp_override on UI side
+ dst_type = "next_main", --TODO allow user differentiate
+ }
+ signals[#signals+1] = sig_table
end
end
-- advance
@@ -143,6 +157,33 @@ function ilrs.set_route(signal, route, try)
c_sigd = c_rseg.next
i = i + 1
end
+
+ -- Get reference to signal at end of route
+ local last_mainsig = nil
+ if c_sigd then
+ local e_tcbs = ildb.get_tcbs(c_sigd)
+ local pos = e_tcbs and e_tcbs.signal
+ if pos then
+ last_mainsig = pos
+ end
+ end
+ for i = #signals, 1, -1 do
+ -- note the signals are iterated backwards. Switch depending on the role
+ local sig = signals[i]
+ -- apply mainaspect
+ sig.tcbs_ref.route_aspect = sig.masp_override or "_default"
+ if sig.role == "distant" or sig.role == "distant_repeater" or sig.role == "main_distant" then
+ -- assign the remote as the last mainsig
+ sig.tcbs_ref.route_remote = last_mainsig
+ end
+ if sig.role == "main" or sig.role == "main_distant" or sig.role == "end" then
+ -- record this as the new last mainsig
+ last_mainsig = sig.pos
+ end
+ -- for shunt signals nothing happens
+ -- update the signal aspect on map
+ advtrains.interlocking.signal.update_route_aspect(sig.tcbs_ref, i ~= 1)
+ end
return true
end
@@ -247,12 +288,13 @@ function ilrs.cancel_route_from(sigd)
--atdebug("cancelling",c_ts.route.rsn)
-- clear signal aspect and routesetting state
c_tcbs.route_committed = nil
- c_tcbs.aspect = nil
+ c_tcbs.route_aspect = nil
+ c_tcbs.route_remote = nil
c_tcbs.routeset = nil
c_tcbs.route_auto = nil
c_tcbs.route_origin = nil
- advtrains.interlocking.update_signal_aspect(c_tcbs)
+ advtrains.interlocking.signal.update_route_aspect(c_tcbs)
c_ts_id = c_tcbs.ts_id
if not c_tcbs then
@@ -302,7 +344,8 @@ function ilrs.update_route(sigd, tcbs, newrte, cancel)
advtrains.interlocking.route.cancel_route_from(sigd)
end
tcbs.route_committed = nil
- tcbs.aspect = nil
+ tcbs.route_aspect = nil
+ tcbs.route_remote = nil
has_changed_aspect = true
tcbs.routeset = nil
tcbs.route_auto = nil
@@ -331,12 +374,13 @@ function ilrs.update_route(sigd, tcbs, newrte, cancel)
end
else
--atdebug("Committed Route:",tcbs.routeset)
- has_changed_aspect = true
+ -- set_route now sets the signal aspects
+ --has_changed_aspect = true
end
end
if has_changed_aspect then
-- FIX: prevent an minetest.after() loop caused by update_signal_aspect dispatching path invalidation, which in turn calls ARS again
- advtrains.interlocking.update_signal_aspect(tcbs)
+ advtrains.interlocking.signal.update_route_aspect(tcbs)
end
advtrains.interlocking.update_player_forms(sigd)
end
diff --git a/advtrains_interlocking/signal_api.lua b/advtrains_interlocking/signal_api.lua
index 83fae4a..65fc787 100644
--- a/advtrains_interlocking/signal_api.lua
+++ b/advtrains_interlocking/signal_api.lua
@@ -1,539 +1,436 @@
-- Signal API implementation
+local F = advtrains.formspec
---[[
-Signal aspect table:
-Note: All speeds are measured in m/s, aka the number of + signs in the HUD.
-asp = {
- main = <int speed>,
- -- Main signal aspect, tells state and permitted speed of next section
- -- 0 = section is blocked
- -- >0 = section is free, speed limit is this value
- -- -1 = section is free, maximum speed permitted
- -- false/nil = Signal doesn't provide main signal information, retain current speed limit.
- shunt = <boolean>,
- -- Whether train may proceed as shunt move, on sight
- -- main aspect takes precedence over this
- -- When main==0, train switches to shunt move and is restricted to speed 6
- proceed_as_main = <boolean>,
- -- If an approaching train is a shunt move and 'shunt' is false,
- -- the train may proceed as a train move under the "main" aspect
- -- if the main aspect permits it (i.e. main!=0)
- -- If this is not set, shunt moves are NOT allowed to switch to
- -- a train move, and must stop even if "main" would permit passing.
- -- This is intended to be used for "Halt for shunt moves" signs.
-
- dst = <int speed>,
- -- Distant signal aspect, tells state and permitted speed of the section after next section
- -- The character of these information is purely informational
- -- At this time, this field is not actively used
- -- 0 = section is blocked
- -- >0 = section is free, speed limit is this value
- -- -1 = section is free, maximum speed permitted
- -- false/nil = Signal doesn't provide distant signal information.
-
- -- the character of call_on and dead_end is purely informative
- 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)
- }
-}
-
-== 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.
+local signal = {}
-The node definition of a signal node should contain those fields:
-groups = {
- advtrains_signal = 2,
- save_in_at_nodedb = 1,
+signal.MASP_HALT = {
+ name = "_halt",
+ halt = true,
}
-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. However, it must
- -- display an aspect that is at least as restrictive as the passed
- -- aspect as far as it is capable of doing so.
- -- Examples:
- -- - 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.
- -- - the german Hl system can only signal speeds of 40, 60
- -- and 100 km/h, a speed of 80km/h should then be signalled
- -- as 60 km/h instead.
- -- 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, the value of the described
- -- field is always assumed to be false (no information)
- -- A speed of 0 means that the signal can show a "blocked" aspect
- -- (which is probably the case for most signals)
- -- If the signal can signal "no information" on one of the fields
- -- (thus false is an acceptable value), include false in the list
- -- 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 = {<speed1>, ..., <speedn>} or nil,
- dst = {<speed1>, ..., <speedn>} or nil,
- shunt = <boolean/nil>,
-
- call_on = <boolean/nil>,
- dead_end = <boolean/nil>,
- w_speed = {<speed1>, ..., <speedn>} or nil,
-
- },
- Example for supported_aspects:
- supported_aspects = {
- main = {0, 6, -1}, -- can show either "Section blocked", "Proceed at speed 6" or "Proceed at maximum speed"
- dst = {0, false}, -- can show only if next signal shows "blocked", no other information.
- shunt = false, -- shunting by this signal is never allowed.
-
- call_on = false,
- dead_end = false,
- w_speed = nil,
- -- none of the information can be shown by the signal
-
- },
-
- 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=false 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 = false (unchanged)
- dst = false (unchanged)
- shunt = false (shunting not allowed)
- info = {} (no further information)
- end,
+
+signal.MASP_DEFAULT = {
+ name = "_default",
+ default = true,
}
-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 = {
+
+signal.ASPI_HALT = {
main = 0,
- dst = false,
shunt = false,
}
-advtrains.interlocking.DANGER = DANGER
-advtrains.interlocking.GENERIC_FREE = {
+signal.ASPI_FREE = {
main = -1,
shunt = false,
- dst = false,
+ proceed_as_main = true,
}
-local function convert_aspect_if_necessary(asp)
- if type(asp.main) == "table" then
- local newasp = {}
- if asp.main.free then
- newasp.main = asp.main.speed
- else
- newasp.main = 0
- end
- if asp.dst and asp.dst.free then
- newasp.dst = asp.dst.speed
- else
- newasp.dst = 0
+--[[
+Implementation plan orwell 2024-01-28:
+Most parts of ywang's implementation are fine, especially I like the formspecs. But I would like to change a few aspects (no pun intended) of this.
+- Signal gets distant assigned via field in signal aspect table (instead of explicitly)
+- Signal speed/shunt are no longer free-text but rather they need to be predefined in the node definition
+To do this: Differentiation between:
+== Main Aspect ==
+This is what a signal is assigned by either the route system or the user.
+It is a string key which has an appropriate entry in the node definition (where it has a description assigned)
+The signal mod defines a function to set a signal to the most appropriate aspect. This function gets
+a) the main aspect table (straight from node def)
+b) the distant signal's aspect group name & aspect table
+
+== Aspect ==
+One concrete combination of lights/shapes that a signal signal shows. Handling these is at the discretion of
+the signal mod defining the signal, and they are typically combinations of main aspect and distant aspect
+Example:
+- A Ks signal has the main_aspect="proceed_12" set for a route
+- The signal at the end of the route shows main_aspect="proceed_8", advtrains also passes on that this means {main=8, shunt=false}
+- The ndef.afunction(pos, node, main_aspect, rem_aspect, rem_aspinfo) determines that the signal should now show
+ blinking green with main indicator 12 and dst indicator 8, and sets the nodes accordingly.
+ This function can now return the Aspect Info table, which will be cached by advtrains until the aspect changes again
+ and will be used when a train approaches the signal. If nil is returned, then the aspect will be queried next time
+ by calling ndef.advtrains.get_aspect_info(pos)
+
+Note that once apply_aspect returns, there is no need for advtrains anymore to query the aspect info.
+When the signal, for any reason, wants to change its aspect by itself *without* going through the signal API then
+it should update the aspect info cache by calling advtrains.interlocking.signal.update_aspect_info(pos)
+
+Apply_aspect may also receive the special main aspect { name = "_halt", halt = true }. It usually means that the signal is not assigned to anything particular,
+and it should cause the signal to show its most restrictive aspect. Typically it is a halt aspect, but e.g. for distant-only
+signals this would be "expect stop".
+
+A special case occurs for pure distant signals: Such signals must set apply_aspect, but must not set main_aspects. Behavior is as follows:
+- Signal is uninitialized, distant signal is not assigned to a main signal, or no route is set: main_aspect == { name = "_halt", halt = true } and rem_aspect == nil
+- A remote main signal is assigned (either by user or by route): main_aspect is always { name = "_default" } and rem_aspect / rem_aspinfo give the correct information
+
+Main aspect names starting with underscore (e.g. "_default") are reserved and must not be used!
+
+== Aspect Info ==
+The actual signal aspect in the already-known format. This is what the trains use to determine halt/proceed and speed.
+asp = {
+ main = 0 (halt) / -1 (max speed) / false (no info) / <number> (speed limit)
+ shunt = true (shunt free) / false (shunt not free)
+ proceed_as_main = true (shunt move can proceed and become train move when main!=0) / false (no)
+ dst = speed of the remote signal (like main, informative character, not actually used)
+}
+
+Node definition of signals:
+- The signal needs some logic to figure out, for each combination of its own aspect group and the distant signal's aspect, what aspect info it can/will show.
+ndef.advtrains = {
+ main_aspects = {
+ { name = "proceed" description = "Proceed at full speed", <more data at discretion of signal>}
+ { name = "reduced" description = "Proceed at reduced speed", <more data at discretion of signal>}
+ }
+ -- This list is mainly for the selection dialog. Order of entries determines list order in the dropdown.
+ -- Some fields have special meaning:
+ -- name: A unique key to identify the main aspect. Might be required by some code.
+ -- description: Text shown in UI dropdown
+ -- Node can set any other fields at its discretion. They are not touched.
+ -- Note: Pure distant signals (that cannot show halt) should NOT have a main_aspects table.
+ -- For these signals no main aspect selection UI is shown and they cannot be startpoint of a route
+ apply_aspect = function(pos, node, main_aspect, rem_aspect, rem_aspinfo)
+ -- set the node to show the desired aspect
+ -- called by advtrains when this signal's aspect group or the remote signal's aspect changes
+ -- main_aspect is never nil, but can be one of the special aspects { halt = true } or { default = true }
+ -- MAY return the aspect_info. If it returns nil then get_aspect_info will be queried at a later point.
+ get_aspect_info(pos, main_aspect)
+ -- Returns the aspect info table (main, shunt, dst etc.)
+ distant_support = true or false
+ -- If true, signal is considered in distant signalling. If false or nil, rem_aspect and rem_aspinfo are never set.
+ route_role = one of "main", "main_distant", "shunt", "distant", "distant_repeater", "end"
+ -- Determines how the signal behaves when routes are set. Only in effect when signal is assigned to a TCB.
+ -- main: The signal is a possible endpoint for a train move route. Distant signals before it refer to it.
+ -- shunt: The signal is a possible endpoint for a shunt move route. Ignored for distant signals.
+ -- distant, distant_repeater: When route is set, signal is always assigned its first main aspect. The next signal with role="main" is set as the remote signal. (currently no further distinction)
+ -- main_distant: Combination of main and distant - like "main", but additionally gets assigned to the next main like a "distant"
+ -- end: like main, but signifies that it marks an end of track and trains cannot continue further. (currently no practical implications above main)
+}
+
+== Nomenclature ==
+The distant/main relation is named as follows:
+ V M
+=====>====>
+Main signal (main) always refers to the signal that is in focus right now (even if that is a distant-only signal)
+From the standpoint of M, V is the distant (dst) signal. M does not need to concern itself with V's aspect but needs to notify V when it changes
+From the standpoint of V, M is the remote (rem) signal. V needs to show an aspect that matches its remote signal M
+
+== Criteria for which signals are eligible for routes ==
+
+All signals must define:
+- get_aspect_info()
+
+Signals that can be assigned to a TCB must satisfy:
+- apply_aspect() defined
+
+Signals that are possible start and end points for a route must satisfy:
+- main_aspects defined (note, pure distant signals should therefore not define main_aspects)
+
+]]
+
+-- Database
+-- Signal Aspect store
+-- Stores for each signal the main aspect and other info, like the assigned remote signal
+-- [signal encodePos] = { main = <table or string>, [remote = encodedPos] }
+-- main is a string: "named aspect" is looked up in the main_aspects table of the ndef
+-- main is a table: this table directly is the main aspect (used for advanced signals with additional lights/indicators)
+signal.aspects = {}
+
+-- Distant signal notification. Records for each signal the distant signals that refer to it
+-- Note: this mapping is weak. Needs always backreference check.
+-- [signal encodePos] = { [distant signal encodePos] = true }
+signal.distant_refs = {}
+
+function signal.load(data)
+ signal.aspects = data.aspects or {}
+ -- rebuild distant_refs after load
+ signal.distant_refs = {}
+ for main, aspt in pairs(signal.aspects) do
+ if aspt.remote then
+ if not signal.distant_refs[aspt.remote] then
+ signal.distant_refs[aspt.remote] = {}
+ end
+ signal.distant_refs[aspt.remote][main] = true
end
- newasp.proceed_as_main = asp.shunt.proceed_as_main
- newasp.shunt = asp.shunt.free
- -- Note: info table not transferred, it's not used right now
- return newasp
end
- return asp
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
+function signal.save(data)
+ data.aspects = signal.aspects
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)
+-- Set a signal's aspect.
+-- Signal aspects should only be set through this function. It takes care of:
+-- - Storing the main aspect and dst pos for this signal permanently (until next change)
+-- - Assigning the distant signal for this signal
+-- - Calling apply_aspect() in the signal's node definition to make the signal show the aspect
+-- - Calling apply_aspect() again whenever the remote signal changes its aspect
+-- - Notifying this signal's distant signals about changes to this signal (unless skip_dst_notify is specified)
+-- main_asp: either a string (==name in ndef.advtrains.main_aspects) or the main aspect table directly (for advanced signals)
+function signal.set_aspect(pos, main_asp, rem_pos, skip_dst_notify)
+ -- safeguard for the two integrated aspects (these two must be passed as string key)
+ if type(main_asp)=="table" and (main_asp.name=="_default" or main_asp.name=="_halt") then
+ error("MASP_HALT and MASP_DEFAULT must be passed via string keys _halt or _default, not as tables!")
+ end
+ local main_pts = advtrains.encode_pos(pos)
+ local old_tbl = signal.aspects[main_pts]
+ local old_remote = old_tbl and old_tbl.remote
+ local new_remote = rem_pos and advtrains.encode_pos(rem_pos)
+
+ -- if remote has changed, unregister from old remote
+ if old_remote and old_remote~=new_remote and signal.distant_refs[old_remote] then
+ atdebug("unregister old remote: ",old_remote,"from",main_pts)
+ signal.distant_refs[old_remote][main_pts] = nil
+ end
+
+ signal.aspects[main_pts] = { main = main_asp, remote = new_remote }
+ -- apply aspect on main signal, this also checks new_remote
+ signal.reapply_aspect(main_pts)
+
+ -- notify my distants about this change (with limit 2)
+ if not skip_dst_notify then
+ signal.notify_distants_of(main_pts, 2)
+ end
end
-function advtrains.interlocking.signal_set_aspect(pos, asp)
- asp = convert_aspect_if_necessary(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)
+function signal.clear_aspect(pos, skip_dst_notify)
+ local main_pts = advtrains.encode_pos(pos)
+ local old_tbl = signal.aspects[main_pts]
+ local old_remote = old_tbl and old_tbl.remote
+
+ -- unregister from old remote
+ if old_remote then
+ signal.distant_refs[old_remote][main_pts] = nil
+ end
+
+ signal.aspects[main_pts] = nil
+ -- apply aspect on main signal, this also checks new_remote
+ signal.reapply_aspect(main_pts)
+
+ -- notify my distants about this change (with limit 2)
+ if not skip_dst_notify then
+ signal.notify_distants_of(main_pts, 2)
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)
+-- Clear any info about aspects from this signal, without resetting/reapplying the aspect.
+-- Supposed to be used for legacy on-off signals when the on-off toggle is used
+function signal.unregister_aspect(pos)
+ local main_pts = advtrains.encode_pos(pos)
+ local old_tbl = signal.aspects[main_pts]
+ local old_remote = old_tbl and old_tbl.remote
- advtrains.invalidate_all_paths_ahead(ipos)
+ -- unregister from old remote
+ if old_remote then
+ signal.distant_refs[old_remote][main_pts] = nil
+ end
+
+ signal.aspects[main_pts] = nil
end
-function advtrains.interlocking.signal_rc_handler(pos, node, player, itemstack, pointed_thing)
- local pname = player:get_player_name()
- local control = player:get_player_control()
- if control.aux1 then
- advtrains.interlocking.show_ip_form(pos, pname)
+-- Notify distant signals of main_pts of a change in the aspect of this signal
+--
+function signal.notify_distants_of(main_pts, limit)
+ atdebug("notify_distants_of",advtrains.decode_pos(main_pts),"limit",limit)
+ if limit <= 0 then
return
end
-
- 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
- local function callback(pname, aspect)
- advtrains.interlocking.signal_set_aspect(pos, aspect)
+ local dstrefs = signal.distant_refs[main_pts]
+ atdebug("dstrefs",dstrefs,"")
+ if dstrefs then
+ for dst,_ in pairs(dstrefs) do
+ -- ensure that the backref is still valid
+ local dst_asp = signal.aspects[dst]
+ if dst_asp and dst_asp.remote == main_pts then
+ signal.reapply_aspect(dst)
+ signal.notify_distants_of(dst, limit - 1)
+ else
+ atwarn("Distant signal backref is not purged: main =",main_pts,", distant =",dst,", remote =",dst_asp.remote,"")
end
- local isasp = ndef.advtrains.get_aspect(pos, node)
-
- advtrains.interlocking.show_signal_aspect_selector(
- pname,
- ndef.advtrains.supported_aspects,
- "Set aspect manually", callback,
- isasp)
- else
- --static signal - only IP
- advtrains.interlocking.show_ip_form(pos, pname)
end
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 convert_aspect_if_necessary(tcbs.aspect)
- end
- end
- return DANGER;
-end
+function signal.notify_trains(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)
--- 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
- return convert_aspect_if_necessary(asp)
- end
- return nil
+ -- FIXME: invalidate_all_paths_ahead does not appear to always work as expected
+ --advtrains.invalidate_all_paths_ahead(ipos)
+ minetest.after(0, advtrains.invalidate_all_paths, ipos)
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
+-- Update waiting trains and distant signals about a changed signal aspect
+-- Must be called when a signal's aspect changes through some other means
+-- and not via the signal mechanism
+function signal.notify_on_aspect_changed(pos, skip_dst_notify)
+ signal.notify_trains(pos)
+ if not skip_dst_notify then
+ signal.notify_distants_of(advtrains.encode_pos(pos), 2)
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" },
- })
+-- Gets the stored main aspect and distant signal position for this signal
+-- This information equals the information last passed to set_aspect
+-- It does not take into consideration the actual speed signalling, please use
+-- get_aspect_info() for this
+-- pos: the position of the signal
+-- returns: main_aspect, dst_pos
+function signal.get_aspect(pos)
+ local aspt = signal.aspects[advtrains.encode_pos(pos)]
+ local ma,dp = signal.get_aspect_internal(pos, aspt)
+ return ma, advtrains.decode_pos(dp)
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).."]"
- advtrains.interlocking.db.check_for_duplicate_ip(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
+local function cache_mainaspects(ndefat)
+ ndefat.main_aspects_lookup = {}
+ for _,ma in ipairs(ndefat.main_aspects) do
+ ndefat.main_aspects_lookup[ma.name] = ma
+ end
+ ndefat.main_aspects_lookup[signal.MASP_HALT.name] = signal.MASP_HALT.name -- halt is always defined
+ ndefat.main_aspects_lookup[signal.MASP_DEFAULT.name] = ndefat.main_aspects[1] -- default is the first one
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
+-- gets the main aspect. resolves named aspects to aspect table on demand
+function signal.get_aspect_internal(pos, aspt)
+ -- look up node and nodedef
+ local node = advtrains.ndb.get_node_or_nil(pos)
+ local ndef = node and minetest.registered_nodes[node.name]
+ if not aspt then
+ -- oh, no main aspect, nevermind
+ return signal.MASP_HALT, nil, node, ndef
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.")
+ local ndefat = ndef.advtrains or {}
+ local masp = aspt.main or signal.MASP_HALT
- 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.")
+ if type(masp) == "string" then
+ if masp=="_halt" then
+ masp = signal.MASP_HALT
+ elseif masp=="_default" and not ndefat.main_aspects then
+ -- case is fine, distant only signal
+ masp = signal.MASP_DEFAULT
+ else
+ assert(ndefat.main_aspects, "With named aspects, node needs advtrains.main_aspects table!")
+ -- resolve the main aspect from the mainaspects table
+ if not ndefat.main_aspects_lookup then
+ cache_mainaspects(ndefat)
end
- else
- minetest.chat_send_player(pname, "Configuring Signal: Node is too far away. Aborted.")
+ masp = ndefat.main_aspects_lookup[aspt.main] or signal.MASP_DEFAULT
end
- players_assign_ip[pname] = nil
end
-end)
-
-
---== aspect selector ==--
-
-local players_aspsel = {}
+ -- return whatever the main aspect is
+ return masp, aspt.remote, node, ndef
+end
---[[
-suppasp: "supported_aspects" table
-purpose: form title string
-callback: func(pname, aspect) called on form submit
-isasp: aspect currently set
-]]
-function advtrains.interlocking.show_signal_aspect_selector(pname, p_suppasp, p_purpose, callback, isasp)
- local suppasp = p_suppasp or {
- main = {0, -1}, dst = {false}, shunt = false, info = {},
- }
- local purpose = p_purpose or ""
-
- local form = "size[7,7]label[0.5,0.5;Select Signal Aspect:]"
- form = form.."label[0.5,1;"..purpose.."]"
-
- form = form.."label[0.5,1.5;== Main Signal ==]"
- local selid = 1
- local entries = {}
- for idx, spv in ipairs(suppasp.main) do
- local entry
- if spv == 0 then
- entry = "Halt"
- elseif spv == -1 then
- entry = "Continue at maximum speed"
- elseif not spv then
- entry = "Continue\\, speed limit unchanged (no info)"
- else
- entry = "Continue at speed of "..spv
+-- For the signal at pos, get the "aspect info" table. This contains the speed signalling information at this location
+function signal.get_aspect_info(pos)
+ -- get aspect internal
+ local aspt = signal.aspects[advtrains.encode_pos(pos)]
+ local masp, remote, node, ndef = signal.get_aspect_internal(pos, aspt)
+ -- call into ndef
+ if ndef.advtrains and ndef.advtrains.get_aspect_info then
+ local ai = ndef.advtrains.get_aspect_info
+ if type(ai)=="function" then
+ ai = ai(pos, masp)
end
- -- hack: the crappy formspec system returns the label, not the index. save the index in it.
- entries[idx] = idx.."| "..entry
- if isasp and spv == (isasp.main or false) then
- selid = idx
+ if type(ai)=="table" then
+ atdebug(pos,"aspectinfo",ai)
+ return ai
+ else
+ error("For node "..node.name..": ndef.advtrains.get_aspect_info must be function or table")
end
+
end
- form = form.."dropdown[0.5,2;6;main;"..table.concat(entries, ",")..";"..selid.."]"
+end
-
- form = form.."label[0.5,3;== Shunting ==]"
- if suppasp.shunt == nil then
- local st = 1
- if isasp and isasp.shunt then st=2 end
- form = form.."dropdown[0.5,3.5;6;shunt_free;---,allowed;"..st.."]"
- end
- form = form.."label[0.5,4.5;== Distant Signal ==]"
- local selid = 1
- local entries = {}
- for idx, spv in ipairs(suppasp.dst) do
- local entry
- if spv == 0 then
- entry = "Expect to stop at the next signal"
- elseif spv == -1 then
- entry = "Expect to pass the next signal at maximum speed"
- elseif not spv then
- entry = "No info"
- else
- entry = string.format("Expect to pass the next signal at speed of %d", spv)
+-- Called when either this signal has changed its main aspect
+-- or when this distant signal's currently assigned main signal has changed its aspect
+-- It retrieves the signal's main aspect and aspect info and calls apply_aspect of the node definition
+-- to update the signal's appearance and aspect info
+-- pts: The signal position to update as encoded_pos
+-- returns: the return value of the nodedef call which may be aspect_info
+function signal.reapply_aspect(pts)
+ -- get aspt
+ local aspt = signal.aspects[pts]
+ atdebug("reapply_aspect",advtrains.decode_pos(pts),"aspt",aspt)
+ local pos = advtrains.decode_pos(pts)
+ -- resolve mainaspect table by name
+ local masp, remote, node, ndef = signal.get_aspect_internal(pos, aspt)
+ -- if we have remote, resolve remote
+ local rem_masp, rem_aspi
+ if remote then
+ -- register in remote signal as distant
+ if not signal.distant_refs[remote] then
+ signal.distant_refs[remote] = {}
end
- entries[idx] = idx.."| "..entry
- if isasp and spv == (isasp.dst or false) then
- selid = idx
+ signal.distant_refs[remote][pts] = true
+ local rem_aspt = signal.aspects[remote]
+ atdebug("resolving remote",advtrains.decode_pos(remote),"aspt",rem_aspt)
+ local rem_pos = advtrains.decode_pos(remote)
+ rem_masp, _, _, rem_ndef = signal.get_aspect_internal(rem_pos, rem_aspt)
+ if rem_masp then
+ if rem_ndef.advtrains and rem_ndef.advtrains.get_aspect_info then
+ rem_aspi = rem_ndef.advtrains.get_aspect_info(rem_pos, rem_masp)
+ end
end
end
- form = form.."dropdown[0.5,5;6;dst;"..table.concat(entries, ",")..";"..selid.."]"
+ -- call into ndef
+ atdebug("applying to",pos,": main_asp",masp,"rem_masp",rem_masp,"rem_aspi",rem_aspi)
+ if ndef.advtrains and ndef.advtrains.apply_aspect then
+ ndef.advtrains.apply_aspect(pos, node, masp, rem_masp, rem_aspi)
+ end
+ -- notify trains
+ signal.notify_trains(pos)
+end
- form = form.."button_exit[0.5,6;5,1;save;Save signal aspect]"
-
- 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)
+-- Update this signal's aspect based on the set route
+--
+function signal.update_route_aspect(tcbs, skip_dst_notify)
+ if tcbs.signal then
+ local asp = tcbs.route_aspect or "_halt"
+ local rem = tcbs.route_remote
+ signal.set_aspect(tcbs.signal, asp, rem, skip_dst_notify)
+ end
end
-local function usebool(sup, val, free)
- if sup == nil then
- return val==free
- else
- return sup
+-- Returns how capable the signal is with regards to aspect setting
+-- 0: not a signal at all
+-- 1: signal has get_aspect_info() but the aspect is not variable (e.g. a sign)
+-- 2: signal has apply_aspect() but does not have main aspects (e.g. a pure distant signal)
+-- 3: Full capabilities, signal has main aspects and can be used as main/shunt signal (can be start/endpoint of a route)
+function signal.get_signal_cap_level(pos)
+ local node = advtrains.ndb.get_node_or_nil(pos)
+ local ndef = node and minetest.registered_nodes[node.name]
+ local ndefat = ndef and ndef.advtrains
+ if ndefat and ndefat.get_aspect_info then
+ if ndefat.apply_aspect then
+ if ndefat.main_aspects then
+ return 3
+ end
+ return 2
+ end
+ return 1
end
+ return 0
end
--- other side of hack: extract the index
-local function ddindex(val)
- return tonumber(string.match(val, "^(%d+)|"))
+----------------
+
+function signal.can_dig(pos)
+ return not advtrains.interlocking.db.get_sigd_for_signal(pos)
end
--- TODO use non-hacky way to parse outputs
+function signal.after_dig(pos)
+ -- TODO clear influence point
+ advtrains.interlocking.signal.clear_aspect(pos)
+end
-minetest.register_on_player_receive_fields(function(player, formname, fields)
+function signal.on_rightclick(pos, node, player, itemstack, pointed_thing)
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 maini = ddindex(fields.main)
- if not maini then return end
- local dsti = ddindex(fields.dst)
- if not dsti then return end
- local asp = {
- main = psl.suppasp.main[maini],
- dst = psl.suppasp.dst[dsti],
- shunt = usebool(psl.suppasp.shunt, fields.shunt_free, "allowed"),
- info = {}
- }
- psl.callback(pname, asp)
- end
- else
- players_aspsel[pname] = nil
- end
- end
-
-end)
+ local control = player:get_player_control()
+ advtrains.interlocking.show_signal_form(pos, node, pname, control.aux1)
+end
+
+advtrains.interlocking.signal = signal
diff --git a/advtrains_interlocking/signal_aspect_ui.lua b/advtrains_interlocking/signal_aspect_ui.lua
new file mode 100644
index 0000000..892c53b
--- /dev/null
+++ b/advtrains_interlocking/signal_aspect_ui.lua
@@ -0,0 +1,237 @@
+local F = advtrains.formspec
+
+function advtrains.interlocking.show_signal_form(pos, node, pname, aux_key)
+ local sigd = advtrains.interlocking.db.get_sigd_for_signal(pos)
+ if sigd and not aux_key then
+ advtrains.interlocking.show_signalling_form(sigd, pname)
+ else
+ if advtrains.interlocking.signal.get_signal_cap_level(pos) >= 2 then
+ advtrains.interlocking.show_ip_sa_form(pos, pname)
+ else
+ advtrains.interlocking.show_ip_form(pos, pname)
+ end
+ end
+end
+
+local players_assign_ip = {}
+local players_assign_distant = {}
+
+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
+
+function advtrains.interlocking.make_ip_formspec_component(pos, x, y, w)
+ advtrains.interlocking.db.check_for_duplicate_ip(pos)
+ local pts, connid = advtrains.interlocking.db.get_ip_by_signalpos(pos)
+ if pts then
+ -- display marker
+ local ipos = minetest.string_to_pos(pts)
+ ipmarker(ipos, connid)
+ return table.concat {
+ F.S_label(x, y, "Influence point is set at @1.", string.format("%s/%s", pts, connid)),
+ F.S_button_exit(x, y+0.5, w/2-0.125, "ip_set", "Modify"),
+ F.S_button_exit(x+w/2+0.125, y+0.5, w/2-0.125, "ip_clear", "Clear"),
+ }
+ else
+ return table.concat {
+ F.S_label(x, y, "Influence point is not set."),
+ F.S_button_exit(x, y+0.5, w, "ip_set", "Set influence point"),
+ }
+ end
+end
+
+-- shows small formspec to set the signal influence point
+-- 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 ipform = advtrains.interlocking.make_ip_formspec_component(pos, 0.5, 0.5, 7)
+ local form = {
+ "formspec_version[4]",
+ "size[8,2.25]",
+ ipform,
+ }
+ if not only_notset or not pts then
+ minetest.show_formspec(pname, "at_il_ipsaform_"..minetest.pos_to_string(pos), table.concat(form))
+ end
+end
+
+-- shows larger formspec to set the signal influence point, main aspect and distant signal pos
+-- only_notset: show only if it is not set yet (used by signal tcb assignment)
+function advtrains.interlocking.show_ip_sa_form(pos, pname)
+ if not minetest.check_player_privs(pname, "interlocking") then
+ return
+ end
+ local ipform = advtrains.interlocking.make_ip_formspec_component(pos, 0.5, 0.5, 7)
+ local ma, rpos = advtrains.interlocking.signal.get_aspect(pos)
+ local form = {
+ "formspec_version[4]",
+ "size[8,4.5]",
+ ipform,
+ }
+ -- Create Signal aspect formspec elements
+ local ndef = advtrains.ndb.get_ndef(pos)
+ if ndef and ndef.advtrains then
+ -- main aspect list
+ if ndef.advtrains.main_aspects then
+ local entries = { "<none>" }
+ local sel = 1
+ for i, mae in ipairs(ndef.advtrains.main_aspects) do
+ entries[i+1] = mae.description
+ if ma and ma.name == mae.name then
+ sel = i+1
+ end
+ end
+ form[#form+1] = F.dropdown(0.5, 2.5, 4, "sa_mainaspect", entries, sel, true)
+ end
+ -- distant signal assign (is shown either when main_aspect is not none, or when pure distant signal)
+ if rpos then
+ form[#form+1] = F.button_exit(0.5, 3.5, 4, "sa_undistant", "Dst: " .. minetest.pos_to_string(rpos))
+ elseif (ma and not ma.halt) or not ndef.advtrains.main_aspects then
+ form[#form+1] = F.button_exit(0.5, 3.5, 4, "sa_distant", "<assign distant>")
+ end
+ end
+
+ minetest.show_formspec(pname, "at_il_ipsaform_"..minetest.pos_to_string(pos), table.concat(form))
+end
+
+function advtrains.interlocking.handle_ip_sa_formspec_fields(pname, pos, fields)
+ if not (pos and minetest.check_player_privs(pname, {train_operator=true, interlocking=true})) then
+ return
+ end
+ local ma, rpos = advtrains.interlocking.signal.get_aspect(pos)
+ -- mainaspect dropdown
+ if fields.sa_mainaspect then
+ local idx = tonumber(fields.sa_mainaspect)
+ local new_ma = nil
+ if idx > 1 then
+ local ndef = advtrains.ndb.get_ndef(pos)
+ if ndef and ndef.advtrains and ndef.advtrains.main_aspects then
+ new_ma = ndef.advtrains.main_aspects[idx - 1]
+ end
+ end
+ if new_ma then
+ advtrains.interlocking.signal.set_aspect(pos, new_ma.name, rpos)
+ else
+ -- reset everything
+ advtrains.interlocking.signal.clear_aspect(pos)
+ end
+
+ end
+ -- buttons
+ if fields.ip_set then
+ advtrains.interlocking.init_ip_assign(pos, pname)
+ return
+ elseif fields.ip_clear then
+ advtrains.interlocking.db.clear_ip_by_signalpos(pos)
+ return
+ elseif fields.sa_distant then
+ advtrains.interlocking.init_distant_assign(pos, pname)
+ return
+ elseif fields.sa_undistant then
+ advtrains.interlocking.signal.set_aspect(pos, ma.name, nil)
+ return
+ end
+ -- show the form again unless one of the buttons was clicked
+ if not fields.quit then
+ advtrains.interlocking.show_ip_sa_form(pos, pname)
+ 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_ipsaform_([^_]+)$")
+ local pos
+ if pts then
+ pos = minetest.string_to_pos(pts)
+ end
+ if pos then
+ advtrains.interlocking.handle_ip_sa_formspec_fields(pname, pos, fields)
+ end
+end)
+
+-- inits the signal IP assignment process
+function advtrains.interlocking.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
+
+-- inits the distant signal assignment process
+function advtrains.interlocking.init_distant_assign(pos, pname)
+ if not minetest.check_player_privs(pname, "interlocking") then
+ minetest.chat_send_player(pname, "Insufficient privileges to use this!")
+ return
+ end
+ minetest.chat_send_player(pname, "Set distant signal: Punch the main signal to assign!")
+
+ players_assign_distant[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
+ -- DST assignment
+ signalpos = players_assign_distant[pname]
+ if signalpos then
+ -- get current mainaspect
+ local ma, rpos = advtrains.interlocking.signal.get_aspect(signalpos)
+ -- if punched pos is valid signal then set it as the new remote, otherwise nil
+ local nrpos
+ if advtrains.interlocking.signal.get_signal_cap_level(pos) > 1 then
+ nrpos = pos
+ if not ma or ma.halt then -- make sure that dst is never set without a main aspect (esp. for pure distant signal case)
+ ma = "_default"
+ end
+ advtrains.interlocking.signal.set_aspect(signalpos, ma, nrpos)
+ end
+ players_assign_distant[pname] = nil
+ end
+end)
+
diff --git a/advtrains_interlocking/spec/basic_signalling_spec.lua b/advtrains_interlocking/spec/basic_signalling_spec.lua
new file mode 100644
index 0000000..a4e1e3a
--- /dev/null
+++ b/advtrains_interlocking/spec/basic_signalling_spec.lua
@@ -0,0 +1,106 @@
+--[[
+This file tests a large part of the signaling system, as a lot of tests for the
+signaling system tend to overlap for various parts of the system.
+]]
+
+require("mineunit")
+mineunit("core")
+
+_G.advtrains = {
+ interlocking = {
+ aspect = fixture("../../aspect"),
+ },
+ ndb = {
+ get_node = minetest.get_node,
+ swap_node = minetest.swap_node,
+ }
+}
+
+fixture("advtrains_helpers")
+fixture("../../database")
+sourcefile("distant")
+sourcefile("signal_api")
+sourcefile("signal_aspect_accessors")
+fixture("../../demosignals")
+
+minetest.register_node("advtrains_interlocking:signal_sign", {
+ advtrains = {
+ get_aspcet = function() return {main = 19} end
+ }
+})
+
+local D = advtrains.distant
+local I = advtrains.interlocking
+local A = I.aspect
+
+local stub_aspect_t1 = {
+ free = {main = -1},
+ slow = {main = 6},
+ danger = {main = 0, shunt = false},
+}
+for k, v in pairs(stub_aspect_t1) do
+ stub_aspect_t1[k] = A(v)
+end
+local stub_pos_t1 = {}
+for i = 1, 4 do
+ stub_pos_t1[i] = {x = 1, y = 0, z = i}
+end
+
+world.layout {
+ {stub_pos_t1[1], "advtrains_interlocking:ds_danger"},
+ {stub_pos_t1[2], "advtrains_interlocking:ds_slow"},
+ {stub_pos_t1[3], "advtrains_interlocking:ds_free"},
+ {stub_pos_t1[4], "advtrains_interlocking:signal_sign"},
+}
+
+describe("API for supposed signal aspects", function()
+ it("should load and save data properly", function()
+ local tbl = {_foo = {}}
+ I.load_supposed_aspects(tbl)
+ assert.same(tbl, I.save_supposed_aspects())
+ end)
+ it("should set and get signals properly", function ()
+ local pos = stub_pos_t1[2]
+ local asp = stub_aspect_t1.slow
+ local newasp = A{ main = math.random(1,5) }
+ assert.equal(asp, I.signal_get_aspect(pos))
+ I.signal_set_aspect(pos, newasp)
+ assert.equal(newasp, I.signal_get_aspect(pos))
+ assert.equal(asp, I.signal_get_real_aspect(pos))
+ I.signal_set_aspect(pos, asp)
+ end)
+end)
+
+describe("Distant signaling", function()
+ it("should assign distant signals and set the distant aspect correspondingly", function()
+ for i = 1, 2 do
+ D.assign(stub_pos_t1[i], stub_pos_t1[i+1])
+ end
+ assert.equal(stub_aspect_t1.danger, I.signal_get_aspect(stub_pos_t1[1]))
+ assert.equal(A{main = 6, dst = 0}, I.signal_get_aspect(stub_pos_t1[2]))
+ assert.equal(A{main = -1, dst = 6}, I.signal_get_aspect(stub_pos_t1[3]))
+ end)
+ it("should report assignments properly", function()
+ assert.same({stub_pos_t1[1], "manual"}, {D.get_main(stub_pos_t1[2])})
+ assert.same({[advtrains.encode_pos(stub_pos_t1[3])] = "manual"}, D.get_dst(stub_pos_t1[2]))
+ end)
+ it("should update distant aspects automatically", function()
+ I.signal_set_aspect(stub_pos_t1[2], {main = 2, dst = -1})
+ assert.equal(A{main = 2, dst = 0}, I.signal_get_aspect(stub_pos_t1[2]))
+ assert.equal(A{main = -1, dst = 2}, I.signal_get_aspect(stub_pos_t1[3]))
+ end)
+ it("should unassign signals when one is removed", function()
+ world.set_node(stub_pos_t1[2], "air")
+ assert.same({}, D.get_dst(stub_pos_t1[1]))
+ assert.same({}, {D.get_main(stub_pos_t1[3])})
+ assert.same(stub_aspect_t1.free, I.signal_get_aspect(stub_pos_t1[3]))
+ end)
+ it("should reject signal signs", function()
+ D.assign(stub_pos_t1[1], stub_pos_t1[4])
+ assert.same({}, D.get_dst(stub_pos_t1[1]))
+ assert.same({}, {D.get_main(stub_pos_t1[4])})
+ D.assign(stub_pos_t1[4], stub_pos_t1[1])
+ assert.same({}, D.get_dst(stub_pos_t1[4]))
+ assert.same({}, {D.get_main(stub_pos_t1[1])})
+ end)
+end)
diff --git a/advtrains_interlocking/spec/fixtures/advtrains_helpers.lua b/advtrains_interlocking/spec/fixtures/advtrains_helpers.lua
new file mode 120000
index 0000000..9b0ab67
--- /dev/null
+++ b/advtrains_interlocking/spec/fixtures/advtrains_helpers.lua
@@ -0,0 +1 @@
+../../../advtrains/helpers.lua \ No newline at end of file
diff --git a/advtrains_interlocking/spec/mineunit.conf b/advtrains_interlocking/spec/mineunit.conf
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/advtrains_interlocking/spec/mineunit.conf
diff --git a/advtrains_interlocking/spec/signal_group_spec.lua b/advtrains_interlocking/spec/signal_group_spec.lua
new file mode 100644
index 0000000..bc9d007
--- /dev/null
+++ b/advtrains_interlocking/spec/signal_group_spec.lua
@@ -0,0 +1,95 @@
+require "mineunit"
+mineunit("core")
+
+_G.advtrains = {
+ interlocking = {
+ aspect = sourcefile("aspect"),
+ },
+ ndb = {
+ get_node = minetest.get_node,
+ swap_node = minetest.swap_node,
+ }
+}
+
+fixture("advtrains_helpers")
+sourcefile("database")
+sourcefile("signal_api")
+sourcefile("distant")
+sourcefile("signal_aspect_accessors")
+
+local A = advtrains.interlocking.aspect
+local D = advtrains.distant
+local I = advtrains.interlocking
+local N = advtrains.ndb
+
+local groupdef = {
+ name = "foo",
+ aspects = {
+ proceed = {main = -1},
+ caution = {},
+ danger = {main = 0},
+ "proceed",
+ {"caution"},
+ "danger",
+ },
+}
+
+for k, v in pairs(groupdef.aspects) do
+ minetest.register_node("advtrains_interlocking:" .. k, {
+ advtrains = {
+ supported_aspects = {
+ group = "foo",
+ },
+ get_aspect = function() return A{group = "foo", name = k} end,
+ set_aspect = function(pos, _, name)
+ N.swap_node(pos, {name = "advtrains_interlocking:" .. name})
+ end,
+ }
+ })
+end
+
+local origin = vector.new(0, 0, 0)
+local dstpos = vector.new(0, 0, 1)
+
+world.layout {
+ {origin, "advtrains_interlocking:danger"},
+ {dstpos, "advtrains_interlocking:proceed"},
+}
+
+describe("signal group registration", function()
+ it("should work", function()
+ A.register_group(groupdef)
+ assert(A.get_group_definition("foo"))
+ end)
+ it("should only be allowed once for the same group", function()
+ assert.has.errors(function() A.register_group(type2def) end)
+ end)
+ it("should handle nonexistant groups", function()
+ assert.is_nil(A.get_group_definition("something_else"))
+ end)
+ it("should reject invalid definitions", function()
+ assert.has.errors(function() A.register_group({}) end)
+ assert.has.errors(function() A.register_group({name="",label={}}) end)
+ assert.has.errors(function() A.register_group({name="",aspects={}}) end)
+ end)
+end)
+
+describe("signal aspect", function()
+ it("should handle empty fields properly", function()
+ assert.equal(A{main = 0}, A{group="foo", name="danger"}:to_group())
+ end)
+ it("should be converted properly", function()
+ assert.equal(A{main = 0}, A{group="foo", name="danger"})
+ assert.equal(A{}, A{group="foo", name="caution"})
+ assert.equal(A{main = -1}, A{group="foo", name="proceed"})
+ end)
+end)
+
+describe("signals in groups", function()
+ it("should support distant signaling", function()
+ assert.equal("caution", A():adjust_distant(A{group="foo",name="danger"}).name)
+ assert.equal("proceed", A():adjust_distant(A{group="foo",name="caution"}).name)
+ assert.equal("proceed", A():adjust_distant(A{group="foo",name="proceed"}).name)
+ assert.equal("danger", A{group="foo",name="danger"}:adjust_distant{}.name)
+ end)
+end)
diff --git a/advtrains_interlocking/tcb_ts_ui.lua b/advtrains_interlocking/tcb_ts_ui.lua
index 9e7fcd4..60be5f3 100755
--- a/advtrains_interlocking/tcb_ts_ui.lua
+++ b/advtrains_interlocking/tcb_ts_ui.lua
@@ -15,6 +15,7 @@ local lntrans = { "A", "B" }
local function sigd_to_string(sigd)
return minetest.pos_to_string(sigd.p).." / "..lntrans[sigd.s]
end
+advtrains.interlocking.sigd_to_string = sigd_to_string
minetest.register_node("advtrains_interlocking:tcb_node", {
drawtype = "mesh",
@@ -185,7 +186,7 @@ minetest.register_on_punchnode(function(pos, node, player, pointed_thing)
local is_signal = minetest.get_item_group(node.name, "advtrains_signal") >= 2
if is_signal then
local ndef = minetest.registered_nodes[node.name]
- if ndef and ndef.advtrains and ndef.advtrains.set_aspect then
+ if ndef and ndef.advtrains and ndef.advtrains.apply_aspect then
local tcbs = ildb.get_tcbs(sigd)
if tcbs then
tcbs.signal = pos
@@ -463,7 +464,7 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
ts.route = nil
for _, sigd in ipairs(ts.tc_breaks) do
local tcbs = ildb.get_tcbs(sigd)
- advtrains.interlocking.update_signal_aspect(tcbs)
+ advtrains.interlocking.signal.update_route_aspect(tcbs)
end
minetest.chat_send_player(pname, "Reset track section "..ts_id.."!")
end
@@ -580,7 +581,7 @@ function advtrains.interlocking.show_signalling_form(sigd, pname, sel_rte, calle
if not tcbs.signal then return end
if not tcbs.routes then tcbs.routes = {} end
- local form = "size[7,10]label[0.5,0.5;Signal at "..minetest.pos_to_string(sigd.p).."]"
+ local form = "size[7,10.25]label[0.5,0.5;Signal at "..minetest.pos_to_string(sigd.p).."]"
form = form.."field[0.8,1.5;5.2,1;name;Signal name;"..minetest.formspec_escape(tcbs.signal_name or "").."]"
form = form.."button[5.5,1.2;1,1;setname;Set]"
@@ -640,12 +641,7 @@ function advtrains.interlocking.show_signalling_form(sigd, pname, sel_rte, calle
if hasprivs then
form = form.."button[0.5,8;2.5,1;newroute;New Route]"
form = form.."button[ 3,8;2.5,1;unassign;Unassign Signal]"
- form = form.."button[ 3,9;2.5,1;influp;Influence Point]"
- end
- if tcbs.ars_disabled then
- form = form.."button[0.5,9;2.5,1;arsenable;Enable ARS]"
- else
- form = form.."button[0.5,9;2.5,1;arsdisable;Disable ARS]"
+ form = form..string.format("checkbox[0.5,8.75;ars;Automatic routesetting;%s]", not tcbs.ars_disabled)
end
elseif sigd_equal(tcbs.route_origin, sigd) then
-- something has gone wrong: tcbs.routeset should have been set...
@@ -663,7 +659,7 @@ function advtrains.interlocking.show_signalling_form(sigd, pname, sel_rte, calle
-- always a good idea to update the signal aspect
if not called_from_form_update then
-- FIX prevent a callback loop
- advtrains.interlocking.update_signal_aspect(tcbs)
+ advtrains.interlocking.signal.update_route_aspect(tcbs)
end
end
@@ -760,7 +756,8 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
local signal_pos = tcbs.signal
ildb.set_sigd_for_signal(signal_pos, nil)
tcbs.signal = nil
- tcbs.aspect = nil
+ tcbs.route_aspect = nil
+ tcbs.route_remote = nil
minetest.close_formspec(pname, formname)
minetest.chat_send_player(pname, "Signal has been unassigned. Name and routes are kept for reuse.")
return
@@ -768,16 +765,9 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
minetest.chat_send_player(pname, "Please cancel route first!")
end
end
- if fields.influp and hasprivs then
- advtrains.interlocking.show_ip_form(tcbs.signal, pname)
- return
- end
- if tcbs.ars_disabled and fields.arsenable then
- tcbs.ars_disabled = nil
- end
- if not tcbs.ars_disabled and fields.arsdisable then
- tcbs.ars_disabled = true
+ if fields.ars then
+ tcbs.ars_disabled = not minetest.is_yes(fields.ars)
end
if fields.auto then
diff --git a/advtrains_interlocking/train_sections.lua b/advtrains_interlocking/train_sections.lua
index 47072dc..41da747 100644
--- a/advtrains_interlocking/train_sections.lua
+++ b/advtrains_interlocking/train_sections.lua
@@ -86,24 +86,22 @@ local function setsection(tid, train, ts_id, ts, sigd)
advtrains.interlocking.route.cancel_route_from(ts.route.origin)
atwarn("Route was cancelled.")
else
- -- train entered route regularily. Reset route and signal
- tcbs.route_committed = nil
- tcbs.route_comitted = nil -- TODO compatibility cleanup
- tcbs.aspect = nil
- tcbs.route_origin = nil
- advtrains.interlocking.update_signal_aspect(tcbs)
- if tcbs.signal and sigd_equal(ts.route.entry, ts.route.origin) then
- if tcbs.route_auto and tcbs.routeset then
- --atdebug("Resetting route (",ts.route.origin,")")
- advtrains.interlocking.route.update_route(ts.route.origin, tcbs)
- else
- tcbs.routeset = nil
- end
- end
+ -- train entered route regularily.
end
ts.route = nil
end
if tcbs.signal then
+ -- Reset route and signal
+ -- Note that the hit-route case is already handled by cancel_route_from
+ -- this code only handles signal at entering tcb and also triggers for non-route ts
+ tcbs.route_committed = nil
+ tcbs.route_aspect = nil
+ tcbs.route_remote = nil
+ tcbs.route_origin = nil
+ if not tcbs.route_auto then
+ tcbs.routeset = nil
+ end
+ advtrains.interlocking.signal.update_route_aspect(tcbs)
advtrains.interlocking.route.update_route(sigd, tcbs)
end
end