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/approach.lua9
-rw-r--r--advtrains_interlocking/ars.lua4
-rw-r--r--advtrains_interlocking/aspect.lua296
-rw-r--r--advtrains_interlocking/database.lua32
-rw-r--r--advtrains_interlocking/demosignals.lua6
-rw-r--r--advtrains_interlocking/distant.lua200
-rw-r--r--advtrains_interlocking/distant_signals.lua83
-rw-r--r--advtrains_interlocking/distant_ui.lua141
-rw-r--r--advtrains_interlocking/init.lua11
-rw-r--r--advtrains_interlocking/routesetting.lua50
-rw-r--r--advtrains_interlocking/signal_api.lua446
-rw-r--r--advtrains_interlocking/signal_aspect_accessors.lua163
-rw-r--r--advtrains_interlocking/signal_aspect_ui.lua262
-rw-r--r--advtrains_interlocking/spec/ars_spec.lua67
-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.lua66
-rw-r--r--advtrains_interlocking/train_sections.lua7
21 files changed, 1609 insertions, 521 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/approach.lua b/advtrains_interlocking/approach.lua
index f60468a..eecf09a 100644
--- a/advtrains_interlocking/approach.lua
+++ b/advtrains_interlocking/approach.lua
@@ -14,19 +14,19 @@ local SHUNT_SPEED_MAX = advtrains.SHUNT_SPEED_MAX
local il = advtrains.interlocking
-local function get_over_function(speed, shunt)
+local function get_over_function(speed, shunt, asptype)
return function(pos, id, train, index, speed, lzbdata)
if speed == 0 and minetest.settings:get_bool("at_il_force_lzb_halt") then
atwarn(id,"overrun LZB 0 restriction (red signal) ",pos)
-- Set train 1 index backward. Hope this does not lead to bugs...
--train.index = index - 0.5
- train.speed_restriction = 0
+ advtrains.speed.set_restriction(train, "main", 0)
--TODO temporary
--advtrains.drb_dump(id)
--error("Debug: "..id.." triggered LZB-0")
else
- train.speed_restriction = speed
+ advtrains.speed.set_restriction(train, asptype, speed or -1)
train.is_shunt = shunt
end
--atdebug("train drove over IP: speed=",speed,"shunt=",shunt)
@@ -94,6 +94,7 @@ advtrains.tnc_register_on_approach(function(pos, id, train, index, has_entered,
end
-- nspd can now be: 1. !=0: new speed restriction, 2. =0: stop here or 3. nil: keep travspd
if nspd then
+ travspd = nspd
if nspd == -1 then
travspd = nil
else
@@ -106,7 +107,7 @@ advtrains.tnc_register_on_approach(function(pos, id, train, index, has_entered,
lspd = travspd
local udata = {signal_pos = spos}
- local callback = get_over_function(lspd, travsht)
+ local callback = get_over_function(lspd, travsht, asp.type)
lzbdata.il_shunt = travsht
lzbdata.il_speed = travspd
--atdebug("new lzbdata",lzbdata)
diff --git a/advtrains_interlocking/ars.lua b/advtrains_interlocking/ars.lua
index 434ae2c..4f50df9 100644
--- a/advtrains_interlocking/ars.lua
+++ b/advtrains_interlocking/ars.lua
@@ -133,9 +133,11 @@ function advtrains.interlocking.ars_check(sigd, train)
local tcbs = il.db.get_tcbs(sigd)
if not tcbs or not tcbs.routes then return end
- if tcbs.ars_disabled then
+ if tcbs.ars_disabled or tcbs.ars_ignore_next then
-- No-ARS mode of signal.
-- ignore...
+ -- Note: ars_ignore_next is set by signalling formspec when route is cancelled
+ tcbs.ars_ignore_next = nil
return
end
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 ee21db9..c5ae906 100644
--- a/advtrains_interlocking/database.lua
+++ b/advtrains_interlocking/database.lua
@@ -131,6 +131,12 @@ function ildb.load(data)
if data.npr_rails then
advtrains.interlocking.npr_rails = data.npr_rails
end
+ if data.supposed_aspects then
+ advtrains.interlocking.load_supposed_aspects(data.supposed_aspects)
+ end
+ if data.distant then
+ advtrains.distant.load(data.distant)
+ end
--COMPATIBILITY to Signal aspect format
-- TODO remove in time...
@@ -173,6 +179,8 @@ function ildb.save()
rs_callbacks = advtrains.interlocking.route.rte_callbacks,
influence_points = influence_points,
npr_rails = advtrains.interlocking.npr_rails,
+ supposed_aspects = advtrains.interlocking.save_supposed_aspects(),
+ distant = advtrains.distant.save(),
}
end
@@ -195,8 +203,6 @@ TCB data structure
signal_name = <string> -- The human-readable name of the signal, only for documenting purposes
routes = { <route definition> } -- a collection of routes from this signal
route_auto = <boolean> -- When set, we will automatically re-set the route (designated by routeset)
- distant = { <sigd1> ... } -- a collection of sigd that points to a side of a TCB with a distant signal for this current signal.
- distant_of = { <sigd>, <index> } -- the reverse of the above field, along with the index of the entry in the list (for easier lookup)
},
-- This is the "B" side of the TCB
[2] = { -- Variant: end of track-circuited area (initial state of TC)
@@ -636,6 +642,28 @@ function ildb.get_ip_by_signalpos(spos)
end
end
end
+function ildb.check_for_duplicate_ip(spos)
+ local main_ip_found = false
+ -- first pass: check for duplicates
+ for pts,tab in pairs(influence_points) do
+ for connid,pos in pairs(tab) do
+ if vector.equals(pos, spos) then
+ if main_ip_found then
+ atwarn("Signal at",spos,": Deleting duplicate signal influence point at",pts,"/",connid)
+ tab[connid] = nil
+ end
+ main_ip_found = true
+ end
+ end
+ end
+ -- second pass: delete empty tables
+ for pts,tab in pairs(influence_points) do
+ if not tab[1] and not tab[2] then -- only those two connids may exist
+ influence_points[pts] = nil
+ end
+ end
+end
+
-- clear signal assignment given the signal position
function ildb.clear_ip_by_signalpos(spos)
local pts, connid = ildb.get_ip_by_signalpos(spos)
diff --git a/advtrains_interlocking/demosignals.lua b/advtrains_interlocking/demosignals.lua
index 1c1b8b2..de6926a 100644
--- a/advtrains_interlocking/demosignals.lua
+++ b/advtrains_interlocking/demosignals.lua
@@ -50,7 +50,7 @@ minetest.register_node("advtrains_interlocking:ds_danger", {
},
on_rightclick = advtrains.interlocking.signal_rc_handler,
can_dig = advtrains.interlocking.signal_can_dig,
- after_dig_node = advtrains.interlocking.signal_after_dig,
+ after_destruct = advtrains.interlocking.signal_after_dig,
})
minetest.register_node("advtrains_interlocking:ds_free", {
description = "Demo signal at Free",
@@ -71,7 +71,7 @@ minetest.register_node("advtrains_interlocking:ds_free", {
},
on_rightclick = advtrains.interlocking.signal_rc_handler,
can_dig = advtrains.interlocking.signal_can_dig,
- after_dig_node = advtrains.interlocking.signal_after_dig,
+ after_destruct = advtrains.interlocking.signal_after_dig,
})
minetest.register_node("advtrains_interlocking:ds_slow", {
description = "Demo signal at Slow",
@@ -92,6 +92,6 @@ minetest.register_node("advtrains_interlocking:ds_slow", {
},
on_rightclick = advtrains.interlocking.signal_rc_handler,
can_dig = advtrains.interlocking.signal_can_dig,
- after_dig_node = advtrains.interlocking.signal_after_dig,
+ after_destruct = advtrains.interlocking.signal_after_dig,
})
diff --git a/advtrains_interlocking/distant.lua b/advtrains_interlocking/distant.lua
new file mode 100644
index 0000000..32ada82
--- /dev/null
+++ b/advtrains_interlocking/distant.lua
@@ -0,0 +1,200 @@
+--- Distant signaling.
+-- This module implements a database backend for distant signal assignments.
+-- The actual modifications to signal aspects are still done by signal aspect accessors.
+-- @module advtrains.interlocking.distant
+
+local db_distant = {}
+local db_distant_of = {}
+
+local pts = advtrains.encode_pos
+local stp = advtrains.decode_pos
+
+--- Replace the distant signal assignment database.
+-- @function load
+-- @param db The new database to load.
+local function db_load(x)
+ if type(x) ~= "table" then
+ return
+ end
+ db_distant = x.distant
+ db_distant_of = x.distant_of
+end
+
+--- Retrieve the current distant signal assignment database.
+-- @function save
+-- @return The current database.
+local function db_save()
+ return {
+ distant = db_distant,
+ distant_of = db_distant_of,
+ }
+end
+
+local update_signal, update_main, update_dst
+
+--- Unassign a distant signal.
+-- @function unassign_dst
+-- @param dst The position of the distant signal.
+-- @param[opt=false] force Whether to skip callbacks.
+local function unassign_dst(dst, force)
+ local pts_dst = pts(dst)
+ local main = db_distant_of[pts_dst]
+ db_distant_of[pts_dst] = nil
+ if main then
+ local pts_main = main[1]
+ local t = db_distant[pts_main]
+ if t then
+ t[pts_dst] = nil
+ end
+ end
+ if not force then
+ update_dst(dst)
+ end
+end
+
+--- Unassign a main signal.
+-- @function unassign_main
+-- @param main The position of the main signal.
+-- @param[opt=false] force Whether to skip callbacks.
+local function unassign_main(main, force)
+ local pts_main = pts(main)
+ local t = db_distant[pts_main]
+ if not t then
+ return
+ end
+ for pts_dst in pairs(t) do
+ local realmain = db_distant_of[pts_dst]
+ if realmain and realmain[1] == pts_main then
+ db_distant_of[pts_dst] = nil
+ if not force then
+ local dst = stp(pts_dst)
+ update_dst(dst)
+ end
+ end
+ end
+ db_distant[pts_main] = nil
+end
+
+--- Remove all (main and distant) signal assignments from a signal.
+-- @function unassign_all
+-- @param pos The position of the signal.
+-- @param[opt=false] force Whether to skip callbacks.
+local function unassign_all(pos, force)
+ unassign_main(pos)
+ unassign_dst(pos, force)
+end
+
+--- Check whether a signal is "appropriate" for the distant signal system.
+-- Currently, a signal is considered appropriate if its signal aspect can be set.
+-- @function appropriate_signal
+-- @param pos The position of the signal
+local function appropriate_signal(pos)
+ local node = advtrains.ndb.get_node(pos)
+ local ndef = minetest.registered_nodes[node.name] or {}
+ if not ndef then
+ return false
+ end
+ local atdef = ndef.advtrains
+ if not atdef then
+ return false
+ end
+ return atdef.supported_aspects and atdef.set_aspect and true
+end
+
+--- Assign a distant signal to a main signal.
+-- @function assign
+-- @param main The position of the main signal.
+-- @param dst The position of the distant signal.
+-- @param[opt="manual"] by The method of assignment.
+-- @param[opt=false] skip_update Whether to skip callbacks.
+local function assign(main, dst, by, skip_update)
+ if not (appropriate_signal(main) and appropriate_signal(dst)) then
+ return
+ end
+ local pts_main = pts(main)
+ local pts_dst = pts(dst)
+ local t = db_distant[pts_main]
+ if not t then
+ t = {}
+ db_distant[pts_main] = t
+ end
+ if not by then
+ by = "manual"
+ end
+ unassign_dst(dst, true)
+ t[pts_dst] = by
+ db_distant_of[pts_dst] = {pts_main, by}
+ if not skip_update then
+ update_dst(dst)
+ end
+end
+
+--- Get the distant signals assigned to a main signal.
+-- @function get_distant
+-- @param main The position of the main signal.
+-- @treturn {[pos]=by,...} A table of distant signals, with the positions encoded using `advtrains.encode_pos`.
+local function get_distant(main)
+ local pts_main = pts(main)
+ return db_distant[pts_main] or {}
+end
+
+--- Get the main signal assigned the a distant signal.
+-- @function get_main
+-- @param dst The position of the distant signal.
+-- @return The position of the main signal.
+-- @return The method of assignment.
+local function get_main(dst)
+ local pts_dst = pts(dst)
+ local main = db_distant_of[pts_dst]
+ if not main then
+ return
+ end
+ if main[1] then
+ return stp(main[1]), unpack(main, 2)
+ else
+ return unpack(main)
+ end
+end
+
+--- Update all distant signals assigned to a main signal.
+-- @function update_main
+-- @param main The position of the main signal.
+update_main = function(main)
+ local pts_main = pts(main)
+ local t = get_distant(main)
+ for pts_dst in pairs(t) do
+ local dst = stp(pts_dst)
+ advtrains.interlocking.signal_readjust_aspect(dst)
+ end
+end
+
+--- Update the aspect of a distant signal.
+-- @function update_dst
+-- @param dst The position of the distant signal.
+update_dst = function(dst)
+ advtrains.interlocking.signal_readjust_aspect(dst)
+end
+
+--- Update the aspect of a combined (main and distant) signal and all distant signals assigned to it.
+-- @function update_signal
+-- @param pos The position of the signal.
+update_signal = function(pos)
+ update_main(pos)
+ update_dst(pos)
+end
+
+advtrains.distant = {
+ load = db_load,
+ save = db_save,
+ assign = assign,
+ unassign_dst = unassign_dst,
+ unassign_main = unassign_main,
+ unassign_all = unassign_all,
+ get_distant = get_distant,
+ get_dst = get_distant,
+ get_main = get_main,
+ update_main = update_main,
+ update_dst = update_dst,
+ update_signal = update_signal,
+ appropriate_signal = appropriate_signal,
+}
diff --git a/advtrains_interlocking/distant_signals.lua b/advtrains_interlocking/distant_signals.lua
deleted file mode 100644
index 0da1c10..0000000
--- a/advtrains_interlocking/distant_signals.lua
+++ /dev/null
@@ -1,83 +0,0 @@
-local interlocking = advtrains.interlocking
-local ildb = advtrains.interlocking.db
-
-local function update_distant(tcbs)
- if not (tcbs and tcbs.signal) then return end
- if not tcbs.aspect then tcbs.aspect = table.copy(interlocking.DANGER) end
- local asp = tcbs.aspect
- if tcbs.distant_of then
- asp.dst = (ildb.get_tcbs(tcbs.distant_of[1]).aspect or interlocking.DANGER).main
- end
- interlocking.update_signal_aspect(tcbs)
- if tcbs.distant then
- local dst = tcbs.distant
- for i = 1, #dst do
- local s = ildb.get_tcbs(dst[i])
- if not s.aspect then s.aspect = table.copy(interlocking.DANGER) end
- s.aspect.dst = asp.main
- interlocking.update_signal_aspect(s)
- end
- end
-end
-
-local function unassign_distant(dsts)
- if not dsts then return end
- local dof = dsts.distant_of
- if not dof then return end
- if dsts.signal and dsts.aspect then
- dsts.aspect.dst = nil
- interlocking.update_signal_aspect(dsts)
- end
- local sigd, idx = dof[1], dof[2]
- local tcbs = ildb.get_tcbs(sigd)
- local dst = tcbs.distant
- dsts.distant_of = nil
- if idx == #dst then
- dst[#dst] = nil
- else
- local ent = dst[#dst]
- dst[idx] = ent
- dst[#dst] = nil
- local repl = ildb.get_tcbs(ent)
- repl.distant_of[2] = idx
- end
-end
-
-local function assign_distant(sigd, dstd)
- if not sigd then return end
- if not dstd then return end
- local tcbs = ildb.get_tcbs(sigd)
- local dsts = ildb.get_tcbs(dstd)
- unassign_distant(dsts)
- if not (tcbs.signal and dsts.signal) then return end
- local dst = tcbs.distant
- if not dst then
- dst = {}
- tcbs.distant = dst
- end
- local newidx = #dst+1
- dsts.distant_of = {sigd, newidx}
- dst[newidx] = dstd
- update_distant(dsts)
-end
-
-local function remove_distant(tcbs)
- if not tcbs then return end
- if tcbs.distant_of then
- unassign_distant(tcbs)
- end
- if tcbs.distant then
- local dst = tcbs.distant
- for i = #dst, 1, -1 do
- local s = ildb.get_tcbs(dst[i])
- unassign_distant(s)
- end
- end
-end
-
-interlocking.distant = {
- assign = assign_distant,
- unassign = unassign_distant,
- remove = remove_distant,
- update = update_distant,
-}
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 cfbb8e5..4d959cc 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,11 +12,16 @@ 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.."distant.lua")
+dofile(modpath.."distant_ui.lua")
+dofile(modpath.."signal_aspect_accessors.lua")
dofile(modpath.."signal_api.lua")
+dofile(modpath.."signal_aspect_ui.lua")
dofile(modpath.."demosignals.lua")
dofile(modpath.."train_sections.lua")
-dofile(modpath.."distant_signals.lua")
dofile(modpath.."route_prog.lua")
dofile(modpath.."routesetting.lua")
dofile(modpath.."tcb_ts_ui.lua")
diff --git a/advtrains_interlocking/routesetting.lua b/advtrains_interlocking/routesetting.lua
index d801608..9973569 100644
--- a/advtrains_interlocking/routesetting.lua
+++ b/advtrains_interlocking/routesetting.lua
@@ -44,13 +44,18 @@ function ilrs.set_route(signal, route, try)
local i = 1
local rtename = route.name
local signalname = ildb.get_tcbs(signal).signal_name
- local c_tcbs, c_ts_id, c_ts, c_rseg, c_lckp, p_ssigd
+ local c_tcbs, c_ts_id, c_ts, c_rseg, c_lckp
+ 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 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
@@ -112,16 +117,9 @@ function ilrs.set_route(signal, route, try)
}
if c_tcbs.signal then
c_tcbs.route_committed = true
- local asp = route.aspect or advtrains.interlocking.GENERIC_FREE
- asp.dst = nil
- c_tcbs.aspect = asp
+ c_tcbs.aspect = route.aspect or advtrains.interlocking.FULL_FREE
c_tcbs.route_origin = signal
- advtrains.interlocking.distant.update(c_tcbs)
- -- Update the previous distant signal
- if p_ssigd then
- advtrains.interlocking.distant.assign(c_sigd, p_ssigd)
- end
- p_ssigd = c_sigd
+ signals[#signals+1] = c_tcbs
end
end
-- advance
@@ -130,15 +128,25 @@ function ilrs.set_route(signal, route, try)
i = i + 1
end
- if c_sigd and p_ssigd then
+ -- Distant signaling
+ local lastsig = nil
+ if c_sigd then
local e_tcbs = ildb.get_tcbs(c_sigd)
- if e_tcbs.signal then
- if p_stcb then
- p_stcb.aspect.dst = (e_tcbs.aspect or advtrains.interlocking.DANGER).main
- advtrains.interlocking.update_signal_aspect(p_stcb)
+ local pos = e_tcbs and e_tcbs.signal
+ if pos then
+ lastsig = pos
+ end
+ end
+ for i = #signals, 1, -1 do
+ if lastsig then
+ local tcbs = signals[i]
+ local pos = tcbs.signal
+ local _, assigned_by = advtrains.distant.get_main(pos)
+ if (not nodst) and (not assigned_by or assigned_by == "routesetting") then
+ advtrains.distant.assign(lastsig, pos, "routesetting", true)
end
+ advtrains.interlocking.update_signal_aspect(tcbs, i ~= 1)
end
- advtrains.interlocking.distant.assign(c_sigd, p_ssigd)
end
return true
@@ -247,6 +255,13 @@ function ilrs.cancel_route_from(sigd)
c_tcbs.route_auto = nil
c_tcbs.route_origin = nil
+ if c_tcbs.signal then
+ local pos = c_tcbs.signal
+ local _, assigned_by = advtrains.distant.get_main(pos)
+ if assigned_by == "routesetting" then
+ advtrains.distant.unassign_dst(pos, true)
+ end
+ end
advtrains.interlocking.update_signal_aspect(c_tcbs)
c_ts_id = c_tcbs.ts_id
@@ -326,7 +341,8 @@ 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
diff --git a/advtrains_interlocking/signal_api.lua b/advtrains_interlocking/signal_api.lua
index a969378..ce8854a 100644
--- a/advtrains_interlocking/signal_api.lua
+++ b/advtrains_interlocking/signal_api.lua
@@ -1,173 +1,9 @@
-- Signal API implementation
-
---[[
-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.
-
-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. 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,
-}
-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 F = advtrains.formspec
local DANGER = {
main = 0,
- dst = false,
shunt = false,
}
advtrains.interlocking.DANGER = DANGER
@@ -177,32 +13,18 @@ advtrains.interlocking.GENERIC_FREE = {
shunt = false,
dst = false,
}
+advtrains.interlocking.FULL_FREE = {
+ main = -1,
+ shunt = 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
- 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
+advtrains.interlocking.signal_convert_aspect_if_necessary = advtrains.interlocking.aspect
-function advtrains.interlocking.update_signal_aspect(tcbs)
+function advtrains.interlocking.update_signal_aspect(tcbs, skipdst)
if tcbs.signal then
local asp = tcbs.aspect or DANGER
- advtrains.interlocking.signal_set_aspect(tcbs.signal, asp)
+ advtrains.interlocking.signal_set_aspect(tcbs.signal, asp, skipdst)
end
end
@@ -212,17 +34,9 @@ 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)
- 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)
- end
+ advtrains.interlocking.signal_clear_aspect(pos)
+ advtrains.distant.unassign_all(pos, true)
end
-- should be called when aspect has changed on this signal.
@@ -230,8 +44,10 @@ 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)
-
- advtrains.invalidate_all_paths_ahead(ipos)
+
+ -- 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
function advtrains.interlocking.signal_rc_handler(pos, node, player, itemstack, pointed_thing)
@@ -241,7 +57,10 @@ function advtrains.interlocking.signal_rc_handler(pos, node, player, itemstack,
advtrains.interlocking.show_ip_form(pos, pname)
return
end
-
+ advtrains.interlocking.show_signal_form(pos, node, pname)
+end
+
+function advtrains.interlocking.show_signal_form(pos, node, pname)
local sigd = advtrains.interlocking.db.get_sigd_for_signal(pos)
if sigd then
advtrains.interlocking.show_signalling_form(sigd, pname)
@@ -252,12 +71,12 @@ function advtrains.interlocking.signal_rc_handler(pos, node, player, itemstack,
local function callback(pname, aspect)
advtrains.interlocking.signal_set_aspect(pos, aspect)
end
- local isasp = ndef.advtrains.get_aspect(pos, node)
-
+ local isasp = advtrains.interlocking.signal_get_aspect(pos, node)
+
advtrains.interlocking.show_signal_aspect_selector(
pname,
ndef.advtrains.supported_aspects,
- "Set aspect manually", callback,
+ pos, callback,
isasp)
else
--static signal - only IP
@@ -278,38 +97,13 @@ function advtrains.interlocking.signal_get_supposed_aspect(pos)
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
- return convert_aspect_if_necessary(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
@@ -319,48 +113,72 @@ local function ipmarker(ipos, connid)
})
end
--- shows small info form for signal IP state/assignment
+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
+ 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"),
+ }, pts, connid
+ 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 info form for signal properties
+-- This function is named show_ip_form because it was originally only intended
+-- for assigning/changing the 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 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)
+ local ipform, pts, connid = advtrains.interlocking.make_ip_formspec_component(pos, 0.5, 0.5, 7)
+ local form = {
+ "formspec_version[4]",
+ "size[8,2.25]",
+ ipform,
+ }
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 advtrains.distant.appropriate_signal(pos) then
+ form[#form+1] = advtrains.interlocking.make_dst_formspec_component(pos, 0.5, 2, 7, 4.25)
+ form[2] = "size[8,6.75]"
+ end
+ form = table.concat(form)
if not only_notset or not pts then
- minetest.show_formspec(pname, "at_il_ipassign_"..minetest.pos_to_string(pos), form)
+ minetest.show_formspec(pname, "at_il_propassign_"..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
+function advtrains.interlocking.handle_ip_formspec_fields(pname, pos, fields)
+ if not (pos and minetest.check_player_privs(pname, {train_operator=true, interlocking=true})) then
return
end
- local pts = string.match(formname, "^at_il_ipassign_([^_]+)$")
+ if fields.ip_set then
+ advtrains.interlocking.signal_init_ip_assign(pos, pname)
+ elseif fields.ip_clear then
+ advtrains.interlocking.db.clear_ip_by_signalpos(pos)
+ 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_propassign_([^_]+)$")
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
+ advtrains.interlocking.handle_ip_formspec_fields(pname, pos, fields)
+ advtrains.interlocking.handle_dst_formspec_fields(pname, pos, fields)
end
end)
@@ -410,129 +228,3 @@ minetest.register_on_punchnode(function(pos, node, player, pointed_thing)
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
-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
- 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
- end
- end
- form = form.."dropdown[0.5,2;6;main;"..table.concat(entries, ",")..";"..selid.."]"
-
-
- 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)
- end
- entries[idx] = idx.."| "..entry
- if isasp and spv == (isasp.dst or false) then
- selid = idx
- end
- end
- form = form.."dropdown[0.5,5;6;dst;"..table.concat(entries, ",")..";"..selid.."]"
-
- 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)
-end
-
-local function usebool(sup, val, free)
- if sup == nil then
- return val==free
- else
- return sup
- end
-end
-
--- other side of hack: extract the index
-local function ddindex(val)
- return tonumber(string.match(val, "^(%d+)|"))
-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 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)
diff --git a/advtrains_interlocking/signal_aspect_accessors.lua b/advtrains_interlocking/signal_aspect_accessors.lua
new file mode 100644
index 0000000..d91df31
--- /dev/null
+++ b/advtrains_interlocking/signal_aspect_accessors.lua
@@ -0,0 +1,163 @@
+--- Signal aspect accessors
+-- @module advtrains.interlocking
+
+local A = advtrains.interlocking.aspect
+local D = advtrains.distant
+local I = advtrains.interlocking
+local N = advtrains.ndb
+local pts = advtrains.roundfloorpts
+
+local get_aspect
+
+local supposed_aspects = {}
+
+--- Replace the signal aspect cache.
+-- @function load_supposed_aspects
+-- @param db The new database.
+function I.load_supposed_aspects(tbl)
+ if tbl then
+ supposed_aspects = {}
+ for k, v in pairs(tbl) do
+ supposed_aspects[k] = A(v)
+ end
+ end
+end
+
+--- Retrieve the signal aspect cache.
+-- @function save_supposed_aspects
+-- @return The current database in use.
+function I.save_supposed_aspects()
+ local t = {}
+ for k, v in pairs(supposed_aspects) do
+ t[k] = v:plain(true)
+ end
+ return t
+end
+
+--- Read the aspect of a signal strictly from cache.
+-- @param pos The position of the signal.
+-- @return[1] The aspect of the signal (if present in cache).
+-- @return[2] The nil constant (otherwise).
+local function get_supposed_aspect(pos)
+ return supposed_aspects[pts(pos)]
+end
+
+--- Update the signal aspect information in cache.
+-- @param pos The position of the signal.
+-- @param asp The new signal aspect
+local function set_supposed_aspect(pos, asp)
+ supposed_aspects[pts(pos)] = asp
+end
+
+--- Get the definition of a node.
+-- @param pos The position of the node.
+-- @return[1] The definition of the node (if present).
+-- @return[2] An empty table (otherwise).
+local function get_ndef(pos)
+ local node = N.get_node(pos)
+ return (minetest.registered_nodes[node.name] or {}), node
+end
+
+--- Get the aspects supported by a signal.
+-- @function signal_get_supported_aspects
+-- @param pos The position of the signal.
+-- @return[1] The table of supported aspects (if present).
+-- @return[2] The nil constant (otherwise).
+local function get_supported_aspects(pos)
+ local ndef = get_ndef(pos)
+ if ndef.advtrains and ndef.advtrains.supported_aspects then
+ return ndef.advtrains.supported_aspects
+ end
+ return nil
+end
+
+--- Adjust a new signal aspect to fit a signal.
+-- @param pos The position of the signal.
+-- @param asp The new signal aspect.
+-- @return The adjusted signal aspect.
+-- @return The information to pass to the `advtrains.set_aspect` field in the node definitions.
+local function adjust_aspect(pos, asp)
+ local asp = A(asp)
+
+ local mainpos = D.get_main(pos)
+ local nxtasp
+ if mainpos then
+ nxtasp = get_aspect(mainpos)
+ end
+ local suppasp = get_supported_aspects(pos)
+ if not suppasp then
+ return asp
+ end
+ return asp:adjust_distant(nxtasp, suppasp.dst_shift):to_group(suppasp.group)
+end
+
+--- Get the aspect of a signal without accessing the cache.
+-- For most cases, `get_aspect` should be used instead.
+-- @function signal_get_real_aspect
+-- @param pos The position of the signal.
+-- @return[1] The signal aspect adjusted using `adjust_aspect` (if present).
+-- @return[2] The nil constant (otherwise).
+local function get_real_aspect(pos)
+ local ndef, node = get_ndef(pos)
+ if ndef.advtrains and ndef.advtrains.get_aspect then
+ local asp = ndef.advtrains.get_aspect(pos, node) or I.DANGER
+ return adjust_aspect(pos, asp)
+ end
+ return nil
+end
+
+--- Get the aspect of a signal.
+-- @function signal_get_aspect
+-- @param pos The position of the signal.
+-- @return[1] The aspect of the signal (if present).
+-- @return[2] The nil constant (otherwise).
+get_aspect = function(pos)
+ local asp = get_supposed_aspect(pos)
+ if not asp then
+ asp = get_real_aspect(pos)
+ set_supposed_aspect(pos, asp)
+ end
+ return asp
+end
+
+--- Set the aspect of a signal.
+-- @function signal_set_aspect
+-- @param pos The position of the signal.
+-- @param asp The new signal aspect.
+-- @param[opt=false] skipdst Whether to skip updating distant signals.
+local function set_aspect(pos, asp, skipdst)
+ local node = N.get_node(pos)
+ local ndef = minetest.registered_nodes[node.name]
+ if ndef and ndef.advtrains and ndef.advtrains.set_aspect then
+ local oldasp = I.signal_get_aspect(pos) or DANGER
+ local newasp = adjust_aspect(pos, asp)
+ set_supposed_aspect(pos, newasp)
+ ndef.advtrains.set_aspect(pos, node, newasp)
+ I.signal_on_aspect_changed(pos)
+ local aspect_changed = oldasp ~= newasp
+ if (not skipdst) and aspect_changed then
+ D.update_main(pos)
+ end
+ end
+end
+
+--- Remove a signal from cache.
+-- @function signal_clear_aspect
+-- @param pos The position of the signal.
+local function clear_aspect(pos)
+ set_supposed_aspect(pos, nil)
+end
+
+--- Readjust the aspect of a signal.
+-- @function signal_readjust_aspect
+-- @param pos The position of the signal.
+local function readjust_aspect(pos)
+ set_aspect(pos, get_aspect(pos))
+end
+
+I.signal_get_supported_aspects = get_supported_aspects
+I.signal_get_real_aspect = get_real_aspect
+I.signal_get_aspect = get_aspect
+I.signal_set_aspect = set_aspect
+I.signal_clear_aspect = clear_aspect
+I.signal_readjust_aspect = readjust_aspect
diff --git a/advtrains_interlocking/signal_aspect_ui.lua b/advtrains_interlocking/signal_aspect_ui.lua
new file mode 100644
index 0000000..a81b7fe
--- /dev/null
+++ b/advtrains_interlocking/signal_aspect_ui.lua
@@ -0,0 +1,262 @@
+local F = advtrains.formspec
+local players_aspsel = {}
+
+local function describe_main_aspect(spv)
+ if spv == 0 then
+ return attrans("Danger (halt)")
+ elseif spv == -1 then
+ return attrans("Continue at maximum speed")
+ elseif not spv then
+ return attrans("Continue with current speed limit")
+ else
+ return attrans("Continue with the speed limit of @1", tostring(spv))
+ end
+end
+
+local function describe_shunt_aspect(shunt)
+ if shunt then
+ return attrans("Shunting allowed")
+ else
+ return attrans("No shunting")
+ end
+end
+
+local function describe_distant_aspect(spv)
+ if spv == 0 then
+ return attrans("Expect to stop at the next signal")
+ elseif spv == -1 then
+ return attrans("Expect to continue at maximum speed")
+ elseif not spv then
+ return attrans("No distant signal information")
+ else
+ return attrans("Expect to continue with a speed limit of @1", tostring(spv))
+ end
+end
+
+advtrains.interlocking.describe_main_aspect = describe_main_aspect
+advtrains.interlocking.describe_shunt_aspect = describe_shunt_aspect
+advtrains.interlocking.describe_distant_aspect = describe_distant_aspect
+
+local function dsel(p, q, x, y)
+ if p == nil then
+ if q then
+ return x
+ else
+ return y
+ end
+ elseif p then
+ return x
+ else
+ return y
+ end
+end
+
+local function describe_supported_aspects(suppasp, isasp)
+ local t = {}
+
+ local entries = {attrans("Use default value")}
+ local selid = 0
+ local mainasps = suppasp.main
+ if type(mainasps) ~= "table" then
+ mainasps = {mainasps}
+ end
+ for idx, spv in ipairs(mainasps) do
+ if isasp and spv == rawget(isasp, "main") then
+ selid = idx
+ end
+ entries[idx+1] = describe_main_aspect(spv)
+ end
+ t.main = entries
+ t.main_current = selid+1
+ t.main_string = tostring(isasp.main)
+ if t.main == nil then
+ t.main_string = ""
+ end
+
+ t.shunt = {
+ attrans("No shunting"),
+ attrans("Shunting allowed"),
+ attrans("Proceed as main"),
+ }
+
+ t.shunt_current = dsel(suppasp.shunt, isasp.shunt, 2, 1)
+ if dsel(suppasp.proceed_as_main, isasp.proceed_as_main, t.shunt_current == 1) then
+ t.shunt_current = 3
+ end
+ t.shunt_const = suppasp.shunt ~= nil
+
+ if suppasp.group then
+ local gdef = advtrains.interlocking.aspect.get_group_definition(suppasp.group)
+ if gdef then
+ t.group = suppasp.group
+ t.groupdef = gdef
+ local entries = {}
+ local selid = 1
+ for idx, name in ipairs(suppasp.name or {}) do
+ entries[idx] = gdef.aspects[name].label
+ if suppasp.group == isasp.group and name == isasp.name then
+ selid = idx
+ end
+ end
+ t.name = entries
+ t.name_current = selid
+ end
+ end
+
+ return t
+end
+
+advtrains.interlocking.describe_supported_aspects = describe_supported_aspects
+
+local function make_signal_aspect_selector(suppasp, purpose, isasp)
+ local t = describe_supported_aspects(suppasp, isasp)
+ local formmode = 1
+
+ local pos
+ if type(purpose) == "table" then
+ formmode = 2
+ pos = purpose.pos
+ end
+
+ local form = {
+ "formspec_version[4]",
+ string.format("size[8,%f]", ({5.75, 10.75})[formmode]),
+ F.S_label(0.5, 0.5, "Select signal aspect"),
+ }
+ local h0 = ({0, 1.5})[formmode]
+ form[#form+1] = F.S_label(0.5, 1.5+h0, "Main aspect")
+ form[#form+1] = F.S_label(0.5, 3+h0, "Shunt aspect")
+ form[#form+1] = F.S_button_exit(0.5, 4.5+h0, 7, "asp_save", "Save signal aspect")
+ if formmode == 1 then
+ form[#form+1] = F.label(0.5, 1, purpose)
+ form[#form+1] = F.field(0.5, 2, 7, "asp_mainval", "", t.main_string)
+ elseif formmode == 2 then
+ if t.group then
+ form[#form+1] = F.S_label(0.5, 1.5, "Signal aspect group: @1", t.groupdef.label)
+ form[#form+1] = F.dropdown(0.5, 2, 7, "asp_namesel", t.name, t.name_current, true)
+ else
+ form[#form+1] = F.S_label(0.5, 1.5, "This signal does not belong to a signal aspect group.")
+ form[#form+1] = F.S_label(0.5, 2, "You can not use a predefined signal aspect.")
+ end
+ form[#form+1] = F.S_label(0.5, 1, "Signal at @1", minetest.pos_to_string(pos))
+ form[#form+1] = F.dropdown(0.5, 3.5, 7, "asp_mainsel", t.main, t.main_current, true)
+ form[#form+1] = advtrains.interlocking.make_ip_formspec_component(pos, 0.5, 7, 7)
+ form[#form+1] = advtrains.interlocking.make_short_dst_formspec_component(pos, 0.5, 8.5, 7)
+ end
+
+ if formmode == 2 and t.shunt_const then
+ form[#form+1] = F.label(0.5, 3.5+h0, t.shunt[t.shunt_current])
+ form[#form+1] = F.S_label(0.5, 4+h0, "The shunt aspect cannot be changed.")
+ else
+ form[#form+1] = F.dropdown(0.5, 3.5+h0, 7, "asp_shunt", t.shunt, t.shunt_current, true)
+ end
+
+ return table.concat(form)
+end
+
+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 pos
+ if type(p_purpose) == "table" then
+ pos = p_purpose
+ purpose = {pname = pname, pos = pos}
+ end
+
+ local form = make_signal_aspect_selector(suppasp, purpose, isasp)
+ if not form then
+ return
+ end
+
+ local token = advtrains.random_id()
+ minetest.show_formspec(pname, "at_il_sigaspdia_"..token, form)
+ minetest.after(0, function()
+ players_aspsel[pname] = {
+ purpose = purpose,
+ 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 get_aspect_from_formspec(suppasp, fields, psl)
+ local namei, group, name = tonumber(fields.asp_namesel), suppasp.group, nil
+ local gdef = advtrains.interlocking.aspect.get_group_definition(group)
+ if gdef then
+ local names = suppasp.name or {}
+ name = names[namei] or names[names]
+ else
+ group = nil
+ end
+ local maini = tonumber(fields.asp_mainsel)
+ local main = (suppasp.main or {})[(maini or 0)-1]
+ if not maini then
+ local mainval = fields.asp_mainval
+ if mainval == "-1" then
+ main = -1
+ elseif mainval == "x" then
+ main = false
+ elseif string.match(mainval, "^%d+$") then
+ main = tonumber(mainval)
+ else
+ main = nil
+ end
+ elseif maini <= 1 then
+ main = nil
+ end
+ local shunti = tonumber(fields.asp_shunt)
+ local shunt = suppasp.shunt
+ if shunt == nil then
+ shunt = shunti == 2
+ end
+ local proceed_as_main = suppasp.proceed_as_main
+ if proceed_as_main == nil then
+ proceed_as_main = shunti == 3
+ end
+ return advtrains.interlocking.aspect {
+ main = main,
+ shunt = shunt,
+ proceed_as_main = proceed_as_main,
+ info = {},
+ name = name,
+ group = group,
+ }
+end
+
+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
+ local suppasp = psl.suppasp
+ if fields.asp_save then
+ local asp
+ asp = get_aspect_from_formspec(suppasp, fields, psl)
+ if asp then
+ psl.callback(pname, asp)
+ end
+ end
+ if type(psl.purpose) == "table" then
+ local pos = psl.purpose.pos
+ advtrains.interlocking.handle_ip_formspec_fields(pname, pos, fields)
+ advtrains.interlocking.handle_dst_formspec_fields(pname, pos, fields)
+ end
+ else
+ players_aspsel[pname] = nil
+ end
+ end
+end)
diff --git a/advtrains_interlocking/spec/ars_spec.lua b/advtrains_interlocking/spec/ars_spec.lua
new file mode 100644
index 0000000..085dbcb
--- /dev/null
+++ b/advtrains_interlocking/spec/ars_spec.lua
@@ -0,0 +1,67 @@
+-- test the serialization function
+
+
+package.path = "../?.lua;" .. package.path
+
+
+
+
+_G.advtrains = {}
+_G.advtrains.interlocking = {}
+
+require("ars")
+
+local arstb = {{ ln="Foo"}, {c="Bar"}, {n=true, rc="Boo"}}
+local arsdef = {{ ln="Foo"}, {c="Bar"}, {rc="Boo"}, default=true}
+local arstr = [[LN Foo
+#Bar
+!RC Boo]]
+local defstr = [[*
+LN Foo
+#Bar
+RC Boo]]
+il = _G.advtrains.interlocking
+
+describe("ars_to_text", function ()
+ it("read table", function ()
+ assert.equals(il.ars_to_text(arstb),arstr)
+ end)
+ it("reads back and forth", function ()
+ assert.equals(il.ars_to_text(il.text_to_ars(arstr)),arstr)
+ end)
+ it("handles default routes properly", function ()
+ assert.equals(il.ars_to_text(arsdef),defstr)
+ end)
+end)
+
+describe("text_to_ars", function ()
+ it("writes table", function()
+ assert.same(il.text_to_ars(arstr),arstb)
+ end)
+ it("handles default routes properly", function ()
+ assert.same(il.text_to_ars(defstr),arsdef)
+ end)
+end)
+
+train1 = {}
+train2 = {}
+train3 = {}
+train1.line = "Foo"
+train1.routingcode = "Boo"
+train2.line= "Bar"
+train2.routingcode = "NotBoo NotBoo"
+train3.routingcode = "Foo Boo Moo Zoo"
+
+describe("check_rule_match", function ()
+ it("matches rules correctly", function()
+ assert.equals(il.ars_check_rule_match(arstb,train1),1)
+ assert.equals(il.ars_check_rule_match(arsdef,train2),nil)
+ end)
+ it("matches negative rules", function()
+ assert.equals(il.ars_check_rule_match(arstb,train2),3)
+ assert.equals(il.ars_check_rule_match(arstb,train3),nil)
+ end)
+ it("matches RC in a list correctly", function()
+ assert.equals(il.ars_check_rule_match(arsdef,train3),3)
+ 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 2425bf2..9aea18c 100755
--- a/advtrains_interlocking/tcb_ts_ui.lua
+++ b/advtrains_interlocking/tcb_ts_ui.lua
@@ -14,6 +14,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",
@@ -197,20 +198,24 @@ minetest.register_on_punchnode(function(pos, node, player, pointed_thing)
if is_signal then
local ndef = minetest.registered_nodes[node.name]
if ndef and ndef.advtrains and ndef.advtrains.set_aspect then
- local tcbs = ildb.get_tcbs(sigd)
- if tcbs then
- tcbs.signal = pos
- if not tcbs.signal_name then
- tcbs.signal_name = "Signal at "..minetest.pos_to_string(sigd.p)
+ if ndef.advtrains.supported_aspects and not ndef.advtrains.supported_aspects.dst_shift then
+ local tcbs = ildb.get_tcbs(sigd)
+ if tcbs then
+ tcbs.signal = pos
+ if not tcbs.signal_name then
+ tcbs.signal_name = "Signal at "..minetest.pos_to_string(sigd.p)
+ end
+ if not tcbs.routes then
+ tcbs.routes = {}
+ end
+ ildb.set_sigd_for_signal(pos, sigd)
+ minetest.chat_send_player(pname, "Configuring TCB: Successfully assigned signal.")
+ advtrains.interlocking.show_ip_form(pos, pname, true)
+ else
+ minetest.chat_send_player(pname, "Configuring TCB: Internal error, TCBS doesn't exist. Aborted.")
end
- if not tcbs.routes then
- tcbs.routes = {}
- end
- ildb.set_sigd_for_signal(pos, sigd)
- minetest.chat_send_player(pname, "Configuring TCB: Successfully assigned signal.")
- advtrains.interlocking.show_ip_form(pos, pname, true)
else
- minetest.chat_send_player(pname, "Configuring TCB: Internal error, TCBS doesn't exist. Aborted.")
+ minetest.chat_send_player(pname, "Configuring TCB: Cannot use distant signal. Aborted.")
end
else
minetest.chat_send_player(pname, "Configuring TCB: Cannot use static signals for routesetting. Aborted.")
@@ -608,7 +613,7 @@ function advtrains.interlocking.show_signalling_form(sigd, pname, sel_rte, calle
if not tcbs.signal_name then tcbs.signal_name = "Signal at "..minetest.pos_to_string(sigd.p) 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).."]"
form = form.."button[5.5,1.2;1,1;setname;Set]"
@@ -668,12 +673,8 @@ 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)
+ form = form..string.format("checkbox[0.5,9.25;dst;Distant signalling;%s]", not tcbs.nodst)
end
elseif sigd_equal(tcbs.route_origin, sigd) then
-- something has gone wrong: tcbs.routeset should have been set...
@@ -723,11 +724,17 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
connid = tonumber(connids)
if not connid or connid<1 or connid>2 then return end
end
- if pos and connid and not fields.quit then
+ if pos and connid then
local sigd = {p=pos, s=connid}
local tcbs = ildb.get_tcbs(sigd)
if not tcbs then return end
-
+
+ if fields.quit then
+ -- form quit: disable temporary ARS ignore
+ tcbs.ars_ignore_next = nil
+ return
+ end
+
local sel_rte
if fields.rtelist then
local tev = minetest.explode_textlist_event(fields.rtelist)
@@ -740,7 +747,7 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
end
if tcbs.routeset and fields.cancelroute then
if tcbs.routes[tcbs.routeset] and tcbs.routes[tcbs.routeset].ars then
- tcbs.ars_disabled = true
+ tcbs.ars_ignore_next = true
end
-- if route committed, cancel route ts info
ilrs.update_route(sigd, tcbs, nil, true)
@@ -749,6 +756,7 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
if fields.newroute and hasprivs then
advtrains.interlocking.init_route_prog(pname, sigd)
minetest.close_formspec(pname, formname)
+ tcbs.ars_ignore_next = nil
return
end
if sel_rte and tcbs.routes[sel_rte] then
@@ -778,7 +786,6 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
ildb.set_sigd_for_signal(signal_pos, nil)
tcbs.signal = nil
tcbs.aspect = nil
- advtrains.interlocking.distant.remove(tcbs)
minetest.close_formspec(pname, formname)
minetest.chat_send_player(pname, "Signal has been unassigned. Name and routes are kept for reuse.")
return
@@ -786,16 +793,13 @@ 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
+ if fields.ars then
+ tcbs.ars_disabled = not minetest.is_yes(fields.ars)
end
- if not tcbs.ars_disabled and fields.arsdisable then
- tcbs.ars_disabled = true
+
+ if fields.dst then
+ tcbs.nodst = not minetest.is_yes(fields.dst)
end
if fields.auto then
diff --git a/advtrains_interlocking/train_sections.lua b/advtrains_interlocking/train_sections.lua
index 757f36a..ec7f95f 100644
--- a/advtrains_interlocking/train_sections.lua
+++ b/advtrains_interlocking/train_sections.lua
@@ -91,6 +91,13 @@ local function setsection(tid, train, ts_id, ts, sigd)
tcbs.route_comitted = nil -- TODO compatibility cleanup
tcbs.aspect = nil
tcbs.route_origin = nil
+ if tcbs.signal then
+ local spos = tcbs.signal
+ local _, setter = advtrains.distant.get_main(spos)
+ if setter == "routesetting" then
+ advtrains.distant.unassign_dst(spos, true)
+ end
+ end
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