diff options
Diffstat (limited to 'advtrains_interlocking')
-rw-r--r-- | advtrains_interlocking/README.md | 41 | ||||
-rw-r--r-- | advtrains_interlocking/aspect.lua | 290 | ||||
-rw-r--r-- | advtrains_interlocking/distant.lua | 1 | ||||
-rw-r--r-- | advtrains_interlocking/init.lua | 2 | ||||
-rw-r--r-- | advtrains_interlocking/signal_api.lua | 28 | ||||
-rw-r--r-- | advtrains_interlocking/signal_aspect_accessors.lua | 80 | ||||
-rw-r--r-- | advtrains_interlocking/signal_aspect_ui.lua | 127 | ||||
-rw-r--r-- | advtrains_interlocking/signal_aspects.lua | 202 | ||||
-rw-r--r-- | advtrains_interlocking/spec/basic_signalling_spec.lua | 26 | ||||
-rw-r--r-- | advtrains_interlocking/spec/signal_group_spec.lua | 95 | ||||
-rw-r--r-- | advtrains_interlocking/spec/type2_spec.lua | 117 |
11 files changed, 465 insertions, 544 deletions
diff --git a/advtrains_interlocking/README.md b/advtrains_interlocking/README.md index 636ad67..d4a2699 100644 --- a/advtrains_interlocking/README.md +++ b/advtrains_interlocking/README.md @@ -2,12 +2,6 @@ The `advtrains_interlocking` mod provides various interlocking and signaling features for Advtrains. -## Signal types -There are two types of signals in Advtrains: - -* Type 1 (speed signals): These signals only give speed information. -* Type 2 (route signals): These signals mainly provide route information, but sometimes also provide speed information. - ## Signal aspect tables Signal aspects are represented using tables with the following (optional) fields: @@ -16,15 +10,16 @@ Signal aspects are represented using tables with the following (optional) fields * `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. -* `type2group`: The type 2 group of the signal. -* `type2name`: The type 2 signal aspect name. +* `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, or -* The constant `false` or `nil`, indicating no change to the speed restriction. +* 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 @@ -38,22 +33,19 @@ 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. -For type 1 signals, the `supported_aspects` table should contain the following fields: +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. - -For type 2 signals, the `supported_aspects` table should contain the following fields: - -* `type`: The numeric constant `2`. -* `group`: The type 2 signal group. +* `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 (or, in the case of type 2 signals, the name of the new signal aspect). For type 1 signals, the new aspect is not guranteed to be supported by the signal itself. +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: @@ -63,23 +55,20 @@ Signals should also have the following callbacks set: Alternatively, custom callbacks should call the respective functions. -## Type 2 signal groups +## Signal groups -Type 2 signals belong to signal gruops, which are registered using `advtrains.interlocking.aspects.register_type2`. +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. -* `main`: A list of signal aspects, from the least restrictive (i.e. proceed) to the most restrictive (i.e. danger). +* `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: -* `name`: The internal name of the signal aspect. * `label`: The description of the signal aspect. -* `main`, `shunt`, `proceed_as_main`: The fields corresponding to the ones in signal aspect tables. - -Type 2 signal aspects are then referred to with the aspect names within the group. +* `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 @@ -89,8 +78,8 @@ It is allowed to provide other methods of setting the signal aspect. However: * Please call `advtrains.interlocking.signal_readjust_aspect` after the signal aspect has changed. ## Examples -An example of type 1 signals can be found in `advtrains_signals_ks`, which provides a subset of German signals. +An example of speed signals can be found in `advtrains_signals_ks`, which provides a subset of German signals. -An example of type 2 signals can be found in `advtrains_signals_japan`, which provides a subset of Japanese 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..1575fb1 --- /dev/null +++ b/advtrains_interlocking/aspect.lua @@ -0,0 +1,290 @@ +--- Signal aspect handling. +-- @module advtrains.interlocking.aspect + +local registered_groups = {} + +local named_aspect_aspfields = {main = true, shunt = true, proceed_as_main = true} + +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 method = signal_aspect[field] + if method then + return method + end + if not named_aspect_aspfields[field] then + return nil + end + local group = registered_groups[rawget(asp, "group")] + if not group then + return false + end + local aspdef = group.aspects[rawget(asp, "name")] + if not aspdef then + return false + end + return aspdef[field] or false + 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(named_aspect_aspfields) 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 = group.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 + 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(named_aspect_aspfields) 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/distant.lua b/advtrains_interlocking/distant.lua index 4175875..32ada82 100644 --- a/advtrains_interlocking/distant.lua +++ b/advtrains_interlocking/distant.lua @@ -6,7 +6,6 @@ local db_distant = {} local db_distant_of = {} -local A = advtrains.interlocking.aspects local pts = advtrains.encode_pos local stp = advtrains.decode_pos diff --git a/advtrains_interlocking/init.lua b/advtrains_interlocking/init.lua index 1a8ef07..4d959cc 100644 --- a/advtrains_interlocking/init.lua +++ b/advtrains_interlocking/init.lua @@ -12,7 +12,7 @@ end local modpath = minetest.get_modpath(minetest.get_current_modname()) .. DIR_DELIM -advtrains.interlocking.aspects = dofile(modpath.."signal_aspects.lua") +advtrains.interlocking.aspect = dofile(modpath.."aspect.lua") dofile(modpath.."database.lua") dofile(modpath.."distant.lua") diff --git a/advtrains_interlocking/signal_api.lua b/advtrains_interlocking/signal_api.lua index 1b4a21c..ce8854a 100644 --- a/advtrains_interlocking/signal_api.lua +++ b/advtrains_interlocking/signal_api.lua @@ -19,27 +19,7 @@ advtrains.interlocking.FULL_FREE = { 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 = convert_aspect_if_necessary +advtrains.interlocking.signal_convert_aspect_if_necessary = advtrains.interlocking.aspect function advtrains.interlocking.update_signal_aspect(tcbs, skipdst) if tcbs.signal then @@ -79,7 +59,7 @@ function advtrains.interlocking.signal_rc_handler(pos, node, player, itemstack, 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 @@ -92,7 +72,7 @@ function advtrains.interlocking.show_signal_form(pos, node, pname) advtrains.interlocking.signal_set_aspect(pos, aspect) end local isasp = advtrains.interlocking.signal_get_aspect(pos, node) - + advtrains.interlocking.show_signal_aspect_selector( pname, ndef.advtrains.supported_aspects, @@ -123,7 +103,7 @@ 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 diff --git a/advtrains_interlocking/signal_aspect_accessors.lua b/advtrains_interlocking/signal_aspect_accessors.lua index e419515..d91df31 100644 --- a/advtrains_interlocking/signal_aspect_accessors.lua +++ b/advtrains_interlocking/signal_aspect_accessors.lua @@ -1,33 +1,12 @@ --- Signal aspect accessors -- @module advtrains.interlocking -local A = advtrains.interlocking.aspects +local A = advtrains.interlocking.aspect local D = advtrains.distant local I = advtrains.interlocking local N = advtrains.ndb local pts = advtrains.roundfloorpts -local signal_aspect_metatable = { - __tostring = function(asp) - local st = {} - if asp.type2group and asp.type2name then - table.insert(st, string.format("%q in group %q", asp.type2name, asp.type2group)) - end - if asp.main then - table.insert(st, string.format("current %d", asp.main)) - end - if asp.main ~= 0 then - if asp.dst then - table.insert(st, string.format("next %d", asp.dst)) - end - if asp.proceed_as_main then - table.insert(st, "proceed as main") - end - end - return string.format("[%s]", table.concat(st, ", ")) - end, -} - local get_aspect local supposed_aspects = {} @@ -37,9 +16,9 @@ local supposed_aspects = {} -- @param db The new database. function I.load_supposed_aspects(tbl) if tbl then - supposed_aspects = tbl - for _, v in pairs(tbl) do - setmetatable(v, signal_aspect_metatable) + supposed_aspects = {} + for k, v in pairs(tbl) do + supposed_aspects[k] = A(v) end end end @@ -48,7 +27,11 @@ end -- @function save_supposed_aspects -- @return The current database in use. function I.save_supposed_aspects() - return 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. @@ -72,7 +55,7 @@ end -- @return[2] An empty table (otherwise). local function get_ndef(pos) local node = N.get_node(pos) - return minetest.registered_nodes[node.name] or {} + return (minetest.registered_nodes[node.name] or {}), node end --- Get the aspects supported by a signal. @@ -94,43 +77,18 @@ end -- @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) - asp = table.copy(I.signal_convert_aspect_if_necessary(asp)) - setmetatable(asp, signal_aspect_metatable) + local asp = A(asp) local mainpos = D.get_main(pos) local nxtasp if mainpos then nxtasp = get_aspect(mainpos) end - if asp.main ~= 0 and mainpos then - asp.dst = nxtasp.main - else - asp.dst = nil - end - local suppasp = get_supported_aspects(pos) if not suppasp then - return asp, asp - end - local stype = suppasp.type - if stype == 2 then - local group = suppasp.group - local name - if suppasp.dst_shift and nxtasp then - asp.main = nil - name = A.type1_to_type2main(nxtasp, group, suppasp.dst_shift) - elseif asp.main ~= 0 and nxtasp and nxtasp.type2group == group and nxtasp.type2name then - name = A.get_type2_dst(group, nxtasp.type2name) - else - name = A.type1_to_type2main(asp, group) - end - asp.type2group = group - asp.type2name = name - return asp, name + return asp end - asp.type2name = nil - asp.type2group = nil - return asp, asp + return asp:adjust_distant(nxtasp, suppasp.dst_shift):to_group(suppasp.group) end --- Get the aspect of a signal without accessing the cache. @@ -140,13 +98,9 @@ end -- @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 = get_ndef(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 - local suppasp = get_supported_aspects(pos) - if suppasp and suppasp.type == 2 then - asp = A.type2_to_type1(suppasp, asp) - end return adjust_aspect(pos, asp) end return nil @@ -176,11 +130,11 @@ local function set_aspect(pos, asp, skipdst) 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, aspval = adjust_aspect(pos, asp) + local newasp = adjust_aspect(pos, asp) set_supposed_aspect(pos, newasp) - ndef.advtrains.set_aspect(pos, node, aspval) + ndef.advtrains.set_aspect(pos, node, newasp) I.signal_on_aspect_changed(pos) - local aspect_changed = A.not_equalp(oldasp, newasp) + local aspect_changed = oldasp ~= newasp if (not skipdst) and aspect_changed then D.update_main(pos) end diff --git a/advtrains_interlocking/signal_aspect_ui.lua b/advtrains_interlocking/signal_aspect_ui.lua index 186d2fe..d36c6bc 100644 --- a/advtrains_interlocking/signal_aspect_ui.lua +++ b/advtrains_interlocking/signal_aspect_ui.lua @@ -1,7 +1,7 @@ local F = advtrains.formspec local players_aspsel = {} -local function describe_t1_main_aspect(spv) +local function describe_main_aspect(spv) if spv == 0 then return attrans("Danger (halt)") elseif spv == -1 then @@ -13,7 +13,7 @@ local function describe_t1_main_aspect(spv) end end -local function describe_t1_shunt_aspect(shunt) +local function describe_shunt_aspect(shunt) if shunt then return attrans("Shunting allowed") else @@ -21,7 +21,7 @@ local function describe_t1_shunt_aspect(shunt) end end -local function describe_t1_distant_aspect(spv) +local function describe_distant_aspect(spv) if spv == 0 then return attrans("Expect to stop at the next signal") elseif spv == -1 then @@ -33,9 +33,9 @@ local function describe_t1_distant_aspect(spv) end end -advtrains.interlocking.describe_t1_main_aspect = describe_t1_main_aspect -advtrains.interlocking.describe_t1_shunt_aspect = describe_t1_shunt_aspect -advtrains.interlocking.describe_t1_distant_aspect = describe_t1_distant_aspect +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 @@ -51,19 +51,23 @@ local function dsel(p, q, x, y) end end -local function describe_supported_aspects_t1(suppasp, isasp) +local function describe_supported_aspects(suppasp, isasp) local t = {} - local entries = {} - local selid = 1 - for idx, spv in ipairs(suppasp.main) do - if isasp and spv == (isasp.main or false) then + local entries = {attrans("Use default value")} + local selid = 0 + local mainasps = suppasp.main + if type(mainasps) ~= "table" then + mainasps = {mainasps or false} + end + for idx, spv in ipairs(mainasps) do + if isasp and spv == rawget(isasp, "main") then selid = idx end - entries[idx] = describe_t1_main_aspect(spv) + entries[idx+1] = describe_main_aspect(spv) end t.main = entries - t.main_current = selid + t.main_current = selid+1 t.main_string = tostring(isasp.main) if t.main == nil then t.main_string = "" @@ -83,21 +87,21 @@ local function describe_supported_aspects_t1(suppasp, isasp) entries = {} selid = 1 - for idx, spv in ipairs(suppasp.dst) do + for idx, spv in ipairs(suppasp.dst or {}) do if isasp and spv == (isasp.dst or false) then selid = idx end - entries[idx] = describe_t1_distant_aspect(spv) + entries[idx] = describe_distant_aspect(spv) end t.dst = entries t.dst_current = selid return t end -advtrains.interlocking.describe_supported_aspects_t1 = describe_supported_aspects_t1 +advtrains.interlocking.describe_supported_aspects = describe_supported_aspects -local function make_signal_aspect_selector_t1(suppasp, purpose, isasp) - local t = describe_supported_aspects_t1(suppasp, isasp) +local function make_signal_aspect_selector(suppasp, purpose, isasp) + local t = describe_supported_aspects(suppasp, isasp) local formmode = 1 local pos @@ -142,55 +146,6 @@ local function make_signal_aspect_selector_t1(suppasp, purpose, isasp) return table.concat(form) end -local function make_signal_aspect_selector_t2(suppasp, purpose, isasp) - local def = advtrains.interlocking.aspects.get_type2_definition(suppasp.group) - if not def then - return nil - end - 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]", ({4.25, 10.25})[formmode]), - F.S_label(0.5, 0.5, "Select signal aspect") - } - if formmode == 1 then - form[#form+1] = F.label(0.5, 1, purpose) - else - form[#form+1] = F.S_label(0.5, 1, "Signal at @1", minetest.pos_to_string(pos)) - end - - local entries = {} - local selid = #def.main - if isasp then - if isasp.type2name ~= def.main[selid].name then - selid = 1 - end - end - if selid > 1 then - selid = 2 - end - local entries = { - def.main[1].label, - def.main[#def.main].label, - } - form[#form+1] = F.S_label(0.5, 1.5, "Signal group: @1", def.label) - form[#form+1] = F.dropdown(0.5, 2, 7, "asp_sel", entries, selid, true) - form[#form+1] = F.S_button_exit(0.5, 3, 7, "asp_save", "Save signal aspect") - - if formmode == 2 then - form[#form+1] = advtrains.interlocking.make_ip_formspec_component(pos, 0.5, 4, 7) - form[#form+1] = advtrains.interlocking.make_dst_formspec_component(pos, 0.5, 5.5, 7, 4.25) - 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}, @@ -205,18 +160,7 @@ function advtrains.interlocking.show_signal_aspect_selector(pname, p_suppasp, p_ purpose = {pname = pname, pos = pos} end - local form - if suppasp.type == 2 then - if suppasp.dst_shift then - if pos then - advtrains.interlocking.show_ip_form(pos, pname) - end - return - end - form = make_signal_aspect_selector_t2(suppasp, purpose, isasp) - else - form = make_signal_aspect_selector_t1(suppasp, purpose, isasp) - end + local form = make_signal_aspect_selector(suppasp, purpose, isasp) if not form then return end @@ -241,9 +185,9 @@ local function usebool(sup, val, free) end end -local function get_aspect_from_formspec_t1(suppasp, fields, psl) +local function get_aspect_from_formspec(suppasp, fields, psl) local maini = tonumber(fields.asp_mainsel) - local main = suppasp.main[maini] + local main = suppasp.main[(maini or 0)-1] if not maini then local mainval = fields.asp_mainval if mainval == "-1" then @@ -253,6 +197,8 @@ local function get_aspect_from_formspec_t1(suppasp, fields, psl) else main = nil end + elseif maini <= 1 then + main = nil end local shunti = tonumber(fields.asp_shunt) local shunt = suppasp.shunt @@ -271,19 +217,6 @@ local function get_aspect_from_formspec_t1(suppasp, fields, psl) } end -local function get_aspect_from_formspec_t2(suppasp, fields, psl) - local sel = tonumber(fields.asp_sel) - local def = advtrains.interlocking.aspects.get_type2_definition(suppasp.group) - if not (sel and def) then - return - end - if sel ~= 1 then - sel = #def.main - end - local asp = advtrains.interlocking.aspects.type2_to_type1(suppasp, sel) - return asp -end - minetest.register_on_player_receive_fields(function(player, formname, fields) local pname = player:get_player_name() local psl = players_aspsel[pname] @@ -292,11 +225,7 @@ minetest.register_on_player_receive_fields(function(player, formname, fields) local suppasp = psl.suppasp if fields.asp_save then local asp - if suppasp.type == 2 then - asp = get_aspect_from_formspec_t2(suppasp, fields, psl) - else - asp = get_aspect_from_formspec_t1(suppasp, fields, psl) - end + asp = get_aspect_from_formspec(suppasp, fields, psl) if asp then psl.callback(pname, asp) end diff --git a/advtrains_interlocking/signal_aspects.lua b/advtrains_interlocking/signal_aspects.lua deleted file mode 100644 index 14e04c7..0000000 --- a/advtrains_interlocking/signal_aspects.lua +++ /dev/null @@ -1,202 +0,0 @@ ---- Signal aspect handling. --- @module advtrains.interlocking.aspects - -local type2defs = {} - ---- Register a type 2 signal group. --- @function register_type2 --- @param def The definition table. -local function register_type2(def) - local t = {type = 2} - local name = def.name - if type(name) ~= "string" then - return error("Name is not a string") - elseif type2defs[name] then - return error(string.format("Attempt to redefine type 2 signal aspect group %q, previously defined in %s", name, type2defs[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 ipairs(def.main) do - local t = {} - local name = asp.name - if type(name) ~= "string" then - return error("Aspect name is not a string") - end - t.name = name - - local label = asp.label or name - if type(label) ~= "string" then - return error("Aspect label is not a string") - end - t.label = label - - t.main = asp.main - t.shunt = asp.shunt - t.proceed_as_main = asp.proceed_as_main - mainasps[idx] = t - mainasps[name] = idx - end - t.main = mainasps - - type2defs[name] = t -end - ---- Get the definition of a type 2 signal group. --- @function get_type2_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_type2_definition(name) - local t = type2defs[name] - if t then - return table.copy(t) - else - return nil - end -end - ---- Get the name of the distant aspect before the current aspect. --- @function get_type2_dst --- @param group The name of the group. --- @param name The name of the current aspect. --- @return[1] The name of the distant aspect (if present). --- @return[2] The nil constant (otherwise). -local function get_type2_dst(group, name) - local def = type2defs[group] - if not def then - return nil - end - local aspidx = name - if type(name) ~= "number" then - aspidx = def.main[name] or 1 - end - return def.main[math.max(1, aspidx-1)].name -end - ---- Convert a type 2 signal aspect to a type 1 signal aspect. --- @function type2_to_type1 --- @param suppasp The table of supported aspects for the signal. --- @param asp The name of the signal aspect. --- @return[1] The type 1 signal aspect table (if present). --- @return[2] The nil constant (otherwise). -local function type2_to_type1(suppasp, asp) - local name = suppasp.group - local shift = suppasp.dst_shift - local def = type2defs[name] - if not def then - return nil - end - local aspidx - if type(asp) == "number" then - aspidx = asp - else - aspidx = def.main[asp] or 2 - end - local realidx = math.min(#def.main, aspidx+(shift or 0)) - local asptbl = def.main[realidx] - if not asptbl then - return nil - end - if type(asp) == "number" then - asp = asptbl.name - end - local main, shunt, dst - if shift then - dst = asptbl.main - else - main = asptbl.main - shunt = asptbl.shunt - dst = def.main[math.min(#def.main, aspidx+1)].main - end - if main == 0 then - dst = nil - end - - local t = { - main = main, - shunt = shunt, - proceed_as_main = asptbl.proceed_as_main, - type2name = asptbl.name, - type2group = name, - dst = dst, - } - if aspidx > 1 and aspidx < #asptbl then - t.dst = asptbl[aspidx+1].main - end - return t -end - ---- Convert a type 1 signal aspect table to a type 2 signal aspect. --- @function type1_to_type2main --- @param asp The type 1 signal aspect table --- @param group The signal aspect group --- @param[opt=0] shift The shift for the signal aspect. --- @return[1] The name of the signal aspect (if present). --- @return[2] The nil constant (otherwise). -local function type1_to_type2main(asp, group, shift) - local def = type2defs[group] - if not def then - return nil - end - local t_main = def.main - local idx - if group == asp.type2group and t_main[asp.type2name] then - idx = t_main[asp.type2name] - elseif not asp.main or asp.main == -1 then - idx = 1 - elseif asp.main == 0 then - idx = #t_main - else - idx = #t_main-1 - end - return t_main[math.max(1, idx-(shift or 0))].name -end - ---- Compare two type 1 signal aspect tables. --- @function equalp --- @param asp1 The first signal aspect table. --- @param asp2 The second signal aspect table. --- @return Whether the two signal aspect tables give the same (type 1 aspect) information. -local function equalp(asp1, asp2) - if asp1 == asp2 then -- same reference - return true - else - for _, k in pairs {"main", "shunt", "dst"} do - if asp1[k] ~= asp2[k] then - return false - end - end - end - if asp1.type2group and asp1.type2group == asp2.type2group then - return asp1.type2name == asp2.type2name - end - return true -end - ---- Compare two signal aspect tables. --- @function not_equalp --- @param asp1 The first signal aspect table. --- @param asp2 The second signal aspect table. --- @return The negation of `equalp``(asp1, asp2)`. -local function not_equalp(asp1, asp2) - return not equalp(asp1, asp2) -end - -return { - register_type2 = register_type2, - get_type2_definition = get_type2_definition, - get_type2_dst = get_type2_dst, - type2_to_type1 = type2_to_type1, - type1_to_type2main = type1_to_type2main, - equalp = equalp, - not_equalp = not_equalp, -} diff --git a/advtrains_interlocking/spec/basic_signalling_spec.lua b/advtrains_interlocking/spec/basic_signalling_spec.lua index cce0f15..a4e1e3a 100644 --- a/advtrains_interlocking/spec/basic_signalling_spec.lua +++ b/advtrains_interlocking/spec/basic_signalling_spec.lua @@ -8,7 +8,7 @@ mineunit("core") _G.advtrains = { interlocking = { - aspects = fixture("../../signal_aspects"), + aspect = fixture("../../aspect"), }, ndb = { get_node = minetest.get_node, @@ -31,12 +31,16 @@ minetest.register_node("advtrains_interlocking:signal_sign", { 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} @@ -55,14 +59,14 @@ describe("API for supposed signal aspects", function() I.load_supposed_aspects(tbl) assert.same(tbl, I.save_supposed_aspects()) end) - it("should set and get type 1 signals properly", function () + it("should set and get signals properly", function () local pos = stub_pos_t1[2] local asp = stub_aspect_t1.slow - local newasp = { main = math.random(1,5) } - assert.same(asp, I.signal_get_aspect(pos)) + local newasp = A{ main = math.random(1,5) } + assert.equal(asp, I.signal_get_aspect(pos)) I.signal_set_aspect(pos, newasp) - assert.same(newasp, I.signal_get_aspect(pos)) - assert.same(asp, I.signal_get_real_aspect(pos)) + 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) @@ -72,9 +76,9 @@ describe("Distant signaling", function() for i = 1, 2 do D.assign(stub_pos_t1[i], stub_pos_t1[i+1]) end - assert.same(stub_aspect_t1.danger, I.signal_get_aspect(stub_pos_t1[1])) - assert.same({main = 6, dst = 0}, I.signal_get_aspect(stub_pos_t1[2])) - assert.same({main = -1, dst = 6}, I.signal_get_aspect(stub_pos_t1[3])) + 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])}) @@ -82,8 +86,8 @@ describe("Distant signaling", function() end) it("should update distant aspects automatically", function() I.signal_set_aspect(stub_pos_t1[2], {main = 2, dst = -1}) - assert.same({main = 2, dst = 0}, I.signal_get_aspect(stub_pos_t1[2])) - assert.same({main = -1, dst = 2}, I.signal_get_aspect(stub_pos_t1[3])) + 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") 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/spec/type2_spec.lua b/advtrains_interlocking/spec/type2_spec.lua deleted file mode 100644 index ac23574..0000000 --- a/advtrains_interlocking/spec/type2_spec.lua +++ /dev/null @@ -1,117 +0,0 @@ -require "mineunit" -mineunit("core") - -_G.advtrains = { - interlocking = { - aspects = sourcefile("signal_aspects"), - }, - 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.aspects -local D = advtrains.distant -local I = advtrains.interlocking -local N = advtrains.ndb - -local type2def = { - name = "foo", - main = { - {name = "proceed", main = -1}, - {name = "caution"}, - {name = "danger", main = 0}, - }, -} - -for _, v in pairs(type2def.main) do - minetest.register_node("advtrains_interlocking:" .. v.name, { - advtrains = { - supported_aspects = { - type = 2, - group = "foo", - }, - get_aspect = function() return v.name end, - set_aspect = function(pos, _, name) - N.swap_node(pos, {name = "advtrains_interlocking:" .. name}) - end, - } - }) -end - -local function asp(group, name, dst) - return A.type2_to_type1({group = group, dst_shift = shift}, name) -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("type 2 signal group registration", function() - it("should work", function() - A.register_type2(type2def) - assert(A.get_type2_definition("foo")) - end) - it("should only be allowed once for the same group", function() - assert.has.errors(function() A.register_type2(type2def) end) - end) - it("should handle nonexistant groups", function() - assert.is_nil(A.get_type2_definition("something_else")) - end) - it("should reject invalid definitions", function() - assert.has.errors(function() A.register_type2({}) end) - assert.has.errors(function() A.register_type2({name="",label={}}) end) - assert.has.errors(function() A.register_type2({name="",main={{name={}}}}) end) - assert.has.errors(function() A.register_type2({name="",main={{name="",label={}}}}) end) - end) -end) - -describe("signal aspect conversion", function() - it("should work for converting from type 1 to type 2", function() - assert.equal("danger", A.type1_to_type2main({main = 0}, "foo")) - assert.equal("caution", A.type1_to_type2main({main = 6}, "foo")) - assert.equal("proceed", A.type1_to_type2main({}, "foo")) - end) - it("should reject invalid type 2 signal information", function() - assert.is_nil(A.type1_to_type2main({}, "?")) - assert.is_nil(A.type2_to_type1({}, "x")) - assert.same(asp("foo","caution"), asp("foo", "x")) - end) - it("should accept integer indices for type 2 signal aspects", function() - assert.same(asp("foo", "caution"), asp("foo", 2)) - assert.same(asp("foo", "danger"), asp("foo", 10)) - assert.same(asp("foo", "proceed"), asp("foo", 1)) - assert.is_nil(asp("foo", -0.5)) - end) -end) - -describe("type 2 signals", function() - it("should support distant signaling", function() - assert.equal("caution", A.get_type2_dst("foo", 3)) - assert.equal("proceed", A.get_type2_dst("foo", "caution")) - assert.equal("proceed", A.get_type2_dst("foo", "proceed")) - end) - it("should work with accessors", function() - assert.same(asp("foo","danger"), I.signal_get_aspect(origin)) - local newasp = {type2group = "foo", type2name = "proceed", main = 6} - I.signal_set_aspect(origin, newasp) - assert.same(newasp, I.signal_get_aspect(origin)) - end) - it("should work with distant signaling", function() - assert.same(asp("foo","proceed"), I.signal_get_aspect(dstpos)) - local dstasp = {type2group = "foo", type2name = "proceed", dst = 6, main = -1} - D.assign(origin, dstpos) - assert.same(dstasp, I.signal_get_aspect(dstpos)) - end) -end) |