NRF SDK 0.9

Creating a mod

Create a tool with a window and clock

A standalone toolMod with no selected signal: form, event, confirmation and date change.

Open a tool independently of signals

Complete read-only tool
package nimby.mod

import nimby.*

fun createMod() = toolMod(modInfo) {
    metadata(author = "Your name", description = "Read the game clock.")
    window("clock", "Clock", shortcut = "F9") { event ->
        showWindow(event, clock().dateTime().toString(),
            listOf(ToolButton("refresh", "Refresh")))
    }
}

Prepare the project with the installation guide: mod.json generates modInfo. This tool declares no signal or texture. A game must be loaded and observed. The shortcut opens a Windows window owned by the game window; it is not a button on the game’s toolbar. Closing the window does not stop the mod.

DeclarationExact purpose
window(id, title, shortcut, handler)id identifies the window and stays stable; title names the window and its shortcut row, and accepts tr; shortcut defaults to F8 when omitted. Up to 8 windows, with distinct IDs and nonempty shortcuts. The player can change each shortcut in Options → NRF Hub, under Shortcuts.
Ctrl / Alt / Shift · A–Z · 0–9 · F1–F24Optional modifiers in this order, followed by one key; named keys such as Enter and PageUp are also available. Empty disables the shortcut. The SDK checks conflicts with the game and other mods and opens the tool only in an active game context outside text entry.
ToolWindowEventwindow identifies the window; action is open on opening and then the button ID; values contains all integer fields. sequence, worldId and generation identify the request and game session.
showWindow(event, message, buttons, inputs)message appears above the fields and buttons. At most 8 fields and 12 buttons. id is internal, label is visible, and enabled enables or disables the control. The complete UTF-8 form is limited to 8192 bytes.

Integer fields support typing, deletion and pasting. On a button click, every value must satisfy its bounds; one event carries the complete form. There is no event per keystroke. The callback should return promptly; losing the game session hides windows and invalidates events.

The example proposes F9 on first use. The value declared in code does not replace an existing compatible player preference, including a disabled shortcut. Do not change the "clock" identifier to rename the window or propose another default shortcut.

The title, message, field labels and buttons accept tr. An already open window follows game language changes without another open event: current input, even when empty, text selection and a pending request are preserved. Only a new showWindow replaces the form with the values supplied by your mod.

Read and change the calendar

Function or typeContract
clock(): ToolClockFresh observation of the current session: utcSeconds is a Unix date in seconds; elapsedMillis is elapsed simulation time in milliseconds. This operation does not capture the entire network.
GameDateTime · ToolClock.dateTime()UTC Gregorian calendar, years 1 to 9999 and months 1 to 12. Impossible dates are rejected. No Windows timezone or game display offset is added. toUtcSeconds and fromUtcSeconds convert without writing to the game.
changeTime(date: GameDateTime, recalculateTrains = false)Applies the selected date. Without recalculation, it preserves positions and shifts relative deadlines. Skipped days are not simulated. Fractions of a second are preserved; the UTC-seconds overload remains available.
recalculateTrains = trueAlso requests native interventions on trains. They may move trains and cost money. This choice must be explicit in your interface.
ToolTimeChange(clock, interventions)Clock returned after application and count of native interventions. An error or timeout does not prove that no effect occurred.
Inside the confirmation callback
val target = GameDateTime(2026, 9, 28, 12, 0, 0)
// Call only after your own confirmation step.
val result = changeTime(target, recalculateTrains = false)
log("Applied UTC=${result.clock.dateTime()}; interventions=${result.interventions}")

BB Timechange offers one mode: changing time with native train interventions, which may move trains and incur costs. Its flow is: read, fill six fields, display the effects, confirm once, read again. The SDK also retains the mode without interventions for other tools. The mod consumes the command before the native call so it never replays it after an uncertain response. Keep confirmation data and session identity, never the ToolContext. Reset confirmations on stop and session changes.