NRF SDK 0.9

Reading and acting

Read train timetables and delay

Separate assignment, simulation deadlines, predicted delay and line-plan offsets.

Identify what the train is assigned to run

Request TrainQuery(includeTimetables = true), which also requests service and the required tracks/stations. Assignment describes the timetable, timetable shift and order index when available. The line is joined from the observed service, which describes current activity; these facts answer different questions.

DataContract
TimetableIdentity of the assigned timetable. Its name is currently unresolved: name remains null.
TimetableShiftIdIdentity of a shift within a timetable. Keep timetableId and value together.
orderIndexObserved order index, possibly absent; do not confuse it with a station index.
Service stationLocation associated with current service; distinct from the physical position station.

Convert deadlines without mixing clocks

Service deadlines are simulation instants. In Native, train.service?.times groups raw values and their GameInstant values. In JVM, record.service directly provides values and Instant conversions. Date accessors account for the simulation origin; do not pass arrivalTimeUs directly to a conversion relative to 1970.

AccessorWhat it represents
observedAtSimulation instant associated with the service observation.
arrivalService arrival deadline, when available.
departureService departure deadline, when available.
dispatchRetryDispatch retry deadline, distinct from scheduled departure.
arrivalRemainingSecondsSigned duration until arrival; may be negative.
departureRemainingSecondsDuration until departure, clamped to zero once the deadline has passed.
dispatchRetryRemainingSeconds (Native) / dispatchRemainingSeconds (JVM)Duration until the dispatch retry, clamped to zero.

GameInstant retains UTC seconds and the microsecond. Its dateTime conversion produces a calendar value to the second; keep GameInstant for finer precision. Durations are not dates, and the real-world capturedAtMillis clock must not be subtracted from a simulation deadline.

Use the delay estimate for what it measures

predictedArrivalDelaySeconds is the signed estimate supplied by the game: positive when late, negative when early. It comes from predictedArrivalDelayUs, a duration in microseconds. It is not a difference to recompute between real-world time and arrival, nor a guarantee about future arrival.

Read the line plan without inventing an absolute passing time

Native exposes context.linePlan(trainId), a nullable LinePlan in the callback capture. Request the required batch first and avoid refreshing the network between related reads. In JVM, selectedTrain adds lineStops to the specialised batch. A missing plan may be unavailable or unstable; a waypoint outside a station may have stationId = null.

arrivalOffsetSeconds and departureOffsetSeconds are relative to the line plan. plannedDwellSeconds calculates their difference when both exist. Partial runs, loops or the train assignment prevent these offsets alone from becoming the next passing date: display them as plan offsets.

Prepare a timing card without invented values

Native: timetable and plan in the tool context
package wiki.traintimetables

import nimby.*

data class TrainTiming(
    val id: TrainId,
    val timetable: Timetable?,
    val shift: TimetableShiftId?,
    val orderIndex: Int?,
    val observedAt: GameInstant?,
    val arrival: GameInstant?,
    val departure: GameInstant?,
    val dispatchRetry: GameInstant?,
    val predictedArrivalDelaySeconds: Double?,
    val plan: LinePlan?,
)

fun readTiming(context: ToolContext, id: TrainId): TrainTiming? {
    // One rich query before reading the plan in this capture.
    val snapshot = context.trains(TrainQuery(includeTimetables = true))
    val train = snapshot[id] ?: return null
    val times = train.service?.times
    val plan = context.linePlan(id)
    return TrainTiming(
        id, train.assignment?.timetable, train.assignment?.shift,
        train.assignment?.orderIndex, times?.observedAt, times?.arrival,
        times?.departure, times?.dispatchRetry,
        train.predictedArrivalDelaySeconds, plan,
    )
}

// Keep offsets; do not manufacture a next passing date.
fun plannedDwells(plan: LinePlan): List<Pair<Int, Long?>> =
    plan.stops.map { it.index to it.plannedDwellSeconds }
JVM: selected train plan and joins within the capture
package wiki.jvmtraintimetables

import fr.nimby.sdk.*
import java.time.Instant

data class PlannedStop(
    val index: Int,
    val station: Station?,
    val arrivalOffsetSeconds: Int?,
    val departureOffsetSeconds: Int?,
    val dwellSeconds: Long?,
)

data class TrainTiming(
    val id: TrainId,
    val timetable: Timetable?,
    val shift: TimetableShiftId?,
    val orderIndex: Int?,
    val observedAt: Instant?,
    val arrival: Instant?,
    val departure: Instant?,
    val dispatchRetry: Instant?,
    val predictedArrivalDelaySeconds: Double?,
    val stops: List<PlannedStop>?,
)

fun readTiming(game: Game, id: TrainId): TrainTiming? {
    // selectedTrain adds its plan; the trains list remains a batch capture.
    val snapshot = game.trains.snapshot(
        selectedTrain = id,
        query = TrainQuery(includeTimetables = true),
    )
    val record = snapshot.train(id) ?: return null
    val service = record.service
    val stops = snapshot.lineStops?.map { stop ->
        PlannedStop(
            stop.index, stop.station?.let(snapshot::station),
            stop.arrivalOffsetSeconds, stop.departureOffsetSeconds,
            stop.plannedDwellSeconds,
        )
    }
    return TrainTiming(
        id, record.details?.timetable, record.details?.shift,
        record.details?.orderIndex, service?.observedAt, service?.arrival,
        service?.departure, service?.dispatchRetry,
        record.metadata?.predictedArrivalDelaySeconds, stops,
    )
}

readTiming preserves every absence, composite identity and kind of instant. Display predicted delay, service deadlines and the plan separately. PlannedStop and TrainTiming models can be tested with known, missing, early or expired values without running the game.