Sur cette page

← Toute la documentation

Concepts : comment fonctionne un mod

Cycle de vie, threads et budgets, événements, permissions, erreurs, déterminisme et interdits.

Chaque affirmation renvoie au code (crates/modding/src/…) ou à Mods TypeScript. Ce qui n'a pas pu être vérifié est marqué [À VÉRIFIER].

Trois genres de scripts, une seule API

Genre Fichier Déclaration Rôle
Mod mods/<dossier>/main.ts (ou index.ts) defineMod HUD, jeux de jugement, vitesses de défilement, notes de difficulté, filtres, touches, modificateurs
Skin skins/<dossier>/skin.ts defineSkin Place le playfield natif et configure les éléments de HUD que les mods fournissent
Script de map script.ts ou <chart>.script.ts dans le dossier de la map defineChart Anime le playfield de cette map (visuel seulement)

Un mod ne peut pas appeler playfield.* ni lanes.set (réservés aux skins et scripts de map) ; un script de map ne peut pas utiliser hud.* ni storage.* (api.rs : playfield_call, for_mods_and_skins). Tableau complet : Disponibilité par genre de script.

Cycle de vie

  1. Chargement : le premier niveau du fichier s'exécute. Seuls defineMod (ou defineSkin/defineChart), log.* et ctx.on fonctionnent ; tout autre appel hôte lève … is not available while the mod loads: call it from `setup` or an event handler. Le chargement sert aussi à l'installateur pour lire la déclaration sans exécuter setup.
  2. Déclaration : defineMod doit être appelé exactement une fois, au premier niveau. Un second appel, une déclaration invalide ou un id déjà pris font échouer le mod, même si l'exception est rattrapée.
  3. setup(mod) : appelée une fois, après le setup des paquets listés dans uses. Seul endroit où les *.register sont permis : ils se ferment après le chargement de tous les mods (… is only available in a mod's setup).
  4. Événements : le mod reçoit game.*, library.*, controls.action, elements.configure, etc., et peut appeler game.*, hud.*, storage.*…

Ordre des mods : les fournisseurs avant leurs dépendants (uses), sinon loadOrder (0 à 10000, défaut 1000) puis id. Cet ordre décide aussi de l'empilement des calques HUD et de la livraison des événements.

Les mods se chargent en arrière-plan : le jeu n'attend pas. Un mod désactivé par le joueur est déclaré (ses métadonnées sont lues) mais son setup ne tourne pas. Un skin et un script de map sont rechargés à neuf à chaque lancement de map : rien ne persiste d'une partie à l'autre, sauf storage pour les mods.

Threads et budgets

  • Tout le code des mods, du skin et des scripts de map tourne sur un seul thread mods, jamais sur les threads d'entrées, de rendu, audio ou de fenêtre. Les threads de jeu envoient des événements par une file bornée (1024) sans jamais attendre les mods ; si elle est pleine, l'événement est perdu. Conséquence : un événement peut manquer. Ne bâtissez pas un état critique sur la réception de chaque game.judgement ; relisez game.player() ou les totaux (counts, combo, accuracy portés par chaque événement).
  • Chaque mod a son propre moteur Rust-TS (QuickJS) : 16 Mio de mémoire, 1 Mio de pile, 30 ms d'exécution par chargement, setup, événement ou tick (Budgets).
  • Un mod est désactivé après 3 échecs consécutifs (exception non rattrapée ou dépassement) ou 3 dépassements de budget au total, jusqu'au prochain démarrage de l'hôte.
  • Les valeurs qui changent à chaque image ne doivent pas passer par le script : utilisez les liaisons (bind, fill, showWhen, animate) que la surcouche résout toute seule, et les animations/déclencheurs de stage.* que le rendu évalue. Un script efficace ne s'exécute que quelques fois par seconde.

Événements

Événement Cadence
game.songStart / game.songEnd une fois par partie
game.judgement à chaque note jugée (best-effort)
game.tick 30 Hz pendant que le chart joue, sans pause ; saute un tick en retard
game.beat à l'heure exacte de chaque temps du chart ; un seul temps (le dernier) si en retard
game.pause / game.resume transitions
game.playfieldChange, game.settingsChange changements
library.ready, library.chartAdd, library.chartsAdded menu / bibliothèque
leaderboard.result, skinImport.opened, skinImport.staged, skinImport.created, skinImport.action réponses au mod demandeur
ratings.progress, metron.performanceResult, ui.action réponses au mod demandeur
controls.action touche d'une action déclarée (peut être perdue sous charge : pas pour un état de scoring)
elements.configure un autre paquet (ou le joueur) configure un élément du mod

Payloads : Événements.

Dépendances et éléments configurables

Un mod (ou un skin) déclare uses: { "autre.mod": "^1" } ; une dépendance est facultative par défaut (absente, ctx.has("autre.mod") vaut false et une note apparaît sur la page Mods). required: true la rend obligatoire. 16 dépendances au plus, pas de cycle (dependency cycle: a → b → a).

Un mod expose des widgets avec provides.elements (options typées : number, integer, boolean, string, color, enum, colors). Les autres paquets les placent avec ctx.element("<paquet>/<élément>").configure({…}) ; le fournisseur reçoit elements.configure. Ordre unique de priorité : défauts déclarés < configure() du skin < configuration du joueur dans l'éditeur de skin. Les scripts ne s'appellent jamais entre eux.

Permissions

Trois permissions existent (type Permission = "stage" | "skinImport" | "network"), à déclarer dans permissions: [...] :

Permission Donne accès à Retrait par le joueur
"stage" stage.* (rendu natif) par mod (réglage stageDenied) ; sans elle chaque appel stage.* lève une erreur
"network" downloads.register, downloads.registerBridge (miroirs et sources de téléchargement) par mod (réglage networkDenied, d'après Mods et téléchargements de la branche feature/mod-download-sources) ; les hôtes déclarés sont montrés au joueur
"skinImport" skinImport.* (convertir un skin osu!) pas de réglage de retrait trouvé dans settings.rs [À VÉRIFIER] ; sans la permission l'appel lève « skinImport needs the skinImport permission »

network ne donne aucun accès réseau au script : il autorise seulement à déclarer des hôtes ; l'hôte (crates/downloader) fait toutes les requêtes, en HTTPS, sur ces hôtes seulement, et garde le jeton du joueur. Il n'y a pas de permission fichier ou DOM : ces accès n'existent pas du tout.

Erreurs que vous verrez

Message (ou début) Cause
\`x\` requires modding API version 1; this game provides version 2 apiVersion différent de 2
another mod already uses the id `…`: the folder `…` loaded first and keeps it id dupliqué entre deux dossiers
defineMod must be called exactly once second appel ou appel après une erreur de déclaration
defineMod can only be called at the top level of the mod's entry file defineMod appelé dans setup ou un gestionnaire
… is not available while the mod loads appel hôte au premier niveau du fichier
… is only available in a mod's setup *.register appelé après le setup
\`playfield.set\` is only available to skins and map scripts playfield.* depuis un mod
… is not available to map scripts: they only change the playfield hud.*/storage.* depuis un script de map
add \`<paquet>\` to \`uses\` to configure its elements ctx.element(...) sur un paquet absent de uses
TypeError avec la raison valeur hors bornes, champ inconnu, couleur illisible, id trop long… (rattrapable)
\`version\` must be a semantic version such as 1.0.0 version invalide (1.0 refusé)
\`id\` must be … id hors a-z 0-9 . - _, majuscule, .., nom réservé Windows
requires Y ^1: package Y missing / features using Y disabled dépendance obligatoire / facultative manquante
a mod registers at most N … limite par mod dépassée (8 jeux, 8 vitesses, 16 tables, 16 vues, 32 filtres, 32 actions, 8 modificateurs)

Un mod en échec n'est pas chargé et l'erreur apparaît sur la page Mods ; les autres mods se chargent normalement. Le chargement transpile sans vérifier les types : une erreur de type n'apparaît qu'avec tsc (voir README).

Déterminisme et rejeu

  • Les jugements sont natifs : un script ne peut ni injecter d'entrées, ni modifier un jugement, ni un score (gameplay.register ne fait que déclarer un genre connu de l'hôte).
  • Les paliers d'un jeu de jugement sont résolus une fois par combinaison de paramètres puis enregistrés tels quels dans les replays, qui se rejugent sans le mod. tiers(params) doit donc être pure et synchrone. De même les conversions toMs des vitesses de défilement : le rendu ne reçoit que le temps résolu.
  • Un script de map est visuel : le replay n'enregistre que son empreinte BLAKE3.
  • Les Hit de game.hits sont l'historique natif réel (accords et relâchements séparés compris), pas une reconstruction par événements.
  • Fonctions non déterministes (Math.random, Date) : leur disponibilité dans le moteur n'est pas documentée dans le code lu [À VÉRIFIER] ; ne les utilisez pas pour un contenu qui doit rester identique d'une partie à l'autre. Il n'y a ni timers (setTimeout…), ni réseau (la permission network ne fait que déclarer des sources), ni accès fichier (Sécurité).

Ce qu'un mod ne peut JAMAIS faire

  • Accéder au DOM, au JavaScript de l'interface, aux commandes IPC, au réseau (le script ne fait jamais de requête, même avec network), aux fichiers hors de son dossier, aux timers.
  • Injecter des touches, modifier les notes, le jugement, le score ou un replay.
  • Dessiner du HTML ou du CSS brut, charger un shader personnalisé.
  • Lire le stockage d'un autre mod, ou appeler un autre mod directement.
  • Charger une bibliothèque native (DLL) : hors périmètre (AGENTS.md).
  • Lire un autre fichier que ses polices (.ttf/.otf) et images (.png) déclarés.
  • import() dynamique (refusé) ; les imports relatifs et node_modules du dossier du mod sont résolus, jamais hors du dossier.
  • Ajouter des comportements de modificateur : seuls les genres connus de l'hôte (auto, ghost, mirror, random, noLn, fullLn) existent.

Mods, skins, thèmes : ne pas confondre

  • Un thème (themes/<id>/) est de la donnée (manifeste + tokens.css), jamais du code : voir Thèmes d'interface.
  • Un skin est du code (skin.ts) qui configure le playfield et les éléments.
  • Un mod fournit les widgets, les jeux de jugement, etc.

Source dans le dépôt du jeu : docs/modding/concepts.md