NRF SDK 0.9

Créer un mod

Signaux et réseau

Composez plusieurs modèles, résolvez leurs voisins et traitez les observations inconnues.

Organiser les rôles d’un modèle

Après le premier mod, vous pouvez ajouter plusieurs familles de signaux au même paquet. Chaque signalModel possède son identité, ses enums et ses callbacks. Le point d’entrée signalMod assemble ces déclarations ; il ne doit pas devenir une grande règle qui compare tous les types.

RôleCe que vous écrivez
DéclarationIdentifiant stable, catalogue, replis et raccordement des fonctions.
IndicationsDeux enums : l’aspect à afficher et le motif qui explique la décision.
RéglagesCases et entiers nommés, valeurs initiales et aide du panneau.
RèglesUne décision à partir de l’observation et, si nécessaire, du voisin aval.
ApparenceImages du catalogue et éventuelle animation.
ConduiteConsigne et vitesses choisies explicitement pour chaque indication.
DiagnosticNoms lisibles des motifs et décisions classées comme défauts.

Gardez les petits modèles dans un fichier. Quand ils grandissent, regroupez leurs fichiers dans un dossier par famille ; placez les règles réellement partagées dans un dossier commun nommé selon leur domaine. Les fonctions de règles travaillent sur les valeurs fournies, sans ouvrir de connexion au jeu ni lancer de minuteur.

Deux modèles avec des vocabulaires indépendants

SignalModels.kt
package wiki.models

import nimby.*

// Deux vocabulaires indépendants, même si leurs ordinaux commencent tous à zéro.
enum class MainAspect { Closed, Open }
enum class MainReason { Unknown, Clear }
enum class DistantAspect { Wait, Proceed }
enum class DistantReason { Unknown, MainClosed, MainOpen }

val mainSignal = signalModel(
    "mon-reseau.principal", "Principal", "textures_principal",
    fallback = Indication(MainAspect.Closed, MainReason.Unknown)
) {
    rules {
        if (fresh && routeKnown && block == Occupancy.Clear &&
            settingsStatus != SettingsStatus.Unavailable &&
            !observation.forcedStop && !observation.lampFailed)
            Indication(MainAspect.Open, MainReason.Clear)
        else Indication(MainAspect.Closed, MainReason.Unknown)
    }
    images { if (it.aspect == MainAspect.Open) "main-open.svg" else "main-closed.svg" }
}

val distantSignal = signalModel(
    "mon-reseau.annonce", "Annonce", "textures_annonce",
    fallback = Indication(DistantAspect.Wait, DistantReason.Unknown)
) {
    rules {
        when {
            !fresh || !routeKnown || block != Occupancy.Clear ||
                settingsStatus == SettingsStatus.Unavailable ||
                observation.forcedStop || observation.lampFailed ->
                Indication(DistantAspect.Wait, DistantReason.Unknown)
            next == null -> null // Demander au SDK de calculer le voisin.
            else -> when (next?.of(mainSignal)?.aspect) {
                MainAspect.Closed -> Indication(DistantAspect.Wait, DistantReason.MainClosed)
                MainAspect.Open -> Indication(DistantAspect.Proceed, DistantReason.MainOpen)
                null -> Indication(DistantAspect.Wait, DistantReason.Unknown)
            }
        }
    }
    images { if (it.aspect == DistantAspect.Proceed) "distant-proceed.svg" else "distant-wait.svg" }
}

// Exemple de composition et de lecture : aucune consigne de conduite déclarée.
fun createNetworkMod() = signalMod("mon-reseau", "Mon réseau") {
    signal(mainSignal)
    signal(distantSignal)
}

Le modèle principal décide localement. Le modèle d’annonce vérifie d’abord son propre canton, puis interprète le principal avec next.of(mainSignal). Ces noms et ces règles sont fictifs : aucune convention ferroviaire nationale n’est fournie par le SDK.

IdentitéPortée
ModInfo.idLe paquet : utilisé pour l’installation et les services optionnels.
SignalType.idLe modèle constructible : ses règles, ses réglages et son catalogue.
Signal.idUne instance posée dans la partie observée ; ne pas réutiliser dans une autre partie.

Un mod déclare de 1 à 16 modèles, avec identifiants et catalogues distincts. Réutilisez la même instance de SignalModel dans signal(model) et next.of(model) : un nom ou un ordinal identique ne rend pas deux déclarations interchangeables.

Résoudre le voisin seulement quand il est utile

  • Le premier appel à rules reçoit next == null. Une indication renvoyée conclut immédiatement pour ce signal.
  • Renvoyer null demande au SDK de résoudre le lien nextSignal. La règle est ensuite rappelée avec le voisin résolu.
  • Si le lien manque, si la dépendance boucle sans décision locale ou si la règle ne conclut toujours pas, le modèle utilise invalidNetwork.

Distinguez next == null de next.of(mainSignal) == null. Dans le premier cas, la résolution n’a pas encore fourni de voisin. Dans le second, un voisin peut être résolu mais appartenir à un autre modèle : choisissez alors une politique explicite. L’exemple ci-dessus renvoie son repli pour ce modèle inconnu.

Propriété du voisinUtilisation
id / typeIdentifier le signal aval et son modèle dans cette observation.
of(model)Lire les enums exactes du modèle reconnu, sans conversion numérique.
drivingRuleConsulter la consigne déclarée, si votre règle sait l’interpréter ; null reste une absence de consigne.
activeLire activeWhen du voisin ; cela ne prouve ni fraîcheur ni voie libre.

next suit le réseau observé de votre mod. Il ne recherche pas tous les signaux proches et ne lit pas automatiquement les décisions privées d’autres mods. evaluateNetwork permet de tester ce même mécanisme hors jeu ; sa limite de 4096 signaux par appel n’est pas une promesse de taille maximale de carte.

Faire de l’inconnu un résultat explicite

EntréeInterprétation à conserver
fresh == falseLes valeurs ne prouvent pas l’état actuel. Choisir le repli du modèle.
routeKnown == falseLe parcours nécessaire n’est pas établi ; ne pas en déduire un canton libre.
Occupancy.UnknownNi Clear ni Occupied ne sont prouvés. Ne pas remplacer par Clear.
SettingsStatus.UnavailableLe profil ne peut pas être lu ; SignalRuleContext rend l’observation non fraîche.
forcedStop / lampFailedDonnées à interpréter dans votre règle ; leur nom ne crée pas une décision à votre place.

fallback sert notamment au calcul isolé qui ne conclut pas ; invalidNetwork couvre une dépendance aval impossible à résoudre. Ces déclarations ne remplacent pas les branches de rules sur fresh, routeKnown et Occupancy.Unknown. Choisissez un motif distinct quand il aide à expliquer le résultat, puis testez les entrées inconnues avant les cas favorables.

Observer une approche à plusieurs cantons

ApproachSignal.kt
package wiki.approach

import nimby.*

enum class ApproachAspect { Closed, Open }
enum class ApproachReason { Unknown, TrainApproaching }

val approachSignal = signalModel(
    "monmod.approche", "Signal à l’approche", "textures_approche",
    fallback = Indication(ApproachAspect.Closed, ApproachReason.Unknown)
) {
    construction(states = listOf("closed.svg", "open.svg"))
    observeApproach(blocks = 2)
    rules {
        if (fresh && routeKnown && block == Occupancy.Clear && trainApproaching &&
            !observation.forcedStop && !observation.lampFailed)
            Indication(ApproachAspect.Open, ApproachReason.TrainApproaching)
        else Indication(ApproachAspect.Closed, ApproachReason.Unknown)
    }
    images { if (it.aspect == ApproachAspect.Open) "open.svg" else "closed.svg" }
    // Le mod choisit ici sa politique de conduite.
    driving { if (it.aspect == ApproachAspect.Open) AutomaticDriving.clear() else AutomaticDriving.stop() }
}

Dans un projet préparé avec le premier tutoriel, ajoutez ce fichier et assemblez approachSignal depuis createMod. Le catalogue déclare closed.svg et open.svg : ajoutez ces fichiers dans assets. Le résultat attendu est un modèle qui s’ouvre uniquement lorsque ses conditions locales et une approche fraîche sont réunies.

src/main/kotlin/Entry.kt
package nimby.mod

import nimby.*
import wiki.approach.approachSignal

fun createMod() = signalMod(modInfo) {
    metadata(author = "Your name", description = "Approach-controlled signal.")
    signal(approachSignal)
}

observeApproach(blocks = 2) demande une tête de train orientée vers le signal dans les deux cantons en amont. La portée accepte 1 à 16 cantons. trainApproaching vaut true seulement avec une approche exploitable et fraîche ; approachingTrain fournit alors son identifiant. Une absence de preuve donne false/null, pas une preuve qu’aucun train n’existe.

Le parcours respecte le sens observé et ne choisit pas arbitrairement une branche. Après le passage de la tête au signal, ce train n’est plus une approche de ce signal. La détection ne prouve ni réservation ni autorisation de mouvement ; elle se combine aux contrôles du canton dans votre règle.