Premiers pas en JavaScript

Qu’est-ce qu’un script JS ?

Un script est un fragment de code JavaScript distribué avec un pack de ressources : le jeu l’analyse au chargement des ressources, puis l’appelle frame par frame pendant l’exécution, afin d’afficher des modèles, du texte, des textures ou de jouer des sons au-delà des mécanismes du jeu vanilla.

  • Le moteur est Rhino — un moteur JavaScript implémenté en pur Java. Vous n’avez besoin de maîtriser que le langage lui-même (variables, fonctions, logique conditionnelle) et les objets intégrés tels que Date ou Map ; l’environnement navigateur (HTML/CSS/DOM) ne sert à rien — ne prenez pas pour argent comptant les complétions « web » de votre IDE.
  • Rhino fournit l’interopérabilité LiveConnect : les scripts peuvent appeler directement des classes et des méthodes Java. Nombre d’API intégrées de JCM sont elles-mêmes des classes Java exposées aux scripts.
  • JavaScript et Java sont deux langages distincts ; savoir programmer en Java n’est pas un prérequis pour écrire des scripts, mais comprendre le système de types de Java aide à éviter les pièges de l’interopérabilité (voir les règles de rédaction).
Relation avec les scripts natifs C++

La branche Yanyang ajoute les scripts natifs C++ en parallèle de JS : les deux partagent le même cycle de vie et le même pipeline de rendu, et peuvent même être mélangés au sein d’une même déclaration (voir Extensions Yanyang). Pour une logique ponctuelle et simple, JS offre l’itération la plus rapide ; le redessin pixel par pixel en haute résolution et les calculs complexes relèvent de C++.

Les quatre types de scripts

Chaque type de script cible un scénario dédié ; les objets globaux et les callbacks disponibles diffèrent légèrement :

TypeObjet contrôléUsages typiques
Scripts de véhiculeVéhicules MTR (rame entière)Écrans de bord, affichage de destination, annonces sonores, sons de moteur
Scripts d’eyecandy (décoration)Blocs de décoration MTREnseignes dynamiques, voyants de portique, installations interactives (événement de clic droit)
Scripts d’ascenseurAscenseurs MTR (habillage de cabine)Afficheurs d’étage, carillon d’arrivée
Scripts PIDSÉcrans d’arrivées JCM (préréglages)Mise en page personnalisée des arrivées, textures dynamiques

Un même code de script n’est analysé qu’une seule fois ; chaque instance concrète en jeu (chaque rame, chaque écran) détient son propre state indépendant et reçoit ses callbacks séparément.

Enregistrer un script

Les scripts sont chargés avec le pack de ressources. Assurez-vous d’abord que le jeu reconnaît le pack — placez un pack.mcmeta à la racine :

{
    "pack_format": 13,
    "description": "你的资源包描述"
}

pack_format ne contraint pas les scripts JCM (inutile de l’ajuster à chaque version de MC), mais ce doit être un nombre JSON valide.

Entrées de déclaration

Les scripts de véhicule / décoration / ascenseur se déclarent dans mtr_custom_resources.json (sous assets/mtr/) ; les scripts PIDS se déclarent dans le tableau pids_images de joban_custom_resources.json (sous assets/jsblock/). Le format MTR 4 sépare « contenu » et « scripts » en deux tableaux : l’entrée de contenu référence l’entrée de script via scriptId — un même script peut être réutilisé par plusieurs véhicules :

{
    "vehicles": [
        { "id": "demo_sp1900", "name": "Demo SP1900", "scriptId": "demo_lcd" }
    ],
    "vehicleScripts": [
        {
            "id": "demo_lcd",
            "scriptLocations": ["demo:js/sp1900_lcd.js"],
            "prependExpressions": ["print('Hello world')"],
            "input": { "era": 2004 },
            "rateLimit": 0.75,
            "dataFetchMode": "ALL"
        }
    ]
}
ChampSignification
scriptLocationsTableau des emplacements des fichiers JS (plusieurs possibles). Depuis JCM v2.3, la plupart des API acceptent aussi directement un id sous forme de chaîne. Un élément sans extension peut, selon les règles Yanyang, être reconnu comme module C++ — voir Entrées mixtes
prependExpressionsCode JS intégré directement, exécuté avant les scriptLocations
inputObjet JSON arbitraire, accessible en lecture seule depuis le script via SCRIPT_INPUT
rateLimitCadence plancher des appels du script (secondes). Monter cette valeur épargne des performances à la logique basse fréquence
dataFetchModeScripts de véhicule uniquement : SKIP (défaut, aucune donnée récupérée) / ALL / MANDATORY — voir Modes de récupération des données du véhicule
forceLoadLorsqu’aucun contenu du même pack ne référence le script, JCM refuse le chargement par défaut ; true force le chargement (pour partager une entrée de script entre packs)
isScriptControlledChamp des entrées de contenu décoration / ascenseur : masque le modèle d’origine et confie tout le rendu au script

Noms des tableaux des trois types : vehicleScripts (véhicules, contenu dans vehicles), objectScripts (décorations, contenu dans objects), liftScripts (ascenseurs, contenu dans lifts). Les PIDS n’ont pas d’étape scriptId — écrivez simplement scriptFiles / scriptTexts sur l’objet de préréglage dans pids_images pour qu’il soit traité comme un préréglage scripté (noms de clés hérités des conventions JCM, voir Scripts PIDS).

Astuce

Tous les champs sont facultatifs, mais une entrée de script n’est analysée que si elle comporte au moins scriptLocations ou prependExpressions. Yanyang accepte en outre l’écriture script_input des exemples de la documentation NTE (équivalente à scriptInput) — les packs recopiés sur les anciens documents continuent de fonctionner.

Ancien format MTR 3 / NTE

Les déclarations de l’ère MTR 3 restent valables : véhicules dans custom_trains (noms de champs script_files / script_texts / script_input / script_rate_limit), décorations dans eyecandy.json. Les véhicules à l’ancien format récupèrent les données en mode MANDATORY par défaut.

Cycle de vie

Phase d’analyse

Au rechargement des ressources (démarrage ou appui sur F3+T), le code de premier niveau de chaque script est exécuté une fois sur le thread principal ; JCM y relève ensuite les fonctions create / render / dispose pour les appeler en exécution. Les travaux à effectuer une fois — chargement de modèles, lecture de textures — se placent idéalement ici ; mais toute opération lourde ralentit d’autant l’intégralité du rechargement : économisez autant que possible.

Phase d’exécution

let displaySpeed = 1;            // exécuté pendant la phase d'analyse

function create(ctx, state, vehicle) {
    state.speed = 0.75;           // exécuté une fois par instance, avant le premier rendu
}

function render(ctx, state, vehicle) {
    state.speed += Timing.delta();  // exécuté à chaque frame (au mieux)
}

function dispose(ctx, state, vehicle) { }   // exécuté au retrait de l'instance

print(displaySpeed);             // exécuté pendant la phase d'analyse, affiche 1
ParamètreSignification
ctxle contexte : le point d’entrée des « actions » — soumettre des dessins, jouer des sons — propre à chaque type (VehicleScriptContext pour les véhicules, etc.)
stateun simple objet JS, persistant par instance — deux rames en détiennent chacune un, sans interférence ; {} au départ
wrapperla source d’informations en lecture seule : vitesse, ouverture des portes, dimensions d’écran, liste des arrivées… — propre à chaque type
  • Exécution asynchrone : les callbacks d’exécution ont lieu sur 4 threads d’arrière-plan dédiés (la phase d’analyse reste sur le thread principal). N’immobilisez pas le thread avec Thread.sleep ou une boucle infinie — une même entrée de script reste fixée au même thread, et d’autres scripts y font peut-être la file.
  • Frame par frame, sans garantie absolue : render vise un appel par frame, mais n’est pas appelé à nouveau tant que la frame précédente n’est pas terminée ; quand la cadence vacille, ne supposez pas « N fois fixes par seconde ». Tout ce qui doit progresser au temps réel se met à l’échelle avec Timing.delta().
  • Erreurs d’exécution : le journal rapporte l’erreur, préfixée par [Scripting] Error executing script!, avec fichier et ligne ; puis le script entier s’interrompt 4 secondes avant de réessayer. En jeu, la surcouche de débogage affiche l’erreur directement à l’écran.
  • Aucune continuité de thread JS : les variables locales d’un callback sont invisibles des autres callbacks — placez dans state les données à partager entre appels.
Correspondance avec le cycle de vie C++

create / render / dispose + état par instance : c’est exactement le modèle que reprennent les scripts natifs C++ (voir Modèle de script et cycle de vie), la migration de l’un à l’autre se traduisant segment par segment.

Aides au développement

  • Surcouche de débogage des scripts : Mod Menu (Fabric) ou l’écran de configuration des mods (Forge) → JCM → Configure → cochez Script debug mode. Le coin supérieur gauche de l’écran liste chaque entrée de script et ses instances ; les instances sont teintées selon leur durée — bleu (normal), jaune (lent), rouge (très lent). Au-delà de 5 secondes, une exécution est marquée comme ne répondant plus, puis interrompue 5 secondes plus tard. Appuyez sur [ / ] pour basculer entre les deux sources [MTR] et [JCM].
  • Rechargement rapide : en mode debug, Ctrl+R recharge les scripts côté MTR (véhicules / décorations / ascenseurs), Shift+R ceux côté JCM (préréglages PIDS) — sans passer par un F3+T complet. Les trois touches sont en réalité liées aux touches « sprint », « s’accroupir » et « rechargement JCM », reconfigurables dans les options de touches.
  • Journaux : print(...) et console.log(...) écrivent dans la console du jeu ; les erreurs de la phase d’analyse ne laissent qu’une ligne dans le chat — le détail se lit dans le logs/latest.log du lanceur.
  • Complétion IDE : VSCode dispose d’une extension tierce, MTR Scripting Tools (trouvable sur Visual Studio Marketplace / Open VSX), qui fournit la complétion pour les types de scripts JCM ; les problèmes se signalent à son auteur.

Règles de rédaction

Au niveau du langage

  • Mode strict : affecter sans déclarer (x = 1) provoque immédiatement une erreur. En global, utilisez var ou let ; dans une fonction, systématiquement let.
  • const a une portée de fonction : le const de Rhino n’a pas la portée de bloc du JS moderne — le déclarer dans le corps d’un for lève une « redéclaration » à la deuxième itération. Pour une sémantique de portée de bloc, utilisez let.
  • Chaînes JS contre chaînes Java : MTR/JCM retourne le plus souvent des String Java. Prenez la longueur via str.length() ; pour la propriété JS .length, convertissez d’abord en concaténant une chaîne vide : (""+str).length. Les List Java s’utilisent avec list[0] et for (x of list), mais les méthodes de tableau JS (map, etc.) ne s’y appliquent pas.
  • Valeurs annulables : une méthode dont le type de retour porte un ? peut retourner null. Traitez-les avec if (x != null), l’opérateur ternaire, le chaînage optionnel x?.getName() et la coalescence des nuls x?.length() ?? 0 ; lire une propriété ou appeler une méthode sur null lève immédiatement une exception.
  • Résolution de surcharge : la surcharge Java est choisie d’après les types des arguments, les nombres penchant par défaut vers double ; en cas de doute, appelez via le suffixe de signature : graphics["drawString(java.lang.String,int,int)"]("文字", 15, 60), new Color["(int,int,int)"](40, 68, 130). Face à un « method not found », soupçonnez d’abord les types des arguments.
  • Étendue des fonctionnalités : Rhino ne couvre pas toutes les dernières fonctionnalités JS — voir le tableau de compatibilité de Mozilla.

Au niveau du comportement

  • Pas de blocage, pas de boucle infinie : il n’existe pas d’écriture « attendre un peu puis continuer » — cadencez l’action et effectuez-la dans les callbacks suivants ; les opérations longues (réseau, gros fichiers) vont au BackgroundWorker. Une boucle infinie est interrompue de force au bout d’environ 10 secondes et traitée comme une erreur d’exécution.
  • Limite de l’API interne : les méthodes / champs absents de cette documentation, la structure interne de MTRClientData et les objets profonds au-delà de VehicleWrapper.getMtrVehicle() relèvent de l’implémentation interne, susceptible de changer d’une version à l’autre ; un pack de ressources distribué au public ne doit pas s’y fier.
  • Liste blanche d’accès aux classes : par défaut, seuls ces paquets sont admis — com.lx862.jcm.mod.scripting, com.lx862.mtrscripting.util, com.lx862.mtrscripting.wrapper, java.lang, java.awt, java.text, java.math, java.util, java.time, javax.imageio, sun.java2d, sun.font, sun.awt, org.mtr. Le joueur peut relâcher la restriction dans les réglages ; si votre pack distribué l’exige, signalez-le bien en évidence dans sa description.
  • Préfixez les variables de state : plusieurs scripts peuvent être accrochés à une même instance, et state est partagé — préfixez (state.myfx_ratelimit, etc.) pour éviter les collisions de noms.
  • Le budget de performance est limité : 16 ms par frame semblent suffire, mais le joueur peut faire tourner des dizaines d’instances de scripts simultanément. Avant de redessiner de grandes textures à chaque frame, faites le compte total : une rame de 8 voitures comptant 10 écrans chacune, avec un upload() intégral par frame, représente 80 transferts de textures — réduire la résolution, mutualiser entre portes, abaisser la cadence de mise à jour (RateLimit) sont les bonnes voies. Un script client peut se contenter d’être « assez bien » — ne visez pas la réplique 1:1.
  • Un script n’est pas une frontière de sécurité : les scripts s’exécutent purement côté client, et chacun peut les réécrire localement — aucun contrôle de permission (tel hasPermissionLevel) n’arrête qui est décidé à tricher ; n’en faites jamais votre contrôle d’accès.

Relation avec NTE / MTR 3

Le système de scripts de JCM est inspiré de Nemo Transit Expansion (NTE) — une continuation non officielle des fonctionnalités de scripts de NTE sur MTR 4, qui conserve quantité d’API de compatibilité homonymes ou équivalentes (les éléments marqués « Obsolète » dans cette documentation). Points de différence :

  • L’enregistrement à l’ancien format fonctionne comme toujours ; un script amphibie MTR 3 / MTR 4 peut bifurquer sur la version via Resources.getAddonVersion("mtr") (Resources.getMTRVersion() dans les anciens scripts).
  • Les opérations Vector3f qui retournaient void sous NTE retournent désormais l’objet lui-même, permettant les appels chaînés ; Resources.manager() retourne null sur MTR 4, et les fonctions concernées acceptent null.
  • Les classes de compatibilité façon ANTE (SoundHelper / TickableSound / GlobalRegister / ComponentUtil / le global asJavaArray()) sont disponibles depuis v2.3, avec des signatures de méthodes identiques à ANTE.
  • MTR 4 ne synchronise au client que les stations / lignes proches — si un script ne trouve pas de données lointaines, ce n’est pas un bug mais une économie de bande passante délibérée ; pour les trois modes de récupération des données des scripts de véhicule, voir Modes de récupération des données du véhicule.

Ensuite : API communes JS — les classes et fonctions partagées par tous les scripts ; Types de scripts JS — le ctx et le wrapper propres à chacun des quatre types.