Creating a mod
Mod options and shortcuts
Declare player preferences, read their typed values and let the SDK manage window shortcuts.
Add options to a mod
This Kotlin/Native API belongs to the 0.9 edition under development. A mod registers its option objects with options(...) inside toolMod or signalMod, including multiple-model declarations. The SDK presents them in Options → NRF Hub. A tool that only declares windows does not need options(...): its shortcuts appear automatically. The project must use a kit and runtime SDK that support this API.
| Section in Options → NRF Hub | Contents |
|---|---|
| Interface | Preferences declared with options(...): checkboxes, numeric values and choices. |
| Shortcuts | Shortcuts for windows declared with window(...), which the player can customize or disable. |
Only sections matching the loaded mods’ settings appear. If there are only shortcuts, they appear directly under Shortcuts; if there are only preferences, they appear directly under Interface. Navigation buttons appear only when both sections are needed. Each section groups settings by mod and uses its labels; the mod does not need to create an additional settings interface.
In a Kotlin/Native project created with the kit, replace Entry.kt with this example and use the manifest below. The plugin generates modInfo from mod.json: toolMod(modInfo) keeps the Kotlin and package identities consistent. The group title in this example is Clock tools; metadata(name = tr("mod.name")) separately supplies the translated name in the game’s mod list.
{
"id": "clock-history",
"name": "Clock tools",
"modId": "ClockHistory",
"version": "0.1.0-alpha.1",
"module": "ClockHistoryMod",
"language": "kotlin-native",
"sdkMin": "0.9.0-alpha.1",
"sdkMaxExclusive": "0.10.0",
"gameSha256": [
"fff49ac21720abfc824c2b4f68b862727630eb0db71cfe1f9ea8f685d0db10ae"
]
}package nimby.mod
import nimby.*
private val showDate = BooleanOption("showDate", tr("options.showDate"), true)
private val historySize = IntegerOption(
"historySize", tr("options.historySize"), defaultValue = 5, minimum = 1, maximum = 20,
)
private val order = ChoiceOption("order", tr("options.order"), listOf(
OptionChoice("newest", tr("options.newest")),
OptionChoice("oldest", tr("options.oldest")),
), "newest")
private val samples = mutableListOf<ToolClock>()
private var session: Pair<String, Long>? = null
fun createMod() = toolMod(modInfo) {
metadata(author = "Your name", name = tr("mod.name"), description = tr("mod.description"))
options(showDate, historySize, order)
window("history", tr("window.history"), shortcut = "F9") { event ->
val currentSession = worldId to generation
if (session != currentSession) {
samples.clear()
session = currentSession
}
when (event.action) {
"open", "sample" -> samples.add(clock())
"refresh" -> Unit
else -> return@window
}
val limit = historySize.value
val newestFirst = order.value == "newest"
val displayDate = showDate.value
while (samples.size > limit) samples.removeAt(0)
val rows = if (newestFirst) samples.asReversed() else samples
val message = rows.joinToString("\n") { sample ->
if (displayDate) sample.dateTime().toString()
else "${sample.elapsedMillis} ms"
}
showWindow(event, message, listOf(
ToolButton("sample", tr("history.sample")),
ToolButton("refresh", tr("history.refresh")),
))
}
onStop {
samples.clear()
session = null
}
}
Opening the window and the Add a reading button record a clock observation. Refresh display reads the preferences without adding a reading. The player chooses the number of retained rows, their order and the displayed text. The mod reads .value during the callback, then limits its history to at most twenty readings. History is cleared when the game session changes or the mod stops; the SDK keeps the saved preferences.
{
"fallback": "en",
"languages": {
"en": {
"mod.name": "Clock tools",
"mod.description": "Keep a short history of clock readings.",
"window.history": "Reading history",
"options.showDate": "Show dates",
"options.historySize": "Number of readings",
"options.order": "Reading order",
"options.newest": "Newest first",
"options.oldest": "Oldest first",
"history.sample": "Add a reading",
"history.refresh": "Refresh display"
},
"fr": {
"mod.name": "Outils d’horloge",
"mod.description": "Conserver un court historique des lectures de l’horloge.",
"window.history": "Historique des lectures",
"options.showDate": "Afficher les dates",
"options.historySize": "Nombre de lectures",
"options.order": "Ordre des lectures",
"options.newest": "Plus récentes en premier",
"options.oldest": "Plus anciennes en premier",
"history.sample": "Ajouter une lecture",
"history.refresh": "Actualiser l’affichage"
}
}
}Name the group, window and options
The mod name and its window title have distinct roles. In Shortcuts, the SDK first shows the mod group, followed by each window’s title and key combination. Seeing BB Timechange followed by Date and time therefore describes one group and one action, not two installations of the mod.
| Displayed element | Source in the mod | Example |
|---|---|---|
| Mod group | toolMod(modInfo) → modInfo.title → mod.json.name | Clock tools |
| Name in the game’s mod list | metadata(name = tr("mod.name")) | Clock tools |
| Window and its shortcut | window(..., title = tr("window.history")) | Reading history |
| Preference | BooleanOption.label / IntegerOption.label / ChoiceOption.label | Show dates — tr("options.showDate") |
| Choice label | OptionChoice.label | Newest first — tr("options.newest") |
The group uses GameMod.title: with toolMod(modInfo) or signalMod(modInfo), it comes from the name field in mod.json. mod.name and window.history are translation keys chosen by the author for the other labels in this example. The SDK does not automatically add the mod name to the window title. Give the group the product name and each window a short title describing its purpose. If window.history already contains the mod name, that name will appear again within its section.
metadata(name = ...) supplies mod information for the game’s mod list; it does not replace the title used for the group in Options → NRF Hub. Use tr(...) and assets/translations.json to translate visible names while keeping identifiers unchanged.
Choose the right type
| Declaration | Value and validation |
|---|---|
| BooleanOption | value is a Boolean. The default is false when omitted. |
| IntegerOption | value is an Int between minimum and maximum, inclusive. The default must satisfy these bounds. |
| ChoiceOption | Two to sixteen distinct OptionChoice entries. value returns the stable choice ID, never its translated label. defaultValue must name an existing choice. |
| window(..., shortcut = ...) | Each window automatically has its configurable shortcut. An empty string disables it; the SDK opens the window and calls its handler. |
An option or OptionChoice id must start with an ASCII letter and then use only ASCII letters, digits, _, . or -, up to 128 characters. Option IDs are unique within the mod; choice IDs are unique within their list. The SDK reserves the window. prefix for shortcut options. Labels accept tr and are limited to 256 UTF-8 bytes; description is optional, accepts tr and allows 1024 bytes.
Keep the same option objects that you registered with options(...). .value is read-only and reads neither the network nor disk. The SDK updates values between callbacks; an invalid change does not replace previously accepted values. A signal rule can read .value without ToolContext.
When an object is created, .value equals defaultValue. The SDK subsequently applies saved preferences. Read .value when handling an action or calculating a rule: a copy made during Kotlin file initialization would retain that earlier value. Declaring two objects with the same id does not link them, and registering duplicate IDs is rejected.
Let the SDK manage shortcuts
Each window(id, title, shortcut, handler) automatically adds a shortcut option; no additional option declaration is needed. shortcut is its default; the player can change or disable it under the mod’s name in the Shortcuts section of Options → NRF Hub. A stable window ID preserves the preference when you reorder declarations. This version provides window-opening shortcuts without a general key callback.
window("history", tr("window.history"), shortcut = "F9") { event ->
showWindow(event, clock().dateTime().toString(), emptyList())
}The history identifier is independent of the translated title. For this ID, the SDK creates the reserved window.history option; you neither declare it nor write its preferences file. Keep the id passed to window stable to preserve the assignment even when its title or window order changes. A mod can declare at most eight windows; their IDs are unique, with 1 to 128 ASCII letters, digits, _, . or -. Two windows in the same mod cannot declare the same nonempty initial shortcut; multiple empty shortcuts are allowed.
Format: Ctrl+, Alt+, Shift+ in that order, each optional, followed by a key. Supported keys: A–Z, 0–9, F1–F24, Backspace, Tab, Enter, Escape, Space, Left, Right, Up, Down, Home, End, PageUp, PageDown, Insert and Delete. Examples: F8, Ctrl+T, Alt+F12, Ctrl+Alt+Shift+F24. An empty string disables the shortcut.
The SDK checks the game bindings and those of loaded mods. A conflicting assignment is rejected and the previous value is retained. An existing conflict disables the affected trigger until resolved. If game bindings cannot be verified, shortcut use remains suspended. Shortcuts must not open a tool during text entry, shortcut capture or while the game is in the background.
A valid combination is not necessarily available: F9 is an example default, not a key reserved for your mod. Checks account for the player’s current assignments. Some native actions also reserve Ctrl, Alt or Shift variants, so adding a modifier does not guarantee an unused combination. The Already used by message identifies the conflicting action.
To change an assignment, the player clicks the current shortcut or Assign a shortcut, then presses the desired combination. Escape cancels capture; Backspace or Clear shortcut disables the assignment. Those two keys control the capture dialogue and are not assigned there as ordinary shortcuts. Closing the panel or leaving Shortcuts cancels capture. Holding a key does not repeatedly open the window.
Preserve player choices
Preferences belong to the user and mod and are shared across that user’s games. The SDK handles saving and loading. The mod identity is GameMod.id, hence modInfo.id from the id field in mod.json when using toolMod(modInfo) or signalMod(modInfo). The manifest’s modId field is a separate distribution identity. Keep id, option IDs and choice IDs stable; translating a label does not change identity. defaultValue applies when no compatible value is saved and when resetting an option.
Changing defaultValue or shortcut in a new version does not replace a saved choice that remains valid. If a value falls outside new numeric bounds or its choice ID no longer exists, the SDK uses that option’s default. Restore this mod’s defaults covers all its preferences and shortcuts, including those in the other section. If a default shortcut conflicts, the reset is rejected without partially applying the other values.
Before distributing the mod, check initial values, persistence after restart, numeric bounds, language changes, a disabled shortcut and an intentional conflict. Also verify that changing game sessions does not replay an old opening request.