User Tools

Site Tools


Minetest Forum
Content Database
Git Repository
Bug Tracker



The Path is a mapping of whole (positive or negative) numbers to a world position, describing a continuous section of track.

Every path node corresponds to a piece of track in the world. The path is a property of every ''train'' Table and is used in various ways:

  1. The wagons of a train use the path and their position in the train to determine their absolute position, rotation and movement direction in the world
  2. The train-to-train collision system relies on nodes in the path being occupied or free. This includes coupling.
  3. Nodes in the path are used as base positions for checking collisions with the environment and player damage (a cube centered around the nearest path node is scanned)
  4. Node callbacks are run per path node


The index is a positive, fractional number that describes a certain position in a path.

index =  5   -> The position described is the path item 5
index =  3.5 -> The point halfway between path items 3 and 4
index =  2.75-> The point between path items 2 and 3, 3/4 of the distance towards item 3
index = -4   -> path item -4
index = -3.5 -> the point halfway between path items -4 and -3

A whole index always maps to a path item. In many places where the exact position between items doesn't matter, the index is rounded to map to the nearest path item directly, instead of interpolating between path items.

Every train has an 'index' property which tells the exact index on the train's path where the front of the train is. The 'end_index' is calculated every step and tells the exact index where the back of the train is.

A train is moved by advancing its index. It is important to note that the train's 'index' is always incremented, never decremented. See also “reversion” in ''train'' Table.


It is important to keep in mind that the index has little to no relation to actual distances in the cartesian space. Depending on the track orientation, the distance between path items can vary from 1 to >2 nodes. The real distance of every item from path item 0 is encoded in train.path_dist

To calculate an index for a given starting index and a given distance, you can use advtrains.path_get_index_by_offset()

In train table

The following tables in the train table are handled by the path system:

-- path      - path positions. 'indices' are relative to this. At the moment, at.round_vector_floor_y(path[i])
--              is the node this item corresponds to, however, this will change in the future.
-- path_node - (reserved)
-- path_cn   - Connid of the current node that points towards path[i+1]
-- path_cp   - Connid of the current node that points towards path[i-1]
--     When the day comes on that path!=node, these will only be set if this index represents a transition between rail nodes
-- path_dist - The total distance of this path element from path element 0
-- path_dir  - The direction of this path item's transition to the next path item, which is the angle of conns[path_cn[i]].c
-- path_speed   - Filled by the LZB subsystem. For every path item, if set, defines the maximum velocity the train is allowed to have in the moment it passes this path item.
--              - If 0, the train will stop 0.1 indices before this path item (definable by LZB_ZERO_APPROACH_DIST in trainlogic.lua)
-- path_ext_f/b - how far path[i] is set
-- path_trk_f/b - how far the path extends along a track. beyond those values, paths are generated in a straight line.
-- path_req_f/b - how far path items were requested in the last step

Path generation

The path is generated on the fly, as path items are requested.

Every call to advtrains.path_get() (or one of the related functions) automatically generates the path as far as needed.

In order to access any of the secondary path tables (such as path_speed or path_cn), you need to make sure that advtrains.path_get() was called before. However, advtrains ensures that the range from train.index to train.end_index is always existent.

The path system deletes path items that are behind the train and no longer needed automatically.

Invalidation and restoration

Whenever the tracks in the world change (e.g. a switch is switched), it is required to update (say 'invalidate') the paths of trains that include the changed track. This happens by a call to advtrains.invalidate_all_paths(pos).

With the new_lzb branch, it is preferred to use advtrains.invalidate_all_paths_ahead(pos). See invalidate ahead below.

A path invalidation clears all path-related tables and variables. They remain cleared until the next call to train_ensure_init(), which should be called before operating on a train, but is at least called before the next train step.

To restore the path from save files or after a path invalidation, the following values are saved in the train (and in the save files) in every step:

train.last_pos -- the world position of the path item currently at floor(train.index)
train.last_connid = the connid of the track connection pointing forward
train.last_frac = the fractional part of train.index

To restore, train.last_pos becomes path item 0, train.index becomes last_frac, and path generation continues in last_connid direction. This causes an index shift, which also prevents integer overflows.

Note that, to have an estimated rough position of a train, you can simply query train.last_pos.

A path invalidation can also occur when LZB checkpoints change, see LZB for more info on that.

Invalidate Ahead

In order to prevent a complete path recalculation when a far-away rail changes, path_invalidate_ahead() was added in the new_lzb branch. This function clears path items that are ahead of (exclusive) the passed index, and does not cause an index shift.

Obviously, due to the way the path restoration works, this would cause problems when the starting index is behind the train's current index. By default, when this case happens, path_invalidate_ahead() throws an error. However passing the ignore_when_passed = true parameter causes nothing to happen in this case, which is sometimes desirable and sufficient (e.g. when a switch is switched after the train has already passed it, it doesn't matter).

path_invalidate_ahead() also invalidates LZB checkpoints and re-calls approach callbacks up from the given index. Important: LZB invalidation occurs inclusive, so the first approach callback to be called again is the node at index, although the first path item that was actually cleared was index + 1. This is required for interlocking signals to work.

In the future, path_invalidate_ahead() is to be preferred over path_invalidate().


A callback system exists to react to path-related events. Most internal components (e.g. LZB) use these to hook into the path system.

Path callbacks

There's a relevant comment in occupation.lua:

Callback system for 3rd-party path checkers:
advtrains.te_register_on_new_path(func(id, train))
-- Called when a train's path is re-initalized, either when it was invalidated
-- or the saves were just loaded
-- It can be assumed that everything is in the state of when the last run
-- of on_update was made, but all indices are shifted by an unknown amount.

advtrains.te_register_on_update(func(id, train))
-- Called each step and after a train moved, its length changed or some other event occured
-- The path is unmodified, and train.index and train.end_index can be reliably
-- queried for the new position and length of the train.
-- note that this function might be called multiple times per step, and this 
-- function being called does not necessarily mean that something has changed.
-- It is ensured that on_new_path callbacks are executed prior to these callbacks whenever
-- an invalidation or a reload occured.

advtrains.te_register_on_create(func(id, train))
-- Called right after a train is created, right after the initial new_path callback
advtrains.te_register_on_remove(func(id, train))
-- Called right before a train is deleted

All callbacks are allowed to save certain values inside the train table, but they must ensure that
those are reinitialized in the on_new_path callback. The on_new_path callback must explicitly
set ALL OF those values to nil or to a new updated value, and must not rely on their existence.

new_lzb adds another callback for invalidate_ahead:

advtrains.te_register_on_invalidate_ahead(func(id, train, start_idx))

Node Callbacks

The node callbacks are driven by a special Path Callback defined in trainlogic.lua:

-- enter/leave-node callbacks
-- signature is advtrains.tnc_register_on_enter/leave(function(pos, id, train, index) ... end)
advtrains.tnc_register_on_enter(func(pos, id, train, index))
advtrains.tnc_register_on_leave(func(pos, id, train, index))

-- Node callback for approaching
-- Might be called multiple times, whenever path is recalculated. Also called for the first node the train is standing on, then has_entered is true.
-- signature is function(pos, id, train, index, has_entered, lzbdata)
-- has_entered: true if the "enter" callback has already been executed for this train in this location
-- lzbdata: arbitrary data (shared between all callbacks), deleted when LZB is restarted.
-- These callbacks are called in order of distance as train progresses along tracks, so lzbdata can be used to
-- keep track of a train's state once it passes this point
advtrains.tnc_register_on_approach(func(pos, id, train, index, has_entered, lzbdata))
dev/core/path.txt · Last modified: 2023-01-11 15:13 by 56independent