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.
| Data | Contract |
|---|---|
| Timetable | Identity of the assigned timetable. Its name is currently unresolved: name remains null. |
| TimetableShiftId | Identity of a shift within a timetable. Keep timetableId and value together. |
| orderIndex | Observed order index, possibly absent; do not confuse it with a station index. |
| Service station | Location 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.
| Accessor | What it represents |
|---|---|
| observedAt | Simulation instant associated with the service observation. |
| arrival | Service arrival deadline, when available. |
| departure | Service departure deadline, when available. |
| dispatchRetry | Dispatch retry deadline, distinct from scheduled departure. |
| arrivalRemainingSeconds | Signed duration until arrival; may be negative. |
| departureRemainingSeconds | Duration 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
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 }
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.