Creating a mod
Where do text and controls appear?
Checkboxes, titles, buttons, fields and messages: location, lifetime and events.
Selected signal panel
Prerequisite: a model declared with signalModel. Its settings appear below the selected signal’s properties in a scrolling group. Tool commands are separate from persistent settings. Long labels can scroll horizontally and help text wraps. Checkbox order follows their Kotlin declaration.
val automatic = checkbox(
name = "automatic",
label = tr("automatic.label"),
description = tr("automatic.help"),
defaultValue = true
)
rules {
if (!enabled(automatic)) Indication(Aspect.Closed, Reason.Disabled)
else /* your decision based on observations */ null
}| Field | Location and effect |
|---|---|
| SignalType.title | Heading above this model’s checkboxes. Accepts tr. It is not automatically the construction-menu name, which comes from construction.name. |
| Checkbox.name | Technical key saved for each placed signal. Invisible to the player. Do not rename it according to the language. |
| Checkbox.label | Text next to the checkbox. Accepts tr. |
| Checkbox.description | Help text below the checkbox, with word wrapping. Accepts tr. An empty string creates no row. It is directly visible, without hovering. |
| Checkbox.defaultValue | Value used before any setting has been saved for this signal. It does not replace an existing saved value. |
| Checkbox.onlyWhenEnabled | If true, the checkbox is shown only while its value is true. Useful for acknowledgement; once unchecked, it disappears. Leave false for a normal setting. |
| enabled(option) / settingsStatus | Your rules read the checkbox; the SDK chooses no consequence. A known signal without saved values receives its defaults and can be Present. Absent means missing from the observed catalogue; Unavailable means the read cannot support a decision and makes the observation stale. |
Persistent value or form input?
| Need | API and lifetime |
|---|---|
| NumberSetting | An integer option saved for each signal, such as a work-zone range. Declare number(option) in the model and use option.read(settings) in the rule. The signal panel handles input and persistence. |
| ToolNumberInput | A temporary tool-form field, such as spacing before placement. The event carries the value; your tool owns its draft. This field creates no persistent signal setting. |
val range = NumberSetting("workBlocks", "Following blocks",
maximum = 64, defaultValue = 0, visibleWhen = "work")
// Declare number(range) inside your signalModel.
// Read range.read(settings) inside its rules.
val proposedSettings = range.withValue(emptyMap(), 2)NumberSetting accepts values from zero to maximum; maximum ranges from 1 to 65535 and the default must fit this range. visibleWhen names a checkbox in the same model: the field appears immediately below it when checked. Hiding it does not delete its value. An empty key displays the field unconditionally. withValue returns a new map; it does not write to the game.
Actions and tool panels
| Declaration | Visible behaviour and event |
|---|---|
| SignalAction(id, label, whenMod, service) | Button below the signal settings, visible when the provider mod and service are available in the same game session. label is displayed; id, whenMod and service remain technical. No mandatory mod dependency. |
| showPanel(request, message, buttons, inputs) | Replaces the original action button with a temporary panel. message appears above the fields and buttons, with wrapping. Each publication replaces these controls without changing persistent checkboxes. |
| ToolButton(id, label, enabled) | label is the button text. enabled=false prevents activation. A click reaches the same service with request.action=id and request.value=null. Up to 12 buttons. |
| ToolNumberInput(id, label, value, minimum, maximum, enabled) | label appears above the integer field. value is the proposed value; minimum and maximum bound accepted values. enabled=false prevents editing. A valid edit arrives with action=id and value=the new integer. Up to 4 fields. |
| Incomplete input | An empty, invalid or out-of-range field retains its draft and blocks commands. The SDK does not invent zero. After a valid edit, the mod republishes the panel and invalidates its old preview. |
| request.signalId / worldId / generation | Action source and game-session identity. Do not reuse a ticket or preview after a generation change. panelToken and originAction connect clicks to the original panel: pass along the received request. |
Declared text is not an automatic action: writing “Close” or “Confirm” does not close or build anything. The service must handle the received identifier. To close, clear the preview and republish the opening button; to build, use a ticket and track its result.
showPanel, showSignalPreview and clearSignalPreview can throw ToolOperationException with isBusy. Keep the presentation request for a later onTick, without a waiting loop. A refused publication confirms neither the new panel nor the preview: locally disable Apply and Undo until the presentation is current. A received click must still pass your session, source, plan and pending-operation checks.
Rebuild a complete mod
For signalling comparable to AB Signalisation lumineuse: combine multiple signalModel declarations, each with its enums, rules, construction, images and driving; add model-specific checkboxes, observe approaching trains where needed, and read next with its type. Colours and speeds remain mod choices. The wiki’s AB Signalisation lumineuse excerpts illustrate focused uses without requiring the project download.
For a tool comparable to BA Signal Placement: declare toolMod and an optional service; read network, calculate positions while respecting connections and junctions, display all positions with showSignalPreview, then prepare a ticket and read the network again before confirmation. Track Pending with pollConstruction, use undoConstruction only when canUndo, and reset your state on session changes or onStop. The tool owns its domain calculations.