NRF Guide

Créer un mod

Signaux et réseau

Plusieurs modèles dans un mod, des règles locales et des décisions liées au signal suivant.

Garder la même composition pour chaque modèle

Le point d’entrée assemble les modèles avec signalMod. Chaque modèle a son dossier et les mêmes rôles. Commencez avec peu de fichiers, puis séparez les rôles ci-dessous quand le modèle grandit ; les fonctions Kotlin se branchent directement dans signalModel.

RôleContenu du modèle
DéclarationIdentité, catalogue, replis et raccordement des fonctions au SDK.
États et motifsDeux enums locales et éventuellement un alias Indication pour la décision.
Panneau et réglagesCases visibles, libellés et valeurs utilisées par les règles.
RèglesChoix de la décision à partir du contexte SDK et du voisin.
TexturesChoix du SVG et phase de clignotement au temps simulé.
Conduite et vitessesConsigne générique et valeurs choisies par votre modèle.
DiagnosticsDécisions considérées comme des anomalies.

Les intégrations facultatives communes à plusieurs modèles appartiennent au niveau du mod. Ne recopiez pas le contexte SDK, le parcours du réseau ou un moteur de freinage dans chaque dossier. Le SDK prépare ces informations et exécute les consignes ; les règles restent chez vous.

Un modèle, ses propres enums

Deux modèles avec leurs propres types
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)
}

Chaque modèle possède ses enums d’indication et de motif, ses replis, ses règles, ses images et ses consignes. Le mod ne fait que les assembler. Ses cases sont locales : deux modèles peuvent avoir une case active avec des défauts différents.

Cet exemple illustre le dialogue entre deux modèles fictifs. Il ne fournit aucune consigne de conduite : ajoutez driving selon vos règles et déclarez les quatre SVG dans votre catalogue de ressources.

ÉlémentRôleExemple fictif
ModPaquet installé, identité et liste des modèles.mon-reseau
Type de signalRègles, cases et catalogue propres à un modèle constructible.mon-reseau.principal
Signal poséUne instance de ce type sur une voie, avec son ID et ses réglages.Un signal choisi dans une partie

Créer un nouveau type ne nécessite pas de créer un autre mod. Choisissez un ID de mod distinct de ceux des types. Un fichier par modèle permet de faire évoluer ses règles sans ajouter des conditions partout dans le point d’entrée.

Comprendre next

Le SDK tente d’abord une décision locale avec next = null. Si votre règle renvoie null, il résout le signal suivant et rappelle la règle avec sa décision. Un lien absent ou un cycle sans décision locale utilise invalidNetwork. Une fermeture locale peut donc interrompre la dépendance au voisin.

Dans la règle du modèle amont
val voisin = next?.of(mainSignal)
// Indication<MainAspect, MainReason>?
// null : pas ce modèle (ou pas encore de voisin).
val idDuVoisin = next?.id
val typeDuVoisin = next?.type?.id
val consigneDuVoisin = next?.drivingRule

Le SDK fournit directement next à tous les modèles : ID, type, décision et consigne déclarée par le voisin. Aucun adaptateur ni fichier Neighbours à écrire. Vous pouvez lire sa consigne générique ou utiliser of(mainSignal) pour ses enums. Le modèle amont reste responsable de son interprétation et du repli pour les informations inconnues.

Le contexte fournit aussi settings, observation, fresh et settingsStatus. Un profil absent reçoit les défauts déclarés ; un profil indisponible rend l’observation non fraîche. Le statut est conservé pour vos règles. next concerne le lien aval résolu dans le réseau du mod, pas tous les signaux géographiquement proches ni les décisions privées des autres mods.

Une règle qui dépend du voisin doit aussi décider comment traiter ses propres observations inconnues. Le SDK ne transforme pas automatiquement un canton inconnu en canton libre.

Observer un train en approche

kotlin
signalModel("monmod.approche", "Signal à l’approche", "textures_approche",
    fallback = Indication(Aspect.Closed, Reason.Unknown)) {
    observeApproach = true
    rules {
        if (fresh && routeKnown && block == Occupancy.Clear && approachingTrain != null
            && !observation.forcedStop && !observation.lampFailed)
            Indication(Aspect.Open, Reason.Clear)
        else Indication(Aspect.Closed, Reason.Unknown)
    }
    images { if (it.aspect == Aspect.Open) "open.svg" else "closed.svg" }
}

approachingTrain contient un identifiant de train lorsque l’observation trouve son premier signal orienté dans le sens de marche. C’est une observation géométrique en amont ; elle ne prouve ni réservation ni autorisation de mouvement.