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.

Natif ou mod : qui décide quoi

Règle de classement (décision du joueur, 2026-10-11) : si ça touche au temps réel, c'est natif et expliqué comme une capacité de l'hôte ; si c'est de la présentation, de la règle ou une transformation de données, c'est un mod.

  1. Natif : les entrées (Raw Input, horodatage QPC), le jugement, l'horloge de la chanson, l'audio, le rendu. Un script tourne sur un fil à part avec un budget (30 ms, 16 Mio) : le mettre dans ces chemins casserait les 2 500 FPS et la précision des entrées. Auto et Ghost sont donc natifs : ce sont des capacités de l'hôte (jouer à la place du joueur, rejouer un replay à côté), que le jeu présente sur la page Mods comme des mods pour l'interface, sans que leur script contienne de logique.
  2. Mod : l'affichage (HUD, calques de skin), les règles de présentation (jeux de jugement, vitesses de défilement, notes de difficulté), et les transformations de données (les notes d'un chart), avec les API typées nécessaires.
  3. Ne jamais appeler « mod » un simple interrupteur natif sans le dire : la documentation et l'interface disent quand un « mod » n'est qu'une déclaration dont l'hôte fait tout le travail.

Audit des mods livrés (content/mods/, 24 dossiers avec main.ts)

Famille Mods Ce qu'ils sont vraiment
HUD (9) accuracy, combo, counters, fps, hit-bar, judgement-display, pause-status, progress-bar, playback-controls De vrais scripts (24 à 138 lignes) qui construisent des nœuds HUD ; la valeur affichée est le plus souvent une liaison résolue par la surcouche. Mods.
Jugement (3) osu, etterna, prism Tables de paliers calculées par le script, enregistrées dans les replays. Mods.
Défilement (4) scroll-prism, scroll-osu, scroll-etterna, scroll-quaver Une formule toMs chacun. Mods.
Difficulté (1) metron Déclare les calculateurs natifs, panneaux, filtres, tables. Mod (le calcul est natif).
Skin (1) skin-converter Convertisseur osu! → skin Prism (199 lignes). Mod.
Modificateurs de jeu (6) auto, ghost, mirror, random, no-ln, full-ln Trois sur six ne font que déclarer (auto, ghost, full-ln) : leur comportement est en Rust. auto et ghost doivent le rester (entrées, jugement). full-ln reste natif : il a besoin des points de timing, des constantes de rate et du compteur d'aperçu de l'hôte. mirror, random et no-ln sont des mods de script (kind: "chartTransform") : ils lisent les notes avec chart.getNotes() et rendent le résultat avec chart.setNotes(), que Rust valide. Mods.

Les mods servaient donc surtout à l'affichage, aux règles de jugement et au défilement, presque pas à ce que fait le joueur. Les meilleurs exemples pour une communauté sont les fonctions du jeu elles-mêmes, écrites en mods lisibles.

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).
  • Les mods livrés partagent un moteur Manivelle (QuickJS, réserve de 16 Mio × leur nombre) et chaque mod du joueur a le sien (voir Budgets) : 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).
  • Une transformation des notes (chartTransform) : le script lit chart.getNotes() et rend chart.setNotes(tampon) ; Rust valide le tampon (crates/core/src/scripted_notes.rs) et refuse l'invalide en retirant seulement cette modification. La partie enregistre les notes résultantes (chartMods.scripted) : elle se rejoue sans le mod, et ce tampon, pas le code, fait foi.
  • 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 le jugement, le score ou un replay (ce qui est temps réel reste natif, voir la règle).
  • 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 un modificateur qui touche aux entrées ou au jugement : les genres auto et ghost sont natifs et fermés. Une transformation des notes passe par chart.getNotes / chart.setNotes (kind: "chartTransform"), jamais par un genre natif ; fullLn reste natif (paramètres typés et aperçu fournis par l'hôte). hidden, sudden et lift sont natifs parce que leur dessin passe par playfield, que seuls les skins et les maps peuvent appeler ; ils ne touchent ni aux notes ni au jugement.

Mods, skins, thèmes : ne pas confondre

  • Un thème (content/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