NRF SDK 0.9

Creating a mod

Images and blinking

Declare resources, select images and verify animation against simulation time.

Connect files to the indication

Starting from a working signalModel, place SVGs in assets and declare every used path through construction(states = …). assets/closed.svg becomes closed.svg in the package. images selects a catalogue path from the complete indication; it changes no driving rule.

Fragment inside a two-aspect model
construction(
    states = listOf("closed.svg", "open.svg"),
    size = 4, left = true
)
images { indication ->
    when (indication.aspect) {
        Aspect.Closed -> "closed.svg"
        Aspect.Open -> "open.svg"
    }
}

size accepts 0 through 4; left and size are initial construction values. Without arguments, defaults are size=0 and left=false. The example explicitly chooses left and size 4. Keep catalogue order stable; changing a label does not justify reordering images from an already used version.

Each model owns its image selection. An ordinal shared by two enums does not mean a shared aspect. Declare complete package-relative paths and verify they exist with assembleReleaseMod.

Declare a game-time cadence

BlinkSignal.kt
package wiki.blinking

import nimby.*

enum class BlinkAspect { Closed, Flashing }
enum class BlinkReason { Unknown, Clear }

private val closedImage = steady("closed.svg")
private val flashingImage = blink(on = "on.svg", off = "off.svg", everyMs = 250)

val blinkingSignal = signalModel(
    "monmod.clignotant", "Blinking signal", "textures_clignotantes",
    fallback = Indication(BlinkAspect.Closed, BlinkReason.Unknown)
) {
    construction(states = listOf("closed.svg", "on.svg", "off.svg"))
    rules {
        if (fresh && routeKnown && block == Occupancy.Clear &&
            !observation.forcedStop && !observation.lampFailed)
            Indication(BlinkAspect.Flashing, BlinkReason.Clear)
        else Indication(BlinkAspect.Closed, BlinkReason.Unknown)
    }
    appearance { if (it.aspect == BlinkAspect.Flashing) flashingImage else closedImage }
    // Blinking grants no permission: this model always requires a stop.
    driving { AutomaticDriving.stop() }
}

Add blinkingSignal to your signalMod and supply closed.svg, on.svg and off.svg in assets. When the rule returns Flashing, on and off alternate every 250 simulation milliseconds. This model deliberately always requires stopping: animation creates no permission.

DeclarationResult
steady(path)A fixed image regardless of simulation time.
blink(on, off, everyMs)Two images, each lasting 100 through 10000 simulation ms. 500 gives a complete 1000 ms cycle.
appearance { … }Returns the indication’s appearance description; declare reused descriptions once.
frameAt(simulationMs)Locally computes the frame at a nonnegative time for tests.

Pause freezes the phase; acceleration follows simulation time. Signals with the same cadence are synchronized. The mod needs no thread, real-time timer or rule reevaluation for each frame change. Actual visible smoothness still depends on game rendering.

Local phase-boundary check
val animation = blink("on.svg", "off.svg", everyMs = 250)
check(animation.frameAt(0) == "on.svg")
check(animation.frameAt(249) == "on.svg")
check(animation.frameAt(250) == "off.svg")
check(animation.frameAt(500) == "on.svg")

Distinguish rule, resource and display

  • Test the indication produced by rules before checking its image.
  • Verify every returned path is declared and present in the package.
  • Test frameAt boundaries, then observe pause and acceleration in a test game.
  • If the image is correct but movement is unexpected, inspect the driving rule and its reason separately.