aboutsummaryrefslogtreecommitdiff
path: root/advtrains_interlocking/signal_api.lua
diff options
context:
space:
mode:
Diffstat (limited to 'advtrains_interlocking/signal_api.lua')
-rw-r--r--advtrains_interlocking/signal_api.lua886
1 files changed, 382 insertions, 504 deletions
diff --git a/advtrains_interlocking/signal_api.lua b/advtrains_interlocking/signal_api.lua
index c70366b..bf14247 100644
--- a/advtrains_interlocking/signal_api.lua
+++ b/advtrains_interlocking/signal_api.lua
@@ -1,576 +1,454 @@
-- Signal API implementation
+local F = advtrains.formspec
---[[
-Signal aspect table:
-Note: All speeds are measured in m/s, aka the number of + signs in the HUD.
-asp = {
- main = <int speed>,
- -- Main signal aspect, tells state and permitted speed of next section
- -- 0 = section is blocked
- -- >0 = section is free, speed limit is this value
- -- -1 = section is free, maximum speed permitted
- -- false/nil = Signal doesn't provide main signal information, retain current speed limit.
- shunt = <boolean>,
- -- Whether train may proceed as shunt move, on sight
- -- main aspect takes precedence over this
- -- When main==0, train switches to shunt move and is restricted to speed 6
- proceed_as_main = <boolean>,
- -- If an approaching train is a shunt move and 'shunt' is false,
- -- the train may proceed as a train move under the "main" aspect
- -- if the main aspect permits it (i.e. main!=0)
- -- If this is not set, shunt moves are NOT allowed to switch to
- -- a train move, and must stop even if "main" would permit passing.
- -- This is intended to be used for "Halt for shunt moves" signs.
-
- dst = <int speed>,
- -- Distant signal aspect, tells state and permitted speed of the section after next section
- -- The character of these information is purely informational
- -- At this time, this field is not actively used
- -- 0 = section is blocked
- -- >0 = section is free, speed limit is this value
- -- -1 = section is free, maximum speed permitted
- -- false/nil = Signal doesn't provide distant signal information.
-
- -- the character of call_on and dead_end is purely informative
- call_on = <boolean>, -- Call-on route, expect train in track ahead (not implemented yet)
- dead_end = <boolean>, -- Route ends on a dead end (e.g. bumper) (not implemented yet)
-
- w_speed = <integer>,
- -- "Warning speed restriction". Supposed for short-term speed
- -- restrictions which always override any other restrictions
- -- imposed by "speed" fields, until lifted by a value of -1
- -- (Example: german Langsamfahrstellen-Signale)
- }
-}
-
-== How signals actually work in here ==
-Each signal (in the advtrains universe) is some node that has at least the
-following things:
-- An "influence point" that is set somewhere on a rail
-- An aspect which trains that pass the "influence point" have to obey
+local signal = {}
-There can be static and dynamic signals. Static signals are, roughly
-spoken, signs, while dynamic signals are "real" signals which can display
-different things.
-
-The node definition of a signal node should contain those fields:
-groups = {
- advtrains_signal = 2,
- save_in_at_nodedb = 1,
-}
-advtrains = {
- set_aspect = function(pos, node, asp)
- -- This function gets called whenever the signal should display
- -- a new or changed signal aspect. It is not required that
- -- the signal actually displays the exact same aspect, since
- -- some signals can not do this by design. However, it must
- -- display an aspect that is at least as restrictive as the passed
- -- aspect as far as it is capable of doing so.
- -- Examples:
- -- - pure shunt signals can not display a "main" aspect
- -- and have no effect on train moves, so they will only ever
- -- honor the shunt.free field for their aspect.
- -- - the german Hl system can only signal speeds of 40, 60
- -- and 100 km/h, a speed of 80km/h should then be signalled
- -- as 60 km/h instead.
- -- In turn, it is not guaranteed that the aspect will fulfill the
- -- criteria put down in supported_aspects.
- -- If set_aspect is present, supported_aspects should also be declared.
-
- -- The aspect passed in here can always be queried using the
- -- advtrains.interlocking.signal_get_supposed_aspect(pos) function.
- -- It is always DANGER when the signal is not used as route signal.
-
- -- For static signals, this function should be completely omitted
- -- If this function is omitted, it won't be possible to use
- -- route setting on this signal.
- end,
- supported_aspects = {
- -- A table which tells which different types of aspects this signal
- -- is able to display. It is used to construct the "aspect editing"
- -- formspec for route programming (and others) It should always be
- -- present alongside with set_aspect. If this is not specified but
- -- set_aspect is, the user will be allowed to select any aspect.
- -- Any of the fields marked with <boolean/nil> support 3 types of values:
- nil: if this signal can switch between free/blocked
- false: always shows "blocked", unchangable
- true: always shows "free", unchangable
- -- Any of the "speed" fields should contain a list of possible values
- -- to be set as restriction. If omitted, the value of the described
- -- field is always assumed to be false (no information)
- -- A speed of 0 means that the signal can show a "blocked" aspect
- -- (which is probably the case for most signals)
- -- If the signal can signal "no information" on one of the fields
- -- (thus false is an acceptable value), include false in the list
- -- If your signal can only display a single speed (may it be -1),
- -- always enclose that single value into a list. (such as {-1})
- main = {<speed1>, ..., <speedn>} or nil,
- dst = {<speed1>, ..., <speedn>} or nil,
- shunt = <boolean/nil>,
-
- call_on = <boolean/nil>,
- dead_end = <boolean/nil>,
- w_speed = {<speed1>, ..., <speedn>} or nil,
-
- },
- Example for supported_aspects:
- supported_aspects = {
- main = {0, 6, -1}, -- can show either "Section blocked", "Proceed at speed 6" or "Proceed at maximum speed"
- dst = {0, false}, -- can show only if next signal shows "blocked", no other information.
- shunt = false, -- shunting by this signal is never allowed.
-
- call_on = false,
- dead_end = false,
- w_speed = nil,
- -- none of the information can be shown by the signal
-
- },
-
- get_aspect = function(pos, node)
- -- This function gets called by the train safety system. It
- should return the aspect that this signal actually displays,
- not preferably the input of set_aspect.
- -- For regular, full-featured light signals, they will probably
- honor all entries in the original aspect, however, e.g.
- simple shunt signals always return main=false regardless of
- the set_aspect input because they can not signal "Halt" to
- train moves.
- -- advtrains.interlocking.DANGER contains a default "all-danger" aspect.
- -- If your signal does not cover certain sub-tables of the aspect,
- the following reasonable defaults are automatically assumed:
- main = false (unchanged)
- dst = false (unchanged)
- shunt = false (shunting not allowed)
- info = {} (no further information)
- end,
+signal.MASP_HALT = {
+ name = "_halt",
+ halt = true,
}
-on_rightclick = advtrains.interlocking.signal_rc_handler
-can_dig = advtrains.interlocking.signal_can_dig
-after_dig_node = advtrains.interlocking.signal_after_dig
-
-(If you need to specify custom can_dig or after_dig_node callbacks,
-please call those functions anyway!)
-
-Important note: If your signal should support external ways to set its
-aspect (e.g. via mesecons), there are some things that need to be considered:
-- advtrains.interlocking.signal_get_supposed_aspect(pos) won't respect this
-- Whenever you change the signal aspect, and that aspect change
-did not happen through a call to
-advtrains.interlocking.signal_set_aspect(pos, asp), you are
-*required* to call this function:
-advtrains.interlocking.signal_on_aspect_changed(pos)
-in order to notify trains about the aspect change.
-This function will query get_aspect to retrieve the new aspect.
-
-]]--
-
-minetest.register_entity("advtrains_interlocking:ipmarker", {
- visual = "mesh",
- mesh = "trackplane.b3d",
- textures = {"at_il_signal_ip.png"},
- collisionbox = {-1,-0.5,-1, 1,-0.4,1},
- visual_size = {x=10, y=10},
- on_punch = function(self)
- self.object:remove()
- end,
- on_rightclick = function(self, player)
- if self.signalpos and player and player:is_player() then
- local node = minetest.get_node(self.signalpos)
- if minetest.get_item_group(node.name, "advtrains_signal") ~= 0 then
- advtrains.interlocking.show_ip_form(self.signalpos, player:get_player_name())
- end
- end
- end,
- get_staticdata = function() return "STATIC" end,
- on_activate = function(self, sdata) if sdata=="STATIC" then self.object:remove() end end,
- static_save = false,
-})
-
-local function clean_ipmarker(spos)
- for _, luaentity in pairs(minetest.luaentities) do
- if luaentity.name == "advtrains_interlocking:ipmarker"
- and luaentity.signalpos
- and vector.equals(luaentity.signalpos, spos) then
- luaentity.object:remove()
- end
- end
-end
-
-local function ipmarker(ipos, connid, spos)
- if spos then
- clean_ipmarker(spos)
- end
- local node_ok, conns, rhe = advtrains.get_rail_info_at(ipos, advtrains.all_tracktypes)
- if not node_ok then return end
-
- local obj = minetest.add_entity(vector.offset(ipos, 0, 0.2, 0), "advtrains_interlocking:ipmarker")
- if not obj then return end
- obj:set_yaw(advtrains.dir_to_angle(conns[connid].c))
- local luaentity = obj:get_luaentity()
- if luaentity then
- luaentity.signalpos = spos
- end
-end
+signal.MASP_DEFAULT = {
+ name = "_default",
+ default = true,
+}
-local DANGER = {
+signal.ASPI_HALT = {
main = 0,
- dst = false,
shunt = false,
}
-advtrains.interlocking.DANGER = DANGER
-advtrains.interlocking.GENERIC_FREE = {
+signal.ASPI_FREE = {
main = -1,
shunt = false,
- dst = false,
+ proceed_as_main = true,
}
-local function convert_aspect_if_necessary(asp)
- if type(asp.main) == "table" then
- local newasp = {}
- if asp.main.free then
- newasp.main = asp.main.speed
- else
- newasp.main = 0
- end
- if asp.dst and asp.dst.free then
- newasp.dst = asp.dst.speed
- else
- newasp.dst = 0
+--[[
+Implementation plan orwell 2024-01-28:
+Most parts of ywang's implementation are fine, especially I like the formspecs. But I would like to change a few aspects (no pun intended) of this.
+- Signal gets distant assigned via field in signal aspect table (instead of explicitly)
+- Signal speed/shunt are no longer free-text but rather they need to be predefined in the node definition
+To do this: Differentiation between:
+== Main Aspect ==
+This is what a signal is assigned by either the route system or the user.
+It is a string key which has an appropriate entry in the node definition (where it has a description assigned)
+The signal mod defines a function to set a signal to the most appropriate aspect. This function gets
+a) the main aspect table (straight from node def)
+b) the distant signal's aspect group name & aspect table
+
+== Aspect ==
+One concrete combination of lights/shapes that a signal signal shows. Handling these is at the discretion of
+the signal mod defining the signal, and they are typically combinations of main aspect and distant aspect
+Example:
+- A Ks signal has the main_aspect="proceed_12" set for a route
+- The signal at the end of the route shows main_aspect="proceed_8", advtrains also passes on that this means {main=8, shunt=false}
+- The ndef.afunction(pos, node, main_aspect, rem_aspect, rem_aspinfo) determines that the signal should now show
+ blinking green with main indicator 12 and dst indicator 8, and sets the nodes accordingly.
+ This function can now return the Aspect Info table, which will be cached by advtrains until the aspect changes again
+ and will be used when a train approaches the signal. If nil is returned, then the aspect will be queried next time
+ by calling ndef.advtrains.get_aspect_info(pos)
+
+Note that once apply_aspect returns, there is no need for advtrains anymore to query the aspect info.
+When the signal, for any reason, wants to change its aspect by itself *without* going through the signal API then
+it should update the aspect info cache by calling advtrains.interlocking.signal.update_aspect_info(pos)
+
+Apply_aspect may also receive the special main aspect { name = "_halt", halt = true }. It usually means that the signal is not assigned to anything particular,
+and it should cause the signal to show its most restrictive aspect. Typically it is a halt aspect, but e.g. for distant-only
+signals this would be "expect stop".
+
+A special case occurs for pure distant signals: Such signals must set apply_aspect, but must not set main_aspects. Behavior is as follows:
+- Signal is uninitialized, distant signal is not assigned to a main signal, or no route is set: main_aspect == { name = "_halt", halt = true } and rem_aspect == nil
+- A remote main signal is assigned (either by user or by route): main_aspect is always { name = "_default" } and rem_aspect / rem_aspinfo give the correct information
+
+Main aspect names starting with underscore (e.g. "_default") are reserved and must not be used!
+
+== Aspect Info ==
+The actual signal aspect in the already-known format. This is what the trains use to determine halt/proceed and speed.
+asp = {
+ main = 0 (halt) / -1 (max speed) / false (no info) / <number> (speed limit)
+ shunt = true (shunt free) / false (shunt not free)
+ proceed_as_main = true (shunt move can proceed and become train move when main!=0) / false (no)
+ dst = speed of the remote signal (like main, informative character, not actually used)
+}
+
+Node definition of signals:
+- The signal needs some logic to figure out, for each combination of its own aspect group and the distant signal's aspect, what aspect info it can/will show.
+ndef.advtrains = {
+ main_aspects = {
+ { name = "proceed" description = "Proceed at full speed", <more data at discretion of signal>}
+ { name = "reduced" description = "Proceed at reduced speed", <more data at discretion of signal>}
+ }
+ -- This list is mainly for the selection dialog. Order of entries determines list order in the dropdown.
+ -- Some fields have special meaning:
+ -- name: A unique key to identify the main aspect. Might be required by some code.
+ -- description: Text shown in UI dropdown
+ -- Node can set any other fields at its discretion. They are not touched.
+ -- Note: Pure distant signals (that cannot show halt) should NOT have a main_aspects table.
+ -- For these signals no main aspect selection UI is shown and they cannot be startpoint of a route
+ apply_aspect = function(pos, node, main_aspect, rem_aspect, rem_aspinfo)
+ -- set the node to show the desired aspect
+ -- called by advtrains when this signal's aspect group or the remote signal's aspect changes
+ -- main_aspect is never nil, but can be one of the special aspects { halt = true } or { default = true }
+ -- MAY return the aspect_info. If it returns nil then get_aspect_info will be queried at a later point.
+ get_aspect_info(pos, main_aspect)
+ -- Returns the aspect info table (main, shunt, dst etc.)
+ distant_support = true or false
+ -- If true, signal is considered in distant signalling. If false or nil, rem_aspect and rem_aspinfo are never set.
+ route_role = one of "main", "main_distant", "shunt", "distant", "distant_repeater", "end"
+ -- Determines how the signal behaves when routes are set. Only in effect when signal is assigned to a TCB.
+ -- main: The signal is a possible endpoint for a train move route. Distant signals before it refer to it.
+ -- shunt: The signal is a possible endpoint for a shunt move route. Ignored for distant signals.
+ -- distant, distant_repeater: The next signal with role="main" is set as the remote signal. main_aspects may be undefined, the main aspect passed to apply_aspect is a dummy one in this case.
+ -- distant: if more than one distant signal is before a main signal, only the last one is assigned (but any number of distant_repeater signals are allowed)
+ -- main_distant: Combination of main and distant - like "main", but additionally gets assigned to the next main like a "distant"
+ -- end: like main, but signifies that it marks an end of track and trains cannot continue further. (currently no practical implications above main)
+}
+
+== Nomenclature ==
+The distant/main relation is named as follows:
+ V M
+=====>====>
+Main signal (main) always refers to the signal that is in focus right now (even if that is a distant-only signal)
+From the standpoint of M, V is the distant (dst) signal. M does not need to concern itself with V's aspect but needs to notify V when it changes
+From the standpoint of V, M is the remote (rem) signal. V needs to show an aspect that matches its remote signal M
+
+== Criteria for which signals are eligible for routes ==
+
+All signals must define:
+- get_aspect_info()
+
+Signals that can be assigned to a TCB must satisfy:
+- apply_aspect() defined
+
+Signals that are possible start and end points for a route must satisfy:
+- main_aspects defined (note, pure distant signals should therefore not define main_aspects)
+
+]]
+
+-- Database
+-- Signal Aspect store
+-- Stores for each signal the main aspect and other info, like the assigned remote signal
+-- [signal encodePos] = { main = <table or string>, [remote = encodedPos] }
+-- main is a string: "named aspect" is looked up in the main_aspects table of the ndef
+-- main is a table: this table directly is the main aspect (used for advanced signals with additional lights/indicators)
+signal.aspects = {}
+
+-- Distant signal notification. Records for each signal the distant signals that refer to it
+-- Note: this mapping is weak. Needs always backreference check.
+-- [signal encodePos] = { [distant signal encodePos] = true }
+signal.distant_refs = {}
+
+function signal.load(data)
+ signal.aspects = data.aspects or {}
+ -- rebuild distant_refs after load
+ signal.distant_refs = {}
+ for main, aspt in pairs(signal.aspects) do
+ if aspt.remote then
+ if not signal.distant_refs[aspt.remote] then
+ signal.distant_refs[aspt.remote] = {}
+ end
+ signal.distant_refs[aspt.remote][main] = true
end
- newasp.proceed_as_main = asp.shunt.proceed_as_main
- newasp.shunt = asp.shunt.free
- -- Note: info table not transferred, it's not used right now
- return newasp
end
- return asp
end
-function advtrains.interlocking.update_signal_aspect(tcbs)
- if tcbs.signal then
- local asp = tcbs.aspect or DANGER
- advtrains.interlocking.signal_set_aspect(tcbs.signal, asp)
- end
+function signal.save(data)
+ data.aspects = signal.aspects
end
-function advtrains.interlocking.signal_can_dig(pos)
- return not advtrains.interlocking.db.get_sigd_for_signal(pos)
-end
-function advtrains.interlocking.signal_after_dig(pos)
- -- clear influence point
- advtrains.interlocking.db.clear_ip_by_signalpos(pos)
- clean_ipmarker(pos)
+-- Set a signal's aspect.
+-- Signal aspects should only be set through this function. It takes care of:
+-- - Storing the main aspect and dst pos for this signal permanently (until next change)
+-- - Assigning the distant signal for this signal
+-- - Calling apply_aspect() in the signal's node definition to make the signal show the aspect
+-- - Calling apply_aspect() again whenever the remote signal changes its aspect
+-- - Notifying this signal's distant signals about changes to this signal (unless skip_dst_notify is specified)
+-- main_asp: either a string (==name in ndef.advtrains.main_aspects) or the main aspect table directly (for advanced signals)
+function signal.set_aspect(pos, main_asp, rem_pos, skip_dst_notify)
+ -- safeguard for the two integrated aspects (these two must be passed as string key)
+ if type(main_asp)=="table" and (main_asp.name=="_default" or main_asp.name=="_halt") then
+ error("MASP_HALT and MASP_DEFAULT must be passed via string keys _halt or _default, not as tables!")
+ end
+ local main_pts = advtrains.encode_pos(pos)
+ local old_tbl = signal.aspects[main_pts]
+ local old_remote = old_tbl and old_tbl.remote
+ local new_remote = rem_pos and advtrains.encode_pos(rem_pos)
+
+ -- if remote has changed, unregister from old remote
+ if old_remote and old_remote~=new_remote and signal.distant_refs[old_remote] then
+ atdebug("unregister old remote: ",old_remote,"from",main_pts)
+ signal.distant_refs[old_remote][main_pts] = nil
+ end
+
+ signal.aspects[main_pts] = { main = main_asp, remote = new_remote }
+ -- apply aspect on main signal, this also checks new_remote
+ signal.reapply_aspect(main_pts)
+
+ -- notify my distants about this change (with limit 2)
+ if not skip_dst_notify then
+ signal.notify_distants_of(main_pts, 2)
+ end
end
-function advtrains.interlocking.signal_set_aspect(pos, asp)
- asp = convert_aspect_if_necessary(asp)
- local node=advtrains.ndb.get_node(pos)
- local ndef=minetest.registered_nodes[node.name]
- if ndef and ndef.advtrains and ndef.advtrains.set_aspect then
- ndef.advtrains.set_aspect(pos, node, asp)
- advtrains.interlocking.signal_on_aspect_changed(pos)
+function signal.clear_aspect(pos, skip_dst_notify)
+ local main_pts = advtrains.encode_pos(pos)
+ local old_tbl = signal.aspects[main_pts]
+ local old_remote = old_tbl and old_tbl.remote
+
+ -- unregister from old remote
+ if old_remote then
+ signal.distant_refs[old_remote][main_pts] = nil
+ end
+
+ signal.aspects[main_pts] = nil
+ -- apply aspect on main signal, this also checks new_remote
+ signal.reapply_aspect(main_pts)
+
+ -- notify my distants about this change (with limit 2)
+ if not skip_dst_notify then
+ signal.notify_distants_of(main_pts, 2)
end
end
--- should be called when aspect has changed on this signal.
-function advtrains.interlocking.signal_on_aspect_changed(pos)
- local ipts, iconn = advtrains.interlocking.db.get_ip_by_signalpos(pos)
- if not ipts then return end
- local ipos = minetest.string_to_pos(ipts)
+-- Clear any info about aspects from this signal, without resetting/reapplying the aspect.
+-- Supposed to be used for legacy on-off signals when the on-off toggle is used
+function signal.unregister_aspect(pos)
+ local main_pts = advtrains.encode_pos(pos)
+ local old_tbl = signal.aspects[main_pts]
+ local old_remote = old_tbl and old_tbl.remote
- advtrains.invalidate_all_paths_ahead(ipos)
+ -- unregister from old remote
+ if old_remote then
+ signal.distant_refs[old_remote][main_pts] = nil
+ end
+
+ signal.aspects[main_pts] = nil
end
-function advtrains.interlocking.signal_rc_handler(pos, node, player, itemstack, pointed_thing)
- local pname = player:get_player_name()
- local control = player:get_player_control()
- if control.aux1 then
- advtrains.interlocking.show_ip_form(pos, pname)
+-- Notify distant signals of main_pts of a change in the aspect of this signal
+--
+function signal.notify_distants_of(main_pts, limit)
+ atdebug("notify_distants_of",advtrains.decode_pos(main_pts),"limit",limit)
+ if limit <= 0 then
return
end
-
- local sigd = advtrains.interlocking.db.get_sigd_for_signal(pos)
- if sigd then
- advtrains.interlocking.show_signalling_form(sigd, pname)
- else
- local ndef = minetest.registered_nodes[node.name]
- if ndef.advtrains and ndef.advtrains.set_aspect then
- -- permit to set aspect manually
- local function callback(pname, aspect)
- advtrains.interlocking.signal_set_aspect(pos, aspect)
+ local dstrefs = signal.distant_refs[main_pts]
+ atdebug("dstrefs",dstrefs,"")
+ if dstrefs then
+ for dst,_ in pairs(dstrefs) do
+ -- ensure that the backref is still valid
+ local dst_asp = signal.aspects[dst]
+ if dst_asp and dst_asp.remote == main_pts then
+ signal.reapply_aspect(dst)
+ signal.notify_distants_of(dst, limit - 1)
+ else
+ atwarn("Distant signal backref is not purged: main =",main_pts,", distant =",dst,", remote =",dst_asp.remote,"")
end
- local isasp = ndef.advtrains.get_aspect(pos, node)
-
- advtrains.interlocking.show_signal_aspect_selector(
- pname,
- ndef.advtrains.supported_aspects,
- "Set aspect manually", callback,
- isasp)
- else
- --static signal - only IP
- advtrains.interlocking.show_ip_form(pos, pname)
end
end
end
--- Returns the aspect the signal at pos is supposed to show
-function advtrains.interlocking.signal_get_supposed_aspect(pos)
- local sigd = advtrains.interlocking.db.get_sigd_for_signal(pos)
- if sigd then
- local tcbs = advtrains.interlocking.db.get_tcbs(sigd)
- if tcbs.aspect then
- return convert_aspect_if_necessary(tcbs.aspect)
- end
- end
- return DANGER;
-end
+function signal.notify_trains(pos)
+ local ipts, iconn = advtrains.interlocking.db.get_ip_by_signalpos(pos)
+ if not ipts then return end
+ local ipos = minetest.string_to_pos(ipts)
--- Returns the actual aspect of the signal at position, as returned by the nodedef.
--- returns nil when there's no signal at the position
-function advtrains.interlocking.signal_get_aspect(pos)
- local node=advtrains.ndb.get_node(pos)
- local ndef=minetest.registered_nodes[node.name]
- if ndef and ndef.advtrains and ndef.advtrains.get_aspect then
- local asp = ndef.advtrains.get_aspect(pos, node)
- if not asp then asp = DANGER end
- return convert_aspect_if_necessary(asp)
- end
- return nil
+ -- FIXME: invalidate_all_paths_ahead does not appear to always work as expected
+ --advtrains.invalidate_all_paths_ahead(ipos)
+ minetest.after(0, advtrains.invalidate_all_paths, ipos)
end
--- Returns the "supported_aspects" of the signal at position, as returned by the nodedef.
--- returns nil when there's no signal at the position
-function advtrains.interlocking.signal_get_supported_aspects(pos)
- local node=advtrains.ndb.get_node(pos)
- local ndef=minetest.registered_nodes[node.name]
- if ndef and ndef.advtrains and ndef.advtrains.supported_aspects then
- local asp = ndef.advtrains.supported_aspects
- return asp
+-- Update waiting trains and distant signals about a changed signal aspect
+-- Must be called when a signal's aspect changes through some other means
+-- and not via the signal mechanism
+function signal.notify_on_aspect_changed(pos, skip_dst_notify)
+ signal.notify_trains(pos)
+ if not skip_dst_notify then
+ signal.notify_distants_of(advtrains.encode_pos(pos), 2)
end
- return nil
end
-local players_assign_ip = {}
+-- Gets the stored main aspect and distant signal position for this signal
+-- This information equals the information last passed to set_aspect
+-- It does not take into consideration the actual speed signalling, please use
+-- get_aspect_info() for this
+-- pos: the position of the signal
+-- returns: main_aspect, dst_pos
+function signal.get_aspect(pos)
+ local aspt = signal.aspects[advtrains.encode_pos(pos)]
+ local ma,dp = signal.get_aspect_internal(pos, aspt)
+ return ma, dp and advtrains.decode_pos(dp)
+end
--- shows small info form for signal IP state/assignment
--- only_notset: show only if it is not set yet (used by signal tcb assignment)
-function advtrains.interlocking.show_ip_form(pos, pname, only_notset)
- if not minetest.check_player_privs(pname, "interlocking") then
- return
- end
- local form = "size[7,5]label[0.5,0.5;Signal at "..minetest.pos_to_string(pos).."]"
- advtrains.interlocking.db.check_for_duplicate_ip(pos)
- local pts, connid = advtrains.interlocking.db.get_ip_by_signalpos(pos)
- if pts then
- form = form.."label[0.5,1.5;Influence point is set at "..pts.."/"..connid.."]"
- form = form.."button_exit[0.5,2.5; 5,1;set;Move]"
- form = form.."button_exit[0.5,3.5; 5,1;clear;Clear]"
- local ipos = minetest.string_to_pos(pts)
- ipmarker(ipos, connid, pos)
- else
- form = form.."label[0.5,1.5;Influence point is not set.]"
- form = form.."label[0.5,2.0;It is recommended to set an influence point.]"
- form = form.."label[0.5,2.5;This is the point where trains will obey the signal.]"
-
- form = form.."button_exit[0.5,3.5; 5,1;set;Set]"
- end
- if not only_notset or not pts then
- minetest.show_formspec(pname, "at_il_ipassign_"..minetest.pos_to_string(pos), form)
- end
+local function cache_mainaspects(ndefat)
+ ndefat.main_aspects_lookup = {}
+ for _,ma in ipairs(ndefat.main_aspects) do
+ ndefat.main_aspects_lookup[ma.name] = ma
+ end
+ ndefat.main_aspects_lookup[signal.MASP_HALT.name] = signal.MASP_HALT.name -- halt is always defined
+ ndefat.main_aspects_lookup[signal.MASP_DEFAULT.name] = ndefat.main_aspects[1] -- default is the first one
end
-minetest.register_on_player_receive_fields(function(player, formname, fields)
- local pname = player:get_player_name()
- if not minetest.check_player_privs(pname, {train_operator=true, interlocking=true}) then
- return
- end
- local pts = string.match(formname, "^at_il_ipassign_([^_]+)$")
- local pos
- if pts then
- pos = minetest.string_to_pos(pts)
- end
- if pos then
- if fields.set then
- advtrains.interlocking.signal_init_ip_assign(pos, pname)
- elseif fields.clear then
- advtrains.interlocking.db.clear_ip_by_signalpos(pos)
- clean_ipmarker(pos)
- end
- end
-end)
--- inits the signal IP assignment process
-function advtrains.interlocking.signal_init_ip_assign(pos, pname)
- if not minetest.check_player_privs(pname, "interlocking") then
- minetest.chat_send_player(pname, "Insufficient privileges to use this!")
- return
+-- gets the main aspect. resolves named aspects to aspect table on demand
+function signal.get_aspect_internal(pos, aspt)
+ -- look up node and nodedef
+ local node = advtrains.ndb.get_node_or_nil(pos)
+ local ndef = node and minetest.registered_nodes[node.name]
+ if not aspt then
+ -- oh, no main aspect, nevermind
+ return signal.MASP_HALT, nil, node, ndef
end
- --remove old IP
- --advtrains.interlocking.db.clear_ip_by_signalpos(pos)
- minetest.chat_send_player(pname, "Configuring Signal: Please look in train's driving direction and punch rail to set influence point.")
+ local ndefat = ndef.advtrains or {}
+ local masp = aspt.main or signal.MASP_HALT
- players_assign_ip[pname] = pos
+ if type(masp) == "string" then
+ if masp=="_halt" then
+ masp = signal.MASP_HALT
+ elseif masp=="_default" and not ndefat.main_aspects then
+ -- case is fine, distant only signal
+ masp = signal.MASP_DEFAULT
+ else
+ assert(ndefat.main_aspects, "With named aspects, node needs advtrains.main_aspects table!")
+ -- resolve the main aspect from the mainaspects table
+ if not ndefat.main_aspects_lookup then
+ cache_mainaspects(ndefat)
+ end
+ masp = ndefat.main_aspects_lookup[aspt.main] or signal.MASP_DEFAULT
+ end
+ end
+ -- return whatever the main aspect is
+ return masp, aspt.remote, node, ndef
end
-minetest.register_on_punchnode(function(pos, node, player, pointed_thing)
- local pname = player:get_player_name()
- if not minetest.check_player_privs(pname, "interlocking") then
- return
- end
- -- IP assignment
- local signalpos = players_assign_ip[pname]
- if signalpos then
- if vector.distance(pos, signalpos)<=50 then
- local node_ok, conns, rhe = advtrains.get_rail_info_at(pos, advtrains.all_tracktypes)
- if node_ok and #conns == 2 then
-
- local yaw = player:get_look_horizontal()
- local plconnid = advtrains.yawToClosestConn(yaw, conns)
-
- -- add assignment if not already present.
- local pts = advtrains.roundfloorpts(pos)
- if not advtrains.interlocking.db.get_ip_signal_asp(pts, plconnid) then
- advtrains.interlocking.db.set_ip_signal(pts, plconnid, signalpos)
- ipmarker(pos, plconnid, signalpos)
- minetest.chat_send_player(pname, "Configuring Signal: Successfully set influence point")
- else
- minetest.chat_send_player(pname, "Configuring Signal: Influence point of another signal is already present!")
- end
- else
- minetest.chat_send_player(pname, "Configuring Signal: This is not a normal two-connection rail! Aborted.")
- end
+-- For the signal at pos, get the "aspect info" table. This contains the speed signalling information at this location
+function signal.get_aspect_info(pos)
+ -- get aspect internal
+ local aspt = signal.aspects[advtrains.encode_pos(pos)]
+ local masp, remote, node, ndef = signal.get_aspect_internal(pos, aspt)
+ -- call into ndef
+ if ndef.advtrains and ndef.advtrains.get_aspect_info then
+ local ai = ndef.advtrains.get_aspect_info
+ if type(ai)=="function" then
+ ai = ai(pos, masp)
+ end
+ if type(ai)=="table" then
+ atdebug(pos,"aspectinfo",ai)
+ return ai
else
- minetest.chat_send_player(pname, "Configuring Signal: Node is too far away. Aborted.")
+ error("For node "..node.name..": ndef.advtrains.get_aspect_info must be function or table")
end
- players_assign_ip[pname] = nil
+
end
-end)
-
-
---== aspect selector ==--
+end
-local players_aspsel = {}
---[[
-suppasp: "supported_aspects" table
-purpose: form title string
-callback: func(pname, aspect) called on form submit
-isasp: aspect currently set
-]]
-function advtrains.interlocking.show_signal_aspect_selector(pname, p_suppasp, p_purpose, callback, isasp)
- local suppasp = p_suppasp or {
- main = {0, -1}, dst = {false}, shunt = false, info = {},
- }
- local purpose = p_purpose or ""
-
- local form = "size[7,7]label[0.5,0.5;Select Signal Aspect:]"
- form = form.."label[0.5,1;"..purpose.."]"
-
- form = form.."label[0.5,1.5;== Main Signal ==]"
- local selid = 1
- local entries = {}
- for idx, spv in ipairs(suppasp.main) do
- local entry
- if spv == 0 then
- entry = "Halt"
- elseif spv == -1 then
- entry = "Continue at maximum speed"
- elseif not spv then
- entry = "Continue\\, speed limit unchanged (no info)"
- else
- entry = "Continue at speed of "..spv
+-- Called when either this signal has changed its main aspect
+-- or when this distant signal's currently assigned main signal has changed its aspect
+-- It retrieves the signal's main aspect and aspect info and calls apply_aspect of the node definition
+-- to update the signal's appearance and aspect info
+-- pts: The signal position to update as encoded_pos
+-- returns: the return value of the nodedef call which may be aspect_info
+function signal.reapply_aspect(pts)
+ -- get aspt
+ local aspt = signal.aspects[pts]
+ atdebug("reapply_aspect",advtrains.decode_pos(pts),"aspt",aspt)
+ local pos = advtrains.decode_pos(pts)
+ -- resolve mainaspect table by name
+ local masp, remote, node, ndef = signal.get_aspect_internal(pos, aspt)
+ -- if we have remote, resolve remote
+ local rem_masp, rem_aspi
+ if remote then
+ -- register in remote signal as distant
+ if not signal.distant_refs[remote] then
+ signal.distant_refs[remote] = {}
end
- -- hack: the crappy formspec system returns the label, not the index. save the index in it.
- entries[idx] = idx.."| "..entry
- if isasp and spv == (isasp.main or false) then
- selid = idx
+ signal.distant_refs[remote][pts] = true
+ local rem_aspt = signal.aspects[remote]
+ atdebug("resolving remote",advtrains.decode_pos(remote),"aspt",rem_aspt)
+ local rem_pos = advtrains.decode_pos(remote)
+ rem_masp, _, _, rem_ndef = signal.get_aspect_internal(rem_pos, rem_aspt)
+ if rem_masp then
+ if rem_ndef.advtrains and rem_ndef.advtrains.get_aspect_info then
+ rem_aspi = rem_ndef.advtrains.get_aspect_info(rem_pos, rem_masp)
+ end
end
end
- form = form.."dropdown[0.5,2;6;main;"..table.concat(entries, ",")..";"..selid.."]"
+ -- call into ndef
+ atdebug("applying to",pos,": main_asp",masp,"rem_masp",rem_masp,"rem_aspi",rem_aspi)
+ if ndef.advtrains and ndef.advtrains.apply_aspect then
+ ndef.advtrains.apply_aspect(pos, node, masp, rem_masp, rem_aspi)
+ end
+ -- notify trains
+ signal.notify_trains(pos)
+end
-
- form = form.."label[0.5,3;== Shunting ==]"
- if suppasp.shunt == nil then
- local st = 1
- if isasp and isasp.shunt then st=2 end
- form = form.."dropdown[0.5,3.5;6;shunt_free;---,allowed;"..st.."]"
+-- Update this signal's aspect based on the set route
+--
+function signal.update_route_aspect(tcbs, skip_dst_notify)
+ if tcbs.signal then
+ local asp = tcbs.route_aspect or "_halt"
+ local rem = tcbs.route_remote
+ signal.set_aspect(tcbs.signal, asp, rem, skip_dst_notify)
end
+end
- form = form.."label[0.5,4.5;== Distant Signal ==]"
- local selid = 1
- local entries = {}
- for idx, spv in ipairs(suppasp.dst) do
- local entry
- if spv == 0 then
- entry = "Expect to stop at the next signal"
- elseif spv == -1 then
- entry = "Expect to pass the next signal at maximum speed"
- elseif not spv then
- entry = "No info"
- else
- entry = string.format("Expect to pass the next signal at speed of %d", spv)
- end
- entries[idx] = idx.."| "..entry
- if isasp and spv == (isasp.dst or false) then
- selid = idx
+-- Returns how capable the signal is with regards to aspect setting
+-- 0: not a signal at all
+-- 1: signal has get_aspect_info() but the aspect is not variable (e.g. a sign)
+-- 2: signal has apply_aspect() but does not have main aspects (e.g. a pure distant signal)
+-- 3: Full capabilities, signal has main aspects and can be used as main/shunt signal (can be start/endpoint of a route)
+function signal.get_signal_cap_level(pos)
+ local node = advtrains.ndb.get_node_or_nil(pos)
+ local ndef = node and minetest.registered_nodes[node.name]
+ local ndefat = ndef and ndef.advtrains
+ if ndefat and ndefat.get_aspect_info then
+ if ndefat.apply_aspect then
+ if ndefat.main_aspects then
+ return 3
+ end
+ return 2
end
+ return 1
end
- form = form.."dropdown[0.5,5;6;dst;"..table.concat(entries, ",")..";"..selid.."]"
-
- form = form.."button_exit[0.5,6;5,1;save;Save signal aspect]"
-
- local token = advtrains.random_id()
-
- minetest.show_formspec(pname, "at_il_sigaspdia_"..token, form)
-
- minetest.after(1, function()
- players_aspsel[pname] = {
- suppasp = suppasp,
- callback = callback,
- token = token,
- }
- end)
+ return 0
end
-local function usebool(sup, val, free)
- if sup == nil then
- return val==free
- else
- return sup
+----------------
+
+function signal.can_dig(pos)
+ local sigd = advtrains.interlocking.db.get_sigd_for_signal(pos)
+ if sigd then
+ local tcbs = advtrains.interlocking.db.get_tcbs(sigd)
+ if tcbs.routeset then
+ return false
+ end
end
+ return true
end
--- other side of hack: extract the index
-local function ddindex(val)
- return tonumber(string.match(val, "^(%d+)|"))
+function signal.after_dig(pos, oldnode, oldmetadata, player)
+ -- unassign signal if necessary
+ local sigd = advtrains.interlocking.db.get_sigd_for_signal(pos)
+ if sigd then
+ local tcbs = advtrains.interlocking.db.get_tcbs(sigd)
+ advtrains.interlocking.db.set_sigd_for_signal(pos, nil)
+ tcbs.signal = nil
+ tcbs.route_aspect = nil
+ tcbs.route_remote = nil
+ minetest.chat_send_player(player:get_player_name(), "Signal has been unassigned. Name and routes are kept for reuse.")
+ end
+ -- TODO clear influence point
+ advtrains.interlocking.signal.unregister_aspect(pos)
end
--- TODO use non-hacky way to parse outputs
-
-minetest.register_on_player_receive_fields(function(player, formname, fields)
+function signal.on_rightclick(pos, node, player, itemstack, pointed_thing)
local pname = player:get_player_name()
- local psl = players_aspsel[pname]
- if psl then
- if formname == "at_il_sigaspdia_"..psl.token then
- if fields.save then
- local maini = ddindex(fields.main)
- if not maini then return end
- local dsti = ddindex(fields.dst)
- if not dsti then return end
- local asp = {
- main = psl.suppasp.main[maini],
- dst = psl.suppasp.dst[dsti],
- shunt = usebool(psl.suppasp.shunt, fields.shunt_free, "allowed"),
- info = {}
- }
- psl.callback(pname, asp)
- end
- else
- players_aspsel[pname] = nil
- end
- end
-
-end)
+ local control = player:get_player_control()
+ advtrains.interlocking.show_signal_form(pos, node, pname, control.aux1)
+end
+
+advtrains.interlocking.signal = signal