Creating a mod
Traverse tracks with ToolTopology
Distances, connections, junctions and travel direction without interpreting the game engine.
Prepare a working copy
Prerequisites: a tool service and a known source signal. The goal is to find a position a given distance ahead of that signal, or refuse when data cannot support a result. In the callback, read network(), then call topology() once. Reuse this copy during calculation and preview renewals; it does not guarantee the game remains unchanged.
| Public element | Interpretation |
|---|---|
| ToolRouteTrack.lengthM | Observed length in metres. ToolTopology omits a track without a reliable length, together with its signals. |
| ToolTrackEnd.A / B | Geometric ends of the track, independent of train direction. |
| ToolTrackConnection.Join(trackId, entry) | A reciprocal connection to a known track; entry identifies the end used to enter it. Entering through A continues toward B, and vice versa. |
| ToolTrackConnection.Junction / Unknown | A junction requires an explicit routing policy. Unknown does not prove a dead end: stop the calculation without inventing a continuation. |
| junctionOffsetsM | Sorted junction positions in metres from A. Also check junctions inside the track, not only endpoint connections. |
| ToolSignal.travelDirection / placementAt(...) | Normalized direction is +1 from A to B and −1 from B to A. placementAt converts that direction for the source model and returns a position usable for both preview and placement. Do not inspect kind to reverse directions yourself. |
A deliberately bounded traversal
package wiki.topology
import nimby.*
// Follow one unambiguous chain. Stop at junctions, unknown links or cycles.
// This policy does not select a route, reserve it or prove it is unoccupied.
fun positionAhead(network: ToolNetwork, sourceId: Long, distanceM: Double): SignalPosition? {
require(distanceM.isFinite() && distanceM > 0)
val topology = network.topology()
val source = topology.signal(sourceId) ?: return null
var track = topology.track(source.track) ?: return null
var direction = source.travelDirection
var offset = source.fraction * track.lengthM
var remaining = distanceM
val visited = HashSet<Long>()
repeat(4096) {
if (!visited.add(track.id)) return null
val toEnd = if (direction > 0) track.lengthM - offset else offset
val stop = minOf(remaining, toEnd)
if (track.junctionOffsetsM.any { val ahead = (it - offset) * direction; ahead >= 0 && ahead <= stop }) return null
if (remaining < toEnd) return source.placementAt(track.id, (offset + direction * remaining) / track.lengthM, direction)
if (remaining == toEnd) return null // No placement exactly at an endpoint.
val connection = track.connection(if (direction > 0) ToolTrackEnd.B else ToolTrackEnd.A)
if (connection !is ToolTrackConnection.Join) return null
remaining -= toEnd
track = topology.track(connection.trackId) ?: return null
direction = if (connection.entry == ToolTrackEnd.A) 1 else -1
offset = if (direction > 0) 0.0 else track.lengthM
}
return null
}
The example finds a position a given distance ahead of the source. It follows only simple connections, rejects exact endpoints, stops at every junction or unknown value, and bounds the number of visited tracks. It returns null when this policy cannot determine a position. It checks neither occupancy nor spacing from other signals nor construction-command validity; add those rules to your planner.