Getting started
Your first mod
Build a two-aspect teaching signal with images, a setting and driving behaviour.
Declare the signal in Kotlin
Prerequisite: the project and mod.json from the setup guide. Add the complete file below. This model opens when its block is known clear and its other conditions hold; otherwise it closes. It is a teaching starting point, not a complete signalling system.
package nimby.mod
import nimby.*
// These names describe your indications, not native codes.
enum class Aspect { Closed, Open }
enum class Reason { Unknown, Disabled, Occupied, Clear }
val firstSignal = signalModel(
id = "monmod.signal",
title = "My first signal",
textures = "mon_premier_signal",
fallback = Indication(Aspect.Closed, Reason.Unknown)
) {
construction(states = listOf("closed.svg", "open.svg"))
val active = checkbox("active", "Enable the signal", defaultValue = true)
rules {
when {
settingsStatus == SettingsStatus.Unavailable -> Indication(Aspect.Closed, Reason.Unknown)
!enabled(active) -> Indication(Aspect.Closed, Reason.Disabled)
!fresh || !routeKnown || observation.lampFailed || observation.forcedStop ->
Indication(Aspect.Closed, Reason.Unknown)
block == Occupancy.Clear -> Indication(Aspect.Open, Reason.Clear)
block == Occupancy.Occupied -> Indication(Aspect.Closed, Reason.Occupied)
else -> Indication(Aspect.Closed, Reason.Unknown)
}
}
// Both SVG files are declared once in construction.
images { indication ->
when (indication.aspect) {
Aspect.Closed -> "closed.svg"
Aspect.Open -> "open.svg"
}
}
driving { indication ->
when (indication.aspect) {
Aspect.Closed -> AutomaticDriving.stop()
Aspect.Open -> AutomaticDriving.clear()
}
}
}
// The mod is the package; the model above retains its own types and rules.
fun createMod(): SignallingMod = signalMod(modInfo) {
metadata(author = "Your name", description = "My first signalling mod.")
signal(firstSignal)
}
Add the two images
File names in construction and images are relative to the package root. assets is copied to that root: assets/closed.svg therefore becomes closed.svg. The mod.txt catalogue is generated from the Kotlin declaration.
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="64" viewBox="0 0 32 64"><rect x="6" y="2" width="20" height="48" rx="10" fill="#161616"/><circle cx="16" cy="15" r="7" fill="#ef4444"/><path d="M16 50v14" stroke="#888" stroke-width="4"/></svg><svg xmlns="http://www.w3.org/2000/svg" width="32" height="64" viewBox="0 0 32 64"><rect x="6" y="2" width="20" height="48" rx="10" fill="#161616"/><circle cx="16" cy="37" r="7" fill="#22c55e"/><path d="M16 50v14" stroke="#888" stroke-width="4"/></svg>You can replace the artwork. If you change a file name, update all of its Kotlin references. Preserve the order of a catalogue already used by saves.
Build and try the signal
.\gradlew.bat assembleReleaseMod verifyNativeModThe package is created in build/gradle/mod/release. verifyNativeMod checks loading, stopping and reloading without opening the game. Then add the project to your Hub developer profile, build it with the same kit and activate the profile while the game is closed.
- Launch a test game with the mod resources enabled and place the signal.
- On a known clear block, check the green image; with an occupied block or missing data, check closure.
- Clear “Enable signal”: this example explicitly requests closure. Check the driving behaviour as well as the colour.
Read the model responsibilities
| Declaration | Responsibility |
|---|---|
| signalModel | Model identity, aspect types and fallback. |
| checkbox → enabled | A setting declared and then read by a rule. |
| rules → Indication | A decision explained by an aspect and a reason. |
| images | Image matching the decision. |
| driving | Driving instruction matching the decision. |
| signalMod → signal | Assembly of models into the mod. |