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(-) 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