Skip to content

Le modèle : carte, niveau, bundle, projet, bibliothèque, release

Note

Technical reference — French only for now. / Référence technique, en français pour l'instant.

Ce document répond, côté développeur, à la question qui revient toutes les semaines : qu'est-ce qui compile l'art d'un jeu, où vit quoi, et qu'est-ce qui part sur la carte ? La description pour l'auteur de jeux est ailleurs — Projects, maps, levels, releases et Working in the App — et n'est pas redite ici : cette page dit les tables, les invariants, et pourquoi ils sont là.

La source de vérité est le service Go de papyDeck_App : le schéma dans internal/store/migrations/*.sql (à lire dans l'ordre, les dernières réécrivent les premières), les types dans internal/store/, les routes dans internal/web/server.go, les règles dans internal/web/. Si le code et ce document divergent, le code a raison — et il faut le dire. État : mi-octobre 2026, migration 047.

Le vocabulaire, en une table

Mot Ce que c'est Où il vit
Compte une personne : ses identités OAuth (Google, GitHub), ses adresses vérifiées, son pseudo unique account, identity, account_email, session
Projet le jeu en cours d'écriture : un dossier plat de .lua (main.lua, toujours), de .mask et de sons project, project_file
Bibliothèque les sprites du compte, en vraies couleurs (RGB/RGBA) : un sprite seul (sprite) ou une ligne d'animation (anime, le nombre de frames dans le nom : torche_4) asset, asset_frame
Modèle ce qu'un niveau place : une boîte, une ancre, un état par défaut et des états, chacun sur un sprite avec sa cadence, sa boucle et son effet. Partagé par les niveaux du compte modele (etats en JSON)
Carte le dessin : tuiles 8×8 (et leur tag solide), tampons, terrains, calques, décors posés, calque de collision, sources carte, carte_calque, carte_tampon, carte_decor
Niveau une carte et ce qui s'y joue : les objets (modèles placés), les réglages de compilation, l'aperçu — l'unité de compilation. Plusieurs niveaux peuvent partager une carte niveau
Bundle le produit de la compilation d'un niveau, sous le nom du niveau : palette, atlas, images indexées, fond tuilé, variantes, et la recette qui les a donnés bundle, bundle_asset, bundle_image, bundle_fond, bundle_variante
Attache « ce projet embarque le niveau N, vu par le jeu sous le nom V » — une référence, pas une copie project_bundle
Groupe (côté console) ce que la console voit d'un niveau : V.pal, V.atlas, ses PNG indexés, ses variantes V.<v>.pal, son fond V-fond.map + V-fond-tiles.png le dossier du jeu
Release l'instantané publié : un dossier complet — les fichiers du projet plus les pièces de ses attaches — figé, identifié par l'empreinte de son manifeste release, release_file, release_niveau
Publics les sprites et les niveaux publiés, en CC0 ou en CC BY, instantanés que les autres comptes copient asset_public, asset_public_frame, niveau_public, niveau_public_asset, et leurs journaux *_retrait
Audio les sons du compte (effets .sfx, morceaux .son), groupés en bundles audio qu'un projet attache audio, audio_bundle, audio_bundle_piece, project_audio_bundle

Donc : un niveau = un bundle = un groupe sur la console. Le bundle n'est qu'une mécanique de stockage et de publication ; il porte le nom du niveau, et s'attache sous le nom que le jeu voit (V, le nom du niveau, sauf pour une copie renommée — voir plus bas). Le jeu écrit gfx.background("V-fond"), phy.anim_load("V/torche_4"), ou ne l'écrit pas : le V.lua généré le fait pour lui. Le nom d'une carte n'arrive jamais à la console.

Le firmware dit « le dossier du jeu » pour l'ensemble que gfx.bundle() compile d'un coup, tous les groupes : c'est la release ici, pas un bundle de l'App.

Seule la compilation d'un niveau produit un bundle : un sprite n'entre dans un jeu que si un modèle qui le porte est placé dans un niveau, même invisible. Jusqu'à la migration 047 (octobre 2026), la carte portait aussi les objets et se compilait elle-même ; la 047 a séparé le dessin (la carte) de ce qui s'y joue (le niveau), sans compatibilité : cartes, bundles à fond, cartes publiques et releases qui en dépendaient ont été effacés, modèles et sprites gardés.

Un niveau désigne sa carte par la clé (account_id, carte_id) → carte (account_id, id) : une carte d'un autre compte est impossible par construction. La clé est en NO ACTION, si bien qu'une carte qu'emploie un niveau ne se supprime pas, alors que la suppression d'un compte emporte les deux d'un bloc.

Les octets : par empreinte, jamais par chemin

  • Le magasin d'objets range sous blob/<sha256>. Postgres ne garde que les métadonnées et l'empreinte. Deux fichiers identiques — le cas normal dès le premier clone — partagent un objet : publier, cloner, copier un public ne copient aucun octet, seulement des lignes.
  • Le prix : rien ne s'efface avec sa ligne. Un balayage prend le complément de ReferencedBlobs (internal/store/blobs.go). Toute colonne qui range une empreinte doit y entrer ; le test TestToutesLesTablesAHashSontBalayees découvre ces colonnes par le schéma et échoue sinon. Oublier une table, c'est effacer des fichiers vivants (c'est arrivé, début septembre 2026).
  • Les cartes et les niveaux, eux, vivent en base (bytea, JSON). Une carte se réécrit à chaque enregistrement et reste bornée (quelques centaines de Kio au pire) : la déduplication par empreinte n'apporte rien à un document qui change tout le temps.
  • Concurrence par If-Match. Un fichier de projet s'écrit sous l'ETag de sa lecture (l'empreinte), une carte ou un niveau sous le sien : deux onglets ne s'écrasent pas, le second reçoit 412.
  • Le serveur ne calcule pas l'art. Découpe, quantification, tuilage, atlas tournent en wasm dans le navigateur (/sim/pdgfx.mjs, le core/ du simulateur, piloté par static/compiler.js). Un second calcul en Go divergerait du premier ; le serveur range des octets et vérifie ce qui se vérifie sans calculer (PNG lisibles, tailles, noms, bornes). Le navigateur ne relit jamais les pixels d'un canvas pour en faire des données : Brave les bruite. Le monde aplati, l'aperçu et les planches importées se composent ou se décodent en JS (png.js) ; les canvas ne servent qu'à l'affichage.

Ce qui va où, et pourquoi

  • Le projet ne contient que du texte et des sons. On y crée des .lua, .mask, .son (creerFichier : jamais vide, jamais par-dessus un fichier existant) ; on y dépose en plus des .sfx, et des .wav convertis en .sfx à l'envoi. Une image déposée est refusée : elle échapperait au calcul de la palette sur l'ensemble. Une pièce de niveau n'est jamais un fichier du projet (046) : un .png, .pal, .atlas ou .map vient d'un niveau attaché ; ce que le dossier Levels ne montre pas, le jeu ne l'a pas.
  • main.lua naît avec le projet et ne meurt qu'avec lui. naitreProjet pose un squelette (_update, _draw) ; supprimerFichier refuse de le supprimer — c'est le point d'entrée. PUT ne fait que modifier un fichier existant : créer et supprimer ont leurs routes.
  • <niveau>.lua est généré, et marqué. Sa première ligne porte -- papyDeck:niveau <V> v1. La compilation n'écrase qu'un fichier qui porte ce marqueur ; un <V>.lua écrit à la main est gardé, la compilation le dit (réponse 409) et écrit le reste. Le texte est produit en Go, déterministe : une recompilation sans changement n'écrit rien. Les .lua de l'ancien marqueur (-- papyDeck:carte) ont été retirés des projets, avec leur .mask, par une purge unique au démarrage qui suit la migration 047 (047_purge_lua).
  • <niveau>.mask est régénéré à chaque compilation, sans marqueur ni garde : c'est le calque de collision de la carte OU les masques des décors posés, une case par tuile. Un .mask du même nom écrit à la main est remplacé.
  • Un effet est une propriété du bitmap. Additif, translucide, contour, derrière le fond : ce sont des rangées de palette. Ils se règlent sur l'état du modèle, valent pour tous les niveaux, et un même sprite ne peut pas porter deux effets dans un niveau (la compilation refuse).
  • Publier matérialise, le projet référence. La release embarque les pièces des attaches (graphiques et audio) ; le projet ne garde que l'attache. Recompiler un niveau change la prochaine publication, pas la précédente.
  • Rien ne se supprime sous un jeu. La base le garantit, ou une requête le vérifie avant d'écrire :
On ne supprime pas… tant que… Garde
un sprite un niveau compilé l'emploie clé bundle_asset.asset_id, différée → ErrAssetEmploye
un niveau (et son bundle) il est attaché à un projet clé project_bundle.bundle_id, différée → ErrBundleAttache
une carte des niveaux la posent clé niveau (account_id, carte_id), NO ACTION → ErrCarteEmployee (409, qui les nomme)
un modèle un niveau le place requête sur niveau.objets (JSON) → ErrModelePlace
un son un bundle audio l'emploie clé audio_bundle_piece.audio_id, différée → ErrAudioEmploye
un bundle audio un projet l'attache clé project_audio_bundle.bundle_id → ErrAudioBundleAttache
un sprite public un niveau public l'emploie requête sur niveau_public_asset
un niveau public un jeu publié l'emploie clé release_niveau.niveau_public_id, sans cascade
main.lua toujours supprimerFichier

Les clés sont différées (deferrable initially deferred) et non restrict, et la nuance compte : restrict interdirait aussi de supprimer un compte, dont la cascade détruit sprites et bundles dans un ordre que Postgres ne garantit pas. Différé, le contrôle a lieu au commit, quand les lignes qui protégeaient sont parties avec leur propriétaire : « on ne supprime pas un sprite employé, mais un compte s'en va d'un bloc ».

Compiler un niveau

POST /api/niveaux/{nom}/compiler, en multipart. L'atelier enregistre la carte si elle a des modifications, puis le niveau, et construit dans le navigateur :

  1. le monde aplati de la carte : tous les calques, masqués compris, du bas vers le haut, chacun suivi de ses décors posés — l'œil et le verrou ne concernent que l'éditeur ;
  2. la recette : les sprites distincts des modèles placés, chacun avec l'effet de ses états (63 sprites plus le fond au plus par construction) ;
  3. le plan des rangées (opaques, translucides, additifs, ombres, derrière) et la rampe du ciel (64 index au plus, en queue) : 255 rangées au plus, la rangée 0 étant transparente ;
  4. le wasm quantifie le fond et les sprites ensemble — une palette — puis retuile le fond (tuiles distinctes par fréquence, miroirs fusionnés, 704 au plus, regroupement par similarité au-delà) ; les variantes dérivent leurs couleurs de cette palette ;
  5. un aperçu PNG du niveau en taille réelle, que sa publication et celle d'un jeu reprendront.

Le formulaire porte aussi carte_maj, l'ETag de la carte sur laquelle le fond a été construit : si la carte a changé depuis, le serveur répond 409 — le fond et le .mask (tiré de la carte en base) ne viendraient pas du même dessin. Le serveur (niveau_compiler.go) range les pièces par empreinte, exige un fond et un seul, nommé fond, refuse ce que la console refuserait — un objet hors de la carte, un modèle placé qui n'existe plus, deux modèles placés que le jeu verrait sous la même clé, deux effets pour un sprite, plus de 64 états à animer (la console en tient 64 à la fois ; M.decharger() du <niveau>.lua les rend avant le niveau suivant), une recette qui ne couvre pas les sprites placés, un nom complet de plus de 47 caractères —, puis écrit :

Pièce Table Nom dans le dossier du jeu
palette, 768 o (512 de couleurs RGB565, 256 de modes) bundle.pal_hash V.pal
atlas (régions, format Kivy multi-pages) bundle.atlas_hash V.atlas
une image indexée par sprite ou animation bundle_image son nom, préfixé du niveau
le fond : carte texte et planche des tuiles bundle_fond (nom court fond) V-fond.map, V-fond-tiles.png
les variantes (réglages et .pal dérivé, mêmes modes) bundle_variante V.<v>.pal
la recette : sprites, effets, dans l'ordre bundle_asset —
le plan et le ciel bundle.plan_*, ciel_* —
l'aperçu niveau.apercu_hash —

PutBundle est un remplacement complet sous le nom du niveau : un bundle ne se renomme pas, et un nom de niveau doit être libre comme niveau et comme bundle. Le bundle et les .mask s'écrivent même quand un .lua est refusé. Puis, pour chaque projet coché : le contrôle des bornes, l'attache sous V (un upsert : recompiler repointe), V.mask, V.lua. La réponse dit, projet par projet, ce qui a été écrit ou refusé. Le niveau note enfin carte_compilee, la date de la carte compilée : un niveau est périmé quand la carte est plus récente, et l'atelier le marque recompile.

Les réglages de compilation — plan, ciel, variantes, projets cochés — sont enregistrés avec le niveau (niveau.compil, JSON). Détacher (DELETE /api/projects/{slug}/bundles/{V}) retire le lien, et avec lui ce que la compilation avait écrit : V.lua s'il porte le marqueur (un fichier écrit à la main reste) et V.mask. Un .lua généré qui resterait ferait croire à la publication que le jeu emploie encore le niveau. Supprimer un niveau supprime son bundle, et c'est refusé tant qu'il est attaché.

Les attaches, et les bornes de la console par projet

project_bundle = (projet, bundle, nom vu), clé (projet, nom vu). Les pièces ne sont pas copiées dans le projet : PiecesDeProjet les calcule à la lecture, aux empreintes du bundle. GET /api/projects/{slug} les liste à part des fichiers (elles ne s'éditent pas), GET /api/projects/{slug}/files/{nom} les sert (le fichier du projet d'abord, la pièce ensuite) : c'est ce que le simulateur et la console demandent, par nom plat, comme require().

La console tient, par jeu, 12 palettes, 16 fonds et 2 Mio de pièces graphiques (PD_LIB_BYTES). Rien ne les comptait avant mi-septembre 2026, et un jeu les découvrait au lancement. Comptés sur les attaches (bornesDuProjet) :

  • palettes : 1 par niveau attaché, plus ses variantes ;
  • fonds : les lignes bundle_fond (un niveau en apporte un ; deux niveaux sur une même carte en apportent deux) ;
  • octets : .pal, .atlas, variantes, images, .map et planches, tailles lues dans le magasin.

La compilation compte le projet sans l'attache de même nom vu, plus le niveau avec ses nouvelles pièces, et refuse d'attacher si une borne déborde. C'est le seul moment où le compte se vérifie ; la fiche du projet le montre. Le quota du projet est une autre affaire : il ne compte que ses fichiers.

Les releases

POST /api/projects/{slug}/publish, corps {notes, licence, confirme, categorie}.

  • Une release est un dossier complet. release_file reçoit les fichiers du brouillon, puis les pièces des attaches graphiques et audio, nommées comme le jeu les voit ; un fichier du projet l'emporte sur une pièce homonyme. L'export .zip du brouillon (/projets/{slug}/export.zip) fait le même dossier.
  • Le manifeste. manifest_hash = sha256 des fichiers triés par nom, chacun rendu nom \0 empreinte \n. Non unique : deux releases identiques au bit près le partagent, et c'est ainsi qu'on repère un clone jamais modifié. Le même calcul sur le brouillon, pièces comprises, répond à « le projet a-t-il changé depuis la dernière version ? ».
  • La version est max + 1 par (compte, slug), sous verrou du projet. Le pseudo de l'auteur n'est pas stocké, il est joint à la lecture : un pseudo change, account_id identifie. project_id passe à null si le projet est supprimé : une version publiée lui survit.
  • La filiation. Cloner pose project.parent_release_id ; chaque release du clone le recopie dans release.parent_release_id. C'est une filiation, pas une succession (la version la dit déjà), remontée sur 32 générations au plus.
  • Un jeu se publie avec ses sources. Ses niveaux sont ses attaches et chaque .lua qui porte le marqueur de l'atelier (un clone n'a pas d'attache avant que l'atelier compile ses niveaux) — le nom vu se résout en niveau par nom_jeu. Chacun doit exister, avoir un aperçu, ne pas être périmé, et ses modèles et sprites aussi ; rien de ce qu'il contient ne doit être non déclaré ni signalé. Il faut alors la case et une catégorie de niveau. La licence est celle que les éléments permettent (licence.Combiner) : CC BY dès qu'un élément l'est, un credits.txt généré entre alors dans la release. Dans une transaction : chaque niveau est publié en niveau public (republié seulement s'il a changé), ses sprites privés entrent dans la bibliothèque publique (catégorie divers, tag tiré du slug), la release est figée, et release_niveau note (release, niveau public, version, nom vu). Tout ou rien. La migration 047 a supprimé les releases qui avaient une release_carte, avec une ligne de journal.
  • Retirer un jeu supprime ses releases (RetirerJeu), toutes versions, avec une ligne de journal (release_retrait). Fichiers et release_niveau partent en cascade, les parent_release_id qui les désignaient passent à null ; les projets clonés restent, les niveaux publics aussi, et redeviennent retirables.
  • La galerie (GET /api/gallery) rend la dernière version de chaque jeu. Les tutoriels — un jeu d'administrateur dont le slug commence par tuto_ — n'y sont que sur demande (?tutos=1) : les consoles en service lisent cette liste.

Publics, copies et clones

Les sprites publics

Publier fige un instantané : asset_public (une ligne par (compte, nom) — la place de republication) et ses frames recopiées en lignes asset_public_frame, aux mêmes empreintes. Retoucher ou supprimer le sprite privé n'y change rien ; republier crée une version si les frames ont changé, et ne fait que mettre à jour la fiche si seuls la catégorie ou les tags ont bougé. Seuls sprite et anime se publient ; licence CC0-1.0 ou CC-BY-4.0 avec son crédit, selon l'origine déclarée, 7 catégories fixes, 8 tags au plus, 500 publications par compte. Retirer supprime la ligne (les octets partent au balayage si plus rien ne les nomme) et journalise sans empreinte.

Copier (CopierPublic) fait un asset privé ordinaire aux mêmes empreintes, avec son origine (asset.origine_*, le pseudo au moment de la copie) sans clé étrangère : un retrait ou un compte supprimé ne touchent jamais une copie. Recopier la même version rend la copie déjà faite. Un nom pris donne -2, -3… avant le nombre de frames (torche-2_4).

Les niveaux publics

Publier un niveau fige un pack : le niveau, sa carte, les modèles de ses objets et leurs sprites. Niveau, carte et modèles forment un document — JSON compressé, déterministe ({format, niveau, carte, modeles}), sans identifiants, dates ni projets cochés — rangé sous doc_hash ; l'aperçu est le PNG du navigateur ; les frames des sprites sont des lignes niveau_public_asset, aux empreintes de la bibliothèque, avec un lien sans clé vers leur publication d'asset. Les sprites encore privés sont publiés dans la même transaction ; une copie inchangée d'un sprite public ne l'est pas, l'original suffit. La licence est la plus stricte de ce que le pack contient : les signalements du niveau et de sa carte, les sources de la carte (ou « Sources not reviewed », qui bloque) et les sprites des modèles posés ; niveau_public garde la licence et les crédits réunis. Même règle de version que les sprites ; 100 niveaux publics par compte. Un niveau public qu'un jeu publié emploie ne se retire pas.

Copier un niveau, cloner un jeu

copierNiveauTx recrée le pack chez l'autre compte, sans jamais rien écraser et sans copier aucun produit compilé : la copie se recompile.

  • elle refuse si le document a changé depuis que le client l'a lu ; la même version déjà copiée rend la copie existante ;
  • les sprites : même nom et mêmes frames → réutilisé ; une copie inchangée de la même publication → réutilisée ; sinon un nom libre (-2…) ;
  • les modèles : identique → réutilisé ; sinon _2, _3… (une clé Lua de 24 caractères) ;
  • la carte : copiée une fois par opération (la clé : l'empreinte de la carte dans le document), sous un nom libre ; elle reçoit une source qui crédite l'auteur du niveau public (level <auteur>/<nom>, la licence de la publication, le crédit <auteur> (papyDeck)) : sans elle, republier la copie ne créditerait personne pour les tuiles ;
  • le niveau : _2, _3…, libre comme niveau et comme bundle, borné pour que <niveau>/<sprite>_<n> tienne avec le plus long de ses sprites ;
  • les références — états vers sprites, objets vers modèles, niveau vers carte — sont réécrites ;
  • niveau et modèles renommés gardent leur nom de jeu (nom_jeu) : la compilation attache niveau1_2 sous niveau1, écrit niveau1.lua, garde la clé M.modeles.joueur. Le code écrit pour l'original marche tel quel.

Cloner (POST /api/clone, {release_id}) fait, en une transaction, le projet (slug libre -2…, quota compté sur la taille logique de la release, parent_release_id posé), ses fichiers depuis release_file — sauf les pièces des niveaux (.map, .atlas, .pal, .png) et credits.txt —, puis la copie de chaque niveau de release_niveau, deux niveaux sur une même carte partageant une seule copie de la carte. Sans ses sources, pas de clone. L'atelier compile ensuite chaque niveau copié dans le clone : c'est ce qui l'attache. Le clone hérite de la licence et des crédits du jeu cloné (project.licence_heritee, credits_herites).

Le retrait pour crédit invalide

Un crédit faux fait retirer tout de suite ce qui est public (POST /api/admin/credit-invalide, {quoi: sprite | niveau | jeu, id, note, apercu}, administrateurs ; apercu rend le bilan sans rien faire). Le retrait s'étend, au point fixe : un sprite emporte les niveaux publics qui l'embarquent, qui emportent les jeux qui les emploient. Les copies déjà faites chez les autres ne s'effacent pas : elles sont signalées (credit_invalide_le, _motif sur asset, carte, niveau, project). Viser un niveau signale ce niveau, sa carte et leurs copies ; la cascade depuis un sprite ne signale que les niveaux. Un élément signalé reste utilisable chez son propriétaire, mais rien de ce qui le contient ne se publie ; son propriétaire lève le signalement en cochant la case (POST /api/signalement/lever, {quoi: sprite | carte | niveau | projet, nom, confirme}), et c'est journalisé.

L'audio

Calqué sur la bibliothèque graphique, avec une différence de fond : pas de palette partagée. Un .wav se convertit seul en .sfx (en-tête PDSX, PCM 8 bits mono 24 kHz) ; un .son est du texte (16 Kio au plus), que seul le navigateur analyse. Rien ne se calcule sur un groupe : le bundle audio n'est qu'un regroupement nommé.

  • audio : un son = un blob (sfx ou musique), avec son nombre d'échantillons ;
  • audio_bundle + audio_bundle_piece : les sons, chacun sous le nom plat que le jeu lira ;
  • project_audio_bundle : l'attache, une étiquette par projet.

Les pièces arrivent dans le dossier du jeu sans préfixe — snd.play("tir") lit tir.sfx, snd.music("theme") lit theme.son —, à l'inverse des pièces graphiques. D'où le refus à l'attache d'un nom déjà fourni par un autre bundle audio ou par un fichier du projet.

La console n'a qu'une réserve : 64 effets et 1 920 Kio d'échantillons (PD_SON_MAX_SFX, PD_SON_PCM_BYTES, ~82 s), tous bundles et fichiers .sfx confondus ; les morceaux n'y comptent pas. Elle refuse le jeu entier au lancement quand elle déborde ; l'App refuse donc avant, aux quatre endroits où l'audio d'un projet grossit : le dépôt d'un .sfx ou .wav dans le projet, l'enregistrement d'un bundle (seul, il doit déjà tenir), l'attache, la publication.

Les noms, et leur borne

Une seule famille de règles, celle de la carte micro-SD : une release se pose telle quelle sur du FAT32, qui ne distingue pas la casse. Donc minuscules partout, pas de nom réservé de Windows (con, nul, com1…), des noms plats comme require().

Nom Règle
slug de projet [a-z0-9][a-z0-9_-]{0,62}
fichier de projet [a-z0-9_-]{1,60} + .lua, .mask, .son (créés) ou .sfx (déposé)
sprite, son [a-z0-9_-]{1,60} ; un anime finit par _<frames>
niveau, carte, bundle audio, variante [a-z0-9_]{1,60} — pas de tiret : il sépare le groupe de son fond (V-fond) ; un niveau est libre comme niveau et comme bundle
modèle, état [a-z][a-z0-9_]{0,23} — une clé Lua (M.modeles.joueur)
calque, décor [a-z0-9_]{1,24}
propriété d'objet [a-z_][a-z0-9_]{0,23}, valeur de 64 caractères au plus
tag public [a-z0-9_-]{1,24}, 8 au plus

Un nom complet — V/<région> pour gfx.id(), V-fond pour gfx.background(), V.<variante> — tient dans les tampons de 48 octets de la console et du simulateur (PD_NAME_MAX du core, PDC_NAME_MAX de RP1, alignés début septembre 2026 après qu'une troncature silencieuse eut rendu gfx.id() muet sur la carte) : 47 caractères au plus. Ce nom n'existe pas avant d'être attaché ; il se vérifie donc à la compilation, contre le nom vu et contre chaque attache existante du bundle, avec un refus qui nomme le fautif. Chaque moitié garde sa propre règle ; aucune paire qui tient n'est interdite. Les copies s'y plient d'avance : un sprite copié est borné pour tenir derrière un niveau d'une lettre, un niveau copié pour tenir avec son plus long sprite.

La surface de l'API

Tout répond en JSON sous /api/, avec de vrais codes HTTP ; un refus porte {error, detail} (la compilation, elle, rend son compte rendu avec refus). La cloison tient en une phrase : /api/projects/** et ce qui appartient au compte exigent la session et se résolvent toujours par (compte, nom) ; la vitrine se lit sans compte.

Famille Routes Ce qu'elles font
Session GET /api/session qui est là, ce que la console tolère
Projets /api/projects, /api/projects/{slug} lister, créer (avec main.lua), lire (fichiers et pièces), supprimer
Fichiers …/{slug}/files, …/files/{nom}, …/upload créer, lire, modifier sous If-Match, supprimer, déposer
Attaches DELETE …/{slug}/bundles/{V}, POST …/{slug}/audio, DELETE …/{slug}/audio/{V} détacher un niveau ; attacher et détacher un bundle audio
Versions …/{slug}/releases, …/{slug}/publish, …/{slug}/retirer l'état des versions, publier, retirer le jeu
Cartes /api/cartes, /api/cartes/{nom} lister (avec les niveaux qui les posent), créer, lire, enregistrer sous If-Match, supprimer (409 si des niveaux la posent)
Niveaux /api/niveaux, /api/niveaux/{nom}, …/compiler lister, créer sur une carte, lire, enregistrer objets et réglages sous If-Match, compiler, supprimer
Modèles /api/modeles, /api/modeles/{nom} lire, créer ou remplacer, supprimer
Bibliothèque /api/assets, /api/assets/{nom}, …/origine, …/publier déposer (le redépôt remplace), lire, déclarer l'origine, supprimer, publier
Sprites publics /api/publics, /api/publics/{id}, …/copier, …/retirer chercher, lire, copier, retirer
Niveaux publics /api/niveaux/{nom}/publication, …/publier, /api/niveaux-publics, …/{id}, …/copier, …/retirer situation et publication d'un niveau ; chercher, lire, copier, retirer
Audio /api/audio, /api/audio/musiques, /api/audio/{nom}, …/origine, /api/audio-bundles/{nom} sons, morceaux (texte), leur origine, bundles audio
Crédits /api/admin/credit-invalide, /api/signalement/lever retirer pour crédit invalide (administrateurs), lever un signalement
Vitrine (sans compte) /api/gallery, /api/releases/{id}, …/files/{nom}, /api/firmware les jeux publiés, leurs fichiers, les firmwares
Clone POST /api/clone un projet depuis une release, sources comprises

Hors API ne restent que les routes qui servent des octets — les frames (/gfx/assets/…, /gfx/publics/…, et leur anim.gif), les sons (/audio/assets/{nom}), l'aperçu d'un niveau public, l'export .zip d'un projet —, la connexion, la page du compte, l'administration, l'atelier lui-même (/app2/) et le simulateur (/sim/). Jusqu'en octobre 2026, des pages HTML rendues par le serveur écrivaient les mêmes tables par des formulaires ; leurs règles sont restées, partagées avec l'API (« deux portes, une règle ») ; les pages, elles, ont disparu.