NRF SDK 0.9

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 elementInterpretation
ToolRouteTrack.lengthMObserved length in metres. ToolTopology omits a track without a reliable length, together with its signals.
ToolTrackEnd.A / BGeometric 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 / UnknownA junction requires an explicit routing policy. Unknown does not prove a dead end: stop the calculation without inventing a continuation.
junctionOffsetsMSorted 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

ToolTopology.kt — pure function
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.