From 220563012d2aa2c753c791fa9faa38346f1355a5 Mon Sep 17 00:00:00 2001 From: "Y. Wang" Date: Fri, 6 May 2022 21:36:21 +0200 Subject: Record signal aspect to avoid excessive get_aspect calls; report testing coverage --- advtrains_interlocking/database.lua | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'advtrains_interlocking/database.lua') diff --git a/advtrains_interlocking/database.lua b/advtrains_interlocking/database.lua index 6787c50..efa5eb8 100644 --- a/advtrains_interlocking/database.lua +++ b/advtrains_interlocking/database.lua @@ -131,6 +131,9 @@ 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 --COMPATIBILITY to Signal aspect format -- TODO remove in time... @@ -173,6 +176,7 @@ 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(), } end -- cgit v1.2.3 From 98c37108762c6d7c9f1d691b84f49bfa65b81b28 Mon Sep 17 00:00:00 2001 From: "Y. Wang" Date: Sat, 11 Jun 2022 18:07:00 +0200 Subject: Implement primitive distant signaling --- advtrains/formspec.lua | 10 ++ advtrains_interlocking/database.lua | 4 + advtrains_interlocking/demosignals.lua | 6 +- advtrains_interlocking/distant.lua | 137 +++++++++++++++++++++ advtrains_interlocking/distant_ui.lua | 76 ++++++++++++ advtrains_interlocking/init.lua | 3 + advtrains_interlocking/routesetting.lua | 21 ++++ advtrains_interlocking/signal_api.lua | 85 +------------ advtrains_interlocking/signal_aspect_accessors.lua | 127 +++++++++++++++++++ advtrains_interlocking/signal_aspect_ui.lua | 6 +- advtrains_interlocking/signal_aspects.lua | 26 +++- advtrains_interlocking/signal_main_ui.lua | 0 .../spec/basic_signalling_spec.lua | 87 +++++++++++++ advtrains_interlocking/spec/signal_api_spec.lua | 49 -------- advtrains_interlocking/tcb_ts_ui.lua | 8 +- 15 files changed, 507 insertions(+), 138 deletions(-) create mode 100644 advtrains_interlocking/distant.lua create mode 100644 advtrains_interlocking/distant_ui.lua create mode 100644 advtrains_interlocking/signal_aspect_accessors.lua delete mode 100644 advtrains_interlocking/signal_main_ui.lua create mode 100644 advtrains_interlocking/spec/basic_signalling_spec.lua delete mode 100644 advtrains_interlocking/spec/signal_api_spec.lua (limited to 'advtrains_interlocking/database.lua') diff --git a/advtrains/formspec.lua b/advtrains/formspec.lua index aa5aa69..20dab59 100644 --- a/advtrains/formspec.lua +++ b/advtrains/formspec.lua @@ -9,6 +9,14 @@ local function make_list(entries) return table.concat(t, ",") end +local function f_button(x, y, w, h, id, text) + return sformat("button[%f,%f;%f,%f;%s;%s]", x, y, w, h, id, text) +end + +local function S_button(x, y, w, h, id, ...) + return f_button(x, y, w, h, id, attrans(...)) +end + local function f_button_exit(x, y, w, h, id, text) return sformat("button_exit[%f,%f;%f,%f;%s;%s]", x, y, w, h, id, text) end @@ -54,6 +62,8 @@ local function f_tabheader(x, y, w, h, id, entries, sel, transparent, border) end return { + button = f_button, + S_button = S_button, button_exit = f_button_exit, S_button_exit = S_button_exit, dropdown = f_dropdown, diff --git a/advtrains_interlocking/database.lua b/advtrains_interlocking/database.lua index efa5eb8..c5ae906 100644 --- a/advtrains_interlocking/database.lua +++ b/advtrains_interlocking/database.lua @@ -134,6 +134,9 @@ function ildb.load(data) 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... @@ -177,6 +180,7 @@ function ildb.save() influence_points = influence_points, npr_rails = advtrains.interlocking.npr_rails, supposed_aspects = advtrains.interlocking.save_supposed_aspects(), + distant = advtrains.distant.save(), } end 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..ffa9e08 --- /dev/null +++ b/advtrains_interlocking/distant.lua @@ -0,0 +1,137 @@ +local db_distant = {} +local db_distant_of = {} + +local A = advtrains.interlocking.aspects +local pts = advtrains.encode_pos +local stp = advtrains.decode_pos + +local function db_load(x) + if type(x) ~= "table" then + return + end + db_distant = x.distant + db_distant_of = x.distant_of +end + +local function db_save() + return { + distant = db_distant, + distant_of = db_distant_of, + } +end + +local update_signal, update_main, update_dst + +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 + +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 + +local function unassign_all(pos, force) + unassign_main(pos) + unassign_dst(pos, force) +end + +local function assign(main, dst, by) + 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} + update_dst(dst) +end + +local function pre_occupy(dst, by) + local pts_dst = pts(dst) + unassign_dst(dst) + db_distant_of[pts_dst] = {nil, by} +end + +local function get_distant(main) + local pts_main = pts(main) + return db_distant[pts_main] or {} +end + +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_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_dst = function(dst) + advtrains.interlocking.signal_readjust_aspect(dst) +end + +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, +} diff --git a/advtrains_interlocking/distant_ui.lua b/advtrains_interlocking/distant_ui.lua new file mode 100644 index 0000000..4ec2255 --- /dev/null +++ b/advtrains_interlocking/distant_ui.lua @@ -0,0 +1,76 @@ +local F = advtrains.formspec +local D = advtrains.distant +local I = advtrains.interlocking + +function advtrains.interlocking.show_distant_signal_form(pos, pname) + local form = {"size[7,7]"} + form[#form+1] = advtrains.interlocking.make_signal_formspec_tabheader(pname, pos, 7, 3) + local main, set_by = D.get_main(pos) + if main then + local pts_main = minetest.pos_to_string(main) + form[#form+1] = F.S_label(0.5, 0.5, "This signal is a distant signal of @1.", pts_main) + if set_by == "manual" then + form[#form+1] = F.S_label(0.5, 1, "The assignment is made manually.") + elseif set_by == "routesetting" then + form[#form+1] = F.S_label(0.5, 1, "The assignment is made by the routesetting system.") + end + else + form[#form+1] = F.S_label(0.5, 0.5, "This signal is not assigned to a main signal.") + form[#form+1] = F.S_label(0.5, 1, "The distant aspect of the signal is not used.") + end + if set_by ~= nil then + form[#form+1] = F.S_button_exit(0.5, 1.5, 3, 1, "unassign_dst", "Unassign") + form[#form+1] = F.S_button_exit(3.5, 1.5, 3, 1, "assign_dst", "Reassign") + else + form[#form+1] = F.S_button_exit(0.5, 1.5, 6, 1, "assign_dst", "Assign") + end + minetest.show_formspec(pname, "advtrains:distant_" .. minetest.pos_to_string(pos), table.concat(form)) +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 + signal_pos[pname] = pos + minetest.chat_send_player(pname, attrans("Please punch the signal to use as the main 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] + if not spos then + return + end + signal_pos[pname] = nil + local is_signal = minetest.get_item_group(node.name, "advtrains_signal") >= 2 + if not is_signal then + minetest.chat_send_player(pname, attrans("Incompatible signal.")) + return + end + minetest.chat_send_player(pname, attrans("Successfully assigned signal.")) + D.assign(pos, spos, "manual") +end) + +minetest.register_on_player_receive_fields(function(player, formname, fields) + local pname = player:get_player_name() + local pos = minetest.string_to_pos(string.match(formname, "^advtrains:distant_(.+)$") or "") + if not pos then + return + end + if not minetest.check_player_privs(pname, "interlocking") then + return + end + if advtrains.interlocking.handle_signal_formspec_tabheader_fields(pname, fields) then + return true + end + if fields.unassign_dst then + D.unassign_dst(pos) + elseif fields.assign_dst then + init_signal_assignment(pname, pos) + end +end) diff --git a/advtrains_interlocking/init.lua b/advtrains_interlocking/init.lua index d0b75a8..908d998 100644 --- a/advtrains_interlocking/init.lua +++ b/advtrains_interlocking/init.lua @@ -15,6 +15,9 @@ local modpath = minetest.get_modpath(minetest.get_current_modname()) .. DIR_DELI advtrains.interlocking.aspects = dofile(modpath.."signal_aspects.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") diff --git a/advtrains_interlocking/routesetting.lua b/advtrains_interlocking/routesetting.lua index 67efaea..f1b4455 100644 --- a/advtrains_interlocking/routesetting.lua +++ b/advtrains_interlocking/routesetting.lua @@ -45,6 +45,7 @@ function ilrs.set_route(signal, route, try) local rtename = route.name local signalname = ildb.get_tcbs(signal).signal_name local c_tcbs, c_ts_id, c_ts, c_rseg, c_lckp + local signals = {} while c_sigd and i<=#route do c_tcbs = ildb.get_tcbs(c_sigd) if not c_tcbs then @@ -115,6 +116,7 @@ function ilrs.set_route(signal, route, try) c_tcbs.aspect = route.aspect or advtrains.interlocking.GENERIC_FREE c_tcbs.route_origin = signal advtrains.interlocking.update_signal_aspect(c_tcbs) + signals[#signals+1] = c_tcbs.signal end end -- advance @@ -122,6 +124,25 @@ function ilrs.set_route(signal, route, try) c_sigd = c_rseg.next i = i + 1 end + + -- Distant signaling + local lastsig = nil + if c_sigd then + local e_tcbs = ildb.get_tcbs(c_sigd) + 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 pos = signals[i] + local _, assigned_by = advtrains.distant.get_main(pos) + if assigned_by ~= "manual" then + advtrains.distant.assign(lastsig, signals[i], "routesetting") + end + end + end return true end diff --git a/advtrains_interlocking/signal_api.lua b/advtrains_interlocking/signal_api.lua index 5b3baf8..1fd4e34 100644 --- a/advtrains_interlocking/signal_api.lua +++ b/advtrains_interlocking/signal_api.lua @@ -167,7 +167,6 @@ This function will query get_aspect to retrieve the new aspect. local DANGER = { main = 0, - dst = false, shunt = false, } advtrains.interlocking.DANGER = DANGER @@ -178,8 +177,6 @@ advtrains.interlocking.GENERIC_FREE = { dst = false, } -local supposed_aspects = {} - local function convert_aspect_if_necessary(asp) if type(asp.main) == "table" then local newasp = {} @@ -200,24 +197,7 @@ local function convert_aspect_if_necessary(asp) end return asp end - -function advtrains.interlocking.load_supposed_aspects(tbl) - if tbl then - supposed_aspects = tbl - end -end - -function advtrains.interlocking.save_supposed_aspects() - return supposed_aspects -end - -local function set_supposed_aspect(pos, asp) - supposed_aspects[advtrains.roundfloorpts(pos)] = asp -end - -local function get_supposed_aspect(pos) - return supposed_aspects[advtrains.roundfloorpts(pos)] -end +advtrains.interlocking.signal_convert_aspect_if_necessary = convert_aspect_if_necessary function advtrains.interlocking.update_signal_aspect(tcbs) if tcbs.signal then @@ -233,27 +213,8 @@ end function advtrains.interlocking.signal_after_dig(pos) -- clear influence point advtrains.interlocking.db.clear_ip_by_signalpos(pos) - set_supposed_aspect(pos, nil) -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 - local oldasp = advtrains.interlocking.signal_get_aspect(pos) or DANGER - local suppasp = advtrains.interlocking.signal_get_supported_aspects(pos) or {} - local newasp = asp - if suppasp.type == 2 then - asp = advtrains.interlocking.aspects.type1_to_type2main(asp, suppasp.group) - end - set_supposed_aspect(pos, newasp) - ndef.advtrains.set_aspect(pos, node, asp) - local aspect_changed = advtrains.interlocking.aspects.not_equalp(oldasp, newasp) - if aspect_changed then - advtrains.interlocking.signal_on_aspect_changed(pos) - end - end + advtrains.interlocking.signal_clear_aspect(pos) + advtrains.distant.unassign_all(pos, true) end -- should be called when aspect has changed on this signal. @@ -312,46 +273,6 @@ 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_real_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) - local suppasp = advtrains.interlocking.signal_get_supported_aspects(pos) or {} - if suppasp.type == 2 then - asp = advtrains.interlocking.aspects.type2main_to_type1(suppasp.group, asp) - end - if not asp then asp = DANGER end - asp = convert_aspect_if_necessary(asp) - return asp - end - return nil -end - --- Returns the signal aspect as reported in the suppasp table. -function advtrains.interlocking.signal_get_aspect(pos) - local asp = get_supposed_aspect(pos) - if not asp then - asp = advtrains.interlocking.signal_get_real_aspect(pos) - set_supposed_aspect(pos, asp) - end - return asp -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) diff --git a/advtrains_interlocking/signal_aspect_accessors.lua b/advtrains_interlocking/signal_aspect_accessors.lua new file mode 100644 index 0000000..02a03ea --- /dev/null +++ b/advtrains_interlocking/signal_aspect_accessors.lua @@ -0,0 +1,127 @@ +local A = advtrains.interlocking.aspects +local D = advtrains.distant +local I = advtrains.interlocking +local N = advtrains.ndb +local pts = advtrains.roundfloorpts + +local get_aspect + +local supposed_aspects = {} + +function I.load_supposed_aspects(tbl) + if tbl then + supposed_aspects = tbl + end +end + +function I.save_supposed_aspects() + return supposed_aspects +end + +local function get_supposed_aspect(pos) + return supposed_aspects[pts(pos)] +end + +local function set_supposed_aspect(pos, asp) + supposed_aspects[pts(pos)] = asp +end + +local function get_ndef(pos) + local node = N.get_node(pos) + return minetest.registered_nodes[node.name] or {} +end + +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 + +local function adjust_aspect(pos, asp) + asp = table.copy(I.signal_convert_aspect_if_necessary(asp)) + + local mainpos = D.get_main(pos) + local nxtasp + if asp.main ~= 0 and mainpos then + nxtasp = get_aspect(mainpos) + 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 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 + end + asp.type2name = nil + asp.type2group = nil + return asp, asp +end + +local function get_real_aspect(pos) + local ndef = 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.type == 2 then + asp = A.type2main_to_type1(suppasp.group, asp) + end + return adjust_aspect(pos, asp) + end + return nil +end + +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 + +local function set_aspect(pos, asp) + 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, aspval = adjust_aspect(pos, asp) + set_supposed_aspect(pos, newasp) + ndef.advtrains.set_aspect(pos, node, aspval) + local aspect_changed = A.not_equalp(oldasp, newasp) + if aspect_changed then + I.signal_on_aspect_changed(pos) + D.update_main(pos) + end + end +end + +local function clear_aspect(pos) + set_supposed_aspect(pos, nil) +end + +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 index 4b41187..30b5165 100644 --- a/advtrains_interlocking/signal_aspect_ui.lua +++ b/advtrains_interlocking/signal_aspect_ui.lua @@ -43,7 +43,7 @@ local function describe_supported_aspects_t1(suppasp, isasp) local entries = {} local selid = 1 for idx, spv in ipairs(suppasp.main) do - if isasp and spv == isasp.main then + if isasp and spv == (isasp.main or false) then selid = idx end entries[idx] = describe_t1_main_aspect(spv) @@ -67,7 +67,7 @@ local function describe_supported_aspects_t1(suppasp, isasp) entries = {} selid = 1 for idx, spv in ipairs(suppasp.dst) do - if isasp and spv == isasp.dst then + if isasp and spv == (isasp.dst or false) then selid = idx end entries[idx] = describe_t1_distant_aspect(spv) @@ -102,6 +102,8 @@ local function handle_signal_formspec_tabheader_fields(pname, fields) advtrains.interlocking.show_signal_form(pos, node, pname) elseif n == 2 then advtrains.interlocking.show_ip_form(pos, pname) + elseif n == 3 then + advtrains.interlocking.show_distant_signal_form(pos, pname) end return true end diff --git a/advtrains_interlocking/signal_aspects.lua b/advtrains_interlocking/signal_aspects.lua index eebb4ba..2866ae1 100644 --- a/advtrains_interlocking/signal_aspects.lua +++ b/advtrains_interlocking/signal_aspects.lua @@ -44,6 +44,27 @@ local function get_type2_definition(name) return type2defs[name] end +local function get_type2_danger(group) + local def = type2defs[group] + if not def then + return nil + end + local main = def.main + return main[#main] +end + +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 + local function type2main_to_type1(name, asp) local def = type2defs[name] if not def then @@ -53,7 +74,7 @@ local function type2main_to_type1(name, asp) if type(asp) == "number" then aspidx = asp else - aspidx = def.main[asp] + aspidx = def.main[asp] or 2 end local asptbl = def.main[aspidx] if not asptbl then @@ -62,11 +83,13 @@ local function type2main_to_type1(name, asp) if type(asp) == "number" then asp = asptbl.name end + local dst = def.main[math.min(#def.main, aspidx+1)].main local t = { main = asptbl.main, type2name = asp, type2group = name, + dst = dst, } if aspidx > 1 and aspidx < #asptbl then t.dst = asptbl[aspidx+1].main @@ -116,6 +139,7 @@ end return { register_type2 = register_type2, get_type2_definition = get_type2_definition, + get_type2_dst = get_type2_dst, type2main_to_type1 = type2main_to_type1, type1_to_type2main = type1_to_type2main, equalp = equalp, diff --git a/advtrains_interlocking/signal_main_ui.lua b/advtrains_interlocking/signal_main_ui.lua deleted file mode 100644 index e69de29..0000000 diff --git a/advtrains_interlocking/spec/basic_signalling_spec.lua b/advtrains_interlocking/spec/basic_signalling_spec.lua new file mode 100644 index 0000000..0b79972 --- /dev/null +++ b/advtrains_interlocking/spec/basic_signalling_spec.lua @@ -0,0 +1,87 @@ +--[[ +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 = { + aspects = fixture("../../signal_aspects"), + }, + 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") + +local D = advtrains.distant +local I = advtrains.interlocking + +local stub_aspect_t1 = { + free = {main = -1}, + slow = {main = 6}, + danger = {main = 0, shunt = false}, +} +local stub_pos_t1 = {} +for i = 1, 3 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"}, +} + +describe("API for supposed signal aspects", function() + it("should load and save data properly", function() + local tbl = {_foo = true} + I.load_supposed_aspects(tbl) + assert.same(tbl, I.save_supposed_aspects()) + end) + it("should set and get type 1 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)) + I.signal_set_aspect(pos, newasp) + assert.same(newasp, I.signal_get_aspect(pos)) + assert.same(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.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])) + 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.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])) + 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) +end) diff --git a/advtrains_interlocking/spec/signal_api_spec.lua b/advtrains_interlocking/spec/signal_api_spec.lua deleted file mode 100644 index cd7a1d1..0000000 --- a/advtrains_interlocking/spec/signal_api_spec.lua +++ /dev/null @@ -1,49 +0,0 @@ -require("mineunit") - -mineunit("core") - -_G.advtrains = { - interlocking = { - aspects = fixture("../../signal_aspects"), - }, - ndb = { - get_node = minetest.get_node, - } -} - -fixture("advtrains_helpers") -fixture("../../database") -sourcefile("signal_api") - -local stub_aspect_t1 = { main = math.random() } -local stub_pos_t1 = {x = 1, y = 0, z = 1} - -minetest.register_node(":stubsignal_t1", { - advtrains = { - supported_aspects = {}, - get_aspect = function () return stub_aspect_t1 end, - set_aspect = function () end, - }, - groups = { advtrains_signal = 2 }, -}) - -world.layout { - {stub_pos_t1, "stubsignal_t1"}, -} - -describe("API for supposed signal aspects", function() - it("should load and save data properly", function() - local tbl = {_foo = true} - advtrains.interlocking.load_supposed_aspects(tbl) - assert.same(tbl, advtrains.interlocking.save_supposed_aspects()) - end) - it("should set and get type 1 signals properly", function () - local pos = stub_pos_t1 - local asp = stub_aspect_t1 - local newasp = { dst = math.random() } - assert.same(asp, advtrains.interlocking.signal_get_aspect(pos)) - advtrains.interlocking.signal_set_aspect(pos, newasp) - assert.same(newasp, advtrains.interlocking.signal_get_aspect(pos)) - assert.same(asp, advtrains.interlocking.signal_get_real_aspect(pos)) - end) -end) diff --git a/advtrains_interlocking/tcb_ts_ui.lua b/advtrains_interlocking/tcb_ts_ui.lua index b3b8221..9f88296 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", @@ -608,7 +609,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,9.75]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 .. advtrains.interlocking.make_signal_formspec_tabheader(pname, tcbs.signal, 7, 1) 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]" @@ -670,6 +671,7 @@ function advtrains.interlocking.show_signalling_form(sigd, pname, sel_rte, calle 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..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... @@ -796,6 +798,10 @@ minetest.register_on_player_receive_fields(function(player, formname, fields) if fields.ars then tcbs.ars_disabled = not minetest.is_yes(fields.ars) end + + if fields.dst then + tcbs.nodst = not minetest.is_yes(fields.dst) + end if fields.auto then tcbs.route_auto = true -- cgit v1.2.3 From eb03b5f301c4244bbc79a101dd9f990b01503ab5 Mon Sep 17 00:00:00 2001 From: orwell Date: Fri, 5 Apr 2024 00:35:40 +0200 Subject: Continue with new-ks rework --- advtrains_interlocking/database.lua | 13 +- advtrains_interlocking/signal_api.lua | 274 ++++++++++++++++++++++++++++++---- advtrains_signals_ks/init.lua | 2 +- 3 files changed, 250 insertions(+), 39 deletions(-) (limited to 'advtrains_interlocking/database.lua') diff --git a/advtrains_interlocking/database.lua b/advtrains_interlocking/database.lua index c5ae906..e23b0e5 100644 --- a/advtrains_interlocking/database.lua +++ b/advtrains_interlocking/database.lua @@ -131,12 +131,9 @@ 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 + + -- let signal_api load data + advtrains.interlocking.signal.load(data) --COMPATIBILITY to Signal aspect format -- TODO remove in time... @@ -171,7 +168,7 @@ function ildb.load(data) end function ildb.save() - return { + local data = { tcbs = track_circuit_breaks, ts=track_sections, signalass = signal_assignments, @@ -182,6 +179,8 @@ function ildb.save() supposed_aspects = advtrains.interlocking.save_supposed_aspects(), distant = advtrains.distant.save(), } + advtrains.interlocking.signal.save(data) + return data end -- diff --git a/advtrains_interlocking/signal_api.lua b/advtrains_interlocking/signal_api.lua index c2cc08b..ce3b03f 100644 --- a/advtrains_interlocking/signal_api.lua +++ b/advtrains_interlocking/signal_api.lua @@ -5,7 +5,8 @@ local F = advtrains.formspec local signal = {} signal.MASP_HALT = { - name = "halt" + name = "halt", + description = "HALT", halt = true, } @@ -55,12 +56,12 @@ It should cause the signal to show its most restrictive aspect. Typically it is signals this would be "expect stop". == Aspect Info == -The actual signal aspect in the already-known format. This is what the trains use to determine halt/proceed and speed. In this, the dst field has to be resolved. +The actual signal aspect in the already-known format. This is what the trains use to determine halt/proceed and speed. asp = { main = 0 (halt) / -1 (max speed) / false (no info) / (speed limit) shunt = true (shunt free) / false (shunt not free) proceed_as_main = true (shunt move can proceed and become train move when main!=0) / false (no) - dst = (like main, informative character, not actually used) + dst = speed of the remote signal (like main, informative character, not actually used) } Node definition of signals: @@ -68,17 +69,84 @@ Node definition of signals: ndef.advtrains = { main_aspects = { { name = "proceed" description = "Proceed at full speed", } - { name = "proceed2" description = "Proceed at full speed", } - } -- The numerical order determines the layout of the list in the selection dialog. - apply_aspect = function(pos, asp_group, dst_aspgrp, dst_aspinfo) + { name = "reduced" description = "Proceed at reduced speed", } + } + -- This list is mainly for the selection dialog. Order of entries determines list order in the dropdown. + -- Some fields have special meaning: + -- name: A unique key to identify the main aspect. Only this key is saved, but APIs always receive the whole table + -- description: Text shown in UI dropdown + -- speed: a number. When present, a speed field is shown in the UI next to the dropdown (prefilled with the value). + -- When user selects a different speed there, this different speed replaces the value in the table whenever the main_aspect is applied. + -- Node can set any other fields at its discretion. They are not touched. + -- Note: On first call advtrains automatically inserts into the ndef.advtrains table a main_aspects_lookup hashtable + -- Note: Pure distant signals (that cannot show halt) should NOT have a main_aspects table + apply_aspect = function(pos, main_aspect, rem_aspect, rem_aspinfo) -- set the node to show the desired aspect - -- called by advtrains when this signal's aspect group or the distant signal's aspect changes + -- called by advtrains when this signal's aspect group or the remote signal's aspect changes -- MAY return the aspect_info. If it returns nil then get_aspect_info will be queried at a later point. - get_aspect_info(pos) - -- Returns the aspect info table (main, shunt, dst etc.)W + get_aspect_info(pos, main_aspect) + -- Returns the aspect info table (main, shunt, dst etc.) + distant_support = true or false + -- If true, signal is considered in distant signalling. If false or nil, rem_aspect and rem_aspinfo are never set. + route_role = one of "main", "shunt", "distant", "distant_repeater", "end" + -- Determines how the signal behaves when routes are set. Only in effect when signal is assigned to a TCB. + -- main: The signal is a possible endpoint for a train move route. Distant signals before it refer to it. + -- shunt: The signal is a possible endpoint for a shunt move route. Ignored for distant signals. + -- distant, distant_repeater: When route is set, signal is always assigned its first main aspect. The next signal with role="main" is set as the remote signal. (currently no further distinction) + -- end: like main, but signifies that it marks an end of track and trains cannot continue further. (currently no practical implications above main) } + +== Nomenclature == +The distant/main relation is named as follows: + V M +=====>====> +Main signal (main) always refers to the signal that is in focus right now (even if that is a distant-only signal) +From the standpoint of M, V is the distant (dst) signal. M does not need to concern itself with V's aspect but needs to notify V when it changes +From the standpoint of V, M is the remote (rem) signal. V needs to show an aspect that matches its remote signal M + +== Criteria for which signals are eligible for routes == + +All signals must define: +- get_aspect_info() + +Signals that can be assigned to a TCB must satisfy: +- apply_aspect() defined + +Signals that are possible start and end points for a route must satisfy: +- main_aspects defined (note, pure distant signals should therefore not define main_aspects) + ]] +-- Database +-- Signal Aspect store +-- Stores for each signal the main aspect and other info, like the assigned remote signal +-- [signal encodePos] = { main_aspect = "proceed", [speed = 12], [remote = encodedPos] } +signal.aspects = {} + +-- Distant signal notification. Records for each signal the distant signals that refer to it +-- Note: this mapping is weak. Needs always backreference check. +-- [signal encodePos] = { [distant signal encodePos] = true } +signal.distant_refs = {} + +function signal.load(data) + signal.aspects = data.aspects or {} + -- rebuild distant_refs after load + signal.distant_refs = {} + for main, aspt in pairs(signal.aspects) do + if aspt.remote then + if not signal.distant_refs[aspt.remote] then + signal.distant_refs[aspt.remote] = {} + end + signal.distant_refs[aspt.remote][main] = true + end + end +end + +function signal.save(data) + data.aspects = signal.aspects +end + + -- Set a signal's aspect. -- Signal aspects should only be set through this function. It takes care of: -- - Storing the main aspect and dst pos for this signal permanently (until next change) @@ -86,30 +154,184 @@ ndef.advtrains = { -- - Calling apply_aspect() in the signal's node definition to make the signal show the aspect -- - Calling apply_aspect() again whenever the distant signal changes its aspect -- - Notifying this signal's distant signals about changes to this signal (unless skip_dst_notify is specified) -function signal.set_aspect(pos, main_aspect, dst_pos, skip_dst_notify) - -- TODO +function signal.set_aspect(pos, main_asp_name, main_asp_speed, rem_pos, skip_dst_notify) + local main_pts = advtrains.encode_pos(pos) + local old_tbl = signal.aspects[main_pts] + local old_remote = old_tbl and old_tbl.remote + local new_remote = rem_pos and advtrains.encode_pos(rem_pos) + + -- if remote has changed, unregister from old remote + if old_remote and old_remote~=new_remote and signal.distant_refs[old_remote] then + signal.distant_refs[old_remote][main_pts] = nil + end + + signal.aspects[main_pts] = { main_aspect = main_asp_name, speed = main_asp_speed, remote = new_remote } + -- apply aspect on main signal, this also checks new_remote + signal.reapply_aspect(main_pts) + + -- notify my distants about this change (with limit 2) + if not skip_dst_notify then + signal.notify_distants_of(main_pts, 2) + end +end + +function signal.clear_aspect(pos, skip_dst_notify) + local main_pts = advtrains.encode_pos(pos) + local old_tbl = signal.aspects[main_pts] + local old_remote = old_tbl and old_tbl.remote + + -- unregister from old remote + if old_remote then + signal.distant_refs[old_remote][main_pts] = nil + end + + signal.aspects[main_pts] = nil + -- apply aspect on main signal, this also checks new_remote + signal.reapply_aspect(main_pts) + + -- notify my distants about this change (with limit 2) + if not skip_dst_notify then + signal.notify_distants_of(main_pts, 2) + end +end + +-- Notify distant signals of main_pts of a change in the aspect of this signal +-- +function signal.notify_distants_of(main_pts, limit) + if limit <= 0 then + return + end + local dstrefs = signal.distant_refs[main_pts] + if dstrefs then + for dst,_ in pairs(dstrefs) do + -- ensure that the backref is still valid + local dst_asp = signal.aspects[dst] + if dst_asp and dst_asp.remote == main_pts then + signal.reapply_aspect(dst) + signal.notify_distants_of(dst, limit - 1) + else + atwarn("Distant signal backref is not purged: main =",main_pts,", distant =",dst,", remote =",dst_asp.remote,"") + end + end + end +end + +function signal.notify_trains(pos) + local ipts, iconn = advtrains.interlocking.db.get_ip_by_signalpos(pos) + if not ipts then return end + local ipos = minetest.string_to_pos(ipts) + + -- 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 + +-- Update waiting trains and distant signals about a changed signal aspect +-- Must be called when a signal's aspect changes through some other means +-- and not via the signal mechanism +function signal.notify_on_aspect_changed(pos, skip_dst_notify) + signal.notify_trains(pos) + if not skip_dst_notify then + signal.notify_distants_of(advtrains.encode_pos(pos), 2) + end end -- Gets the stored main aspect and distant signal position for this signal -- This information equals the information last passed to set_aspect -- It does not take into consideration the actual speed signalling, please use -- get_aspect_info() for this +-- pos: the position of the signal -- returns: main_aspect, dst_pos function signal.get_aspect(pos) - --TODO + local aspt = signal.aspects[advtrains.encode_pos(pos)] + local ma,dp = signal.get_aspect_internal(pos, aspt) + return ma, advtrains.decode_pos(dp) +end + +local function cache_mainaspects(ndefat) + ndefat.main_aspects_lookup = { + -- always define halt aspect + halt = signal.MASP_HALT + } + for _,ma in ipairs(ndefat.main_aspects) then + ndefat.main_aspects_lookup[ma.name] = ma + end +end + +function signal.get_aspect_internal(pos, aspt) + if not aspt then + -- oh, no main aspect, nevermind + return nil, aspt.remote, nil + end + -- look aspect in nodedef + local node = advtrains.ndb.get_node_or_nil(pos) + local ndef = node and minetest.registered_nodes[node.name] + local ndefat = ndef and ndef.advtrains + -- only if signal defines main aspect and its set in aspt + if ndefat and ndefat.main_aspects and aspt.main_aspect then + if not ndefat.main_aspects_lookup then + cache_mainaspects(ndefat) + end + local masp = ndefat.main_aspects_lookup[aspt.name] + -- if speed, then apply speed + if masp.speed and aspt.speed then + masp = table.copy(masp) + masp.speed = aspt.speed + end + return masp, aspt.remote, ndef + end + -- invalid node or no main aspect, return nil for masp + return nil, aspt.remote, ndef end -function signal.get_distant_signals_of(pos) - --TODO +-- For the signal at pos, get the "aspect info" table. This contains the speed signalling information at this location +function signal.get_aspect_info(pos) + -- get aspect internal + local aspt = signal.aspects[advtrains.encode_pos(pos)] + local masp, remote, ndef = signal.get_aspect_internal(pos, aspt) + -- call into ndef + if ndef.advtrains and ndef.advtrains.get_aspect_info then + return ndef.advtrains.get_aspect_info(pos, masp) + end end + -- Called when either this signal has changed its main aspect -- or when this distant signal's currently assigned main signal has changed its aspect -- It retrieves the signal's main aspect and aspect info and calls apply_aspect of the node definition -- to update the signal's appearance and aspect info -- pts: The signal position to update as encoded_pos -function signal.reapply_aspect(pts, p_mainaspect) - --TODO +-- returns: the return value of the nodedef call which may be aspect_info +function signal.reapply_aspect(pts) + -- get aspt + local aspt = signal.aspects[pts] + if not aspt then + return -- oop, nothing to do + end + -- resolve mainaspect table by name + local pos = advtrains.decode_pos(pts) + -- note: masp may be nil, when aspt.main_aspect was nil. Valid case for distant-only signals + local masp, remote, ndef = signal.get_aspect_internal(pos, aspt) + -- if we have remote, resolve remote + local rem_masp, rem_aspi + if remote then + local rem_aspt = signal.aspects[remote] + if rem_aspt and rem_aspt.name then + local rem_pos = advtrains.decode_pos(remote) + rem_masp, _, rem_ndef = signal.get_aspect_internal(rem_pos, rem_aspt) + if rem_masp then + if rem_ndef.advtrains and rem_ndef.advtrains.get_aspect_info then + rem_aspi = rem_ndef.advtrains.get_aspect_info(pos, rem_masp) + end + end + end + end + -- call into ndef + if ndef.advtrains and ndef.advtrains.apply_aspect then + return ndef.advtrains.apply_aspect(pos, masp, rem_masp, rem_aspi) + end + -- notify trains + signal.notify_trains(pos) end -- Update this signal's aspect based on the set route @@ -121,26 +343,16 @@ function signal.update_route_aspect(tcbs, skip_dst_notify) end end + +---------------- + function signal.can_dig(pos) return not advtrains.interlocking.db.get_sigd_for_signal(pos) end function advtrains.interlocking.signal_after_dig(pos) - -- clear influence point - advtrains.interlocking.signal_clear_aspect(pos) - advtrains.distant.unassign_all(pos, true) -- TODO -end - --- Update waiting trains and distant signals about a changed signal aspect -function signal.notify_on_aspect_changed(pos, skip_dst_notify) - --TODO update distant? - local ipts, iconn = advtrains.interlocking.db.get_ip_by_signalpos(pos) - if not ipts then return end - local ipos = minetest.string_to_pos(ipts) - - -- 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) + -- TODO clear influence point + advtrains.interlocking.signal.clear_aspect(pos) end function signal.on_rightclick(pos, node, player, itemstack, pointed_thing) diff --git a/advtrains_signals_ks/init.lua b/advtrains_signals_ks/init.lua index c91b4ec..0a43db0 100755 --- a/advtrains_signals_ks/init.lua +++ b/advtrains_signals_ks/init.lua @@ -48,7 +48,7 @@ local function getzs3v(msp) return speed end -local setaspectf = function(rot) +local applyaspectf_main = function(rot) return function(pos, node, main_aspect, dst_aspect, dst_aspect_info) -- set zs3 signal to show speed according to main_aspect setzs3(pos, asp.zs3, rot) -- cgit v1.2.3