Le datapack et le resource pack du serveur Switch (Paralya) : un lobby, une infinité de mini-jeux votés entre chaque partie, un système de maps régénérables, des boutiques, des statistiques et des succès.
Tout est généré en Python avec StewBeet, un framework au-dessus de beet. Aucun fichier .mcfunction n'est écrit à la main : ils sont produits par le code de src/.
Documents (Google Sheet) :
- Dépôt GitHub du serveur Switch
- Python 3.14+
- StewBeet :
pip install -U stewbeet
Une seule commande, à la racine du dépôt :
stewbeet| Commande | Effet |
|---|---|
stewbeet |
Build complet (équivaut à stewbeet build) |
stewbeet rebuild |
Nettoie les caches puis rebuild |
stewbeet clean |
Nettoie les caches et les dossiers de sortie |
stewbeet --help |
Liste toutes les commandes |
Le build remplit build/ puis copie les .zip vers les chemins de build_copy_destinations (beet.yml).
Ces chemins sont personnels (resourcepacks local, SFTP du serveur) : adaptez-les chez vous, mais ne committez pas vos chemins locaux.
build/ est entièrement généré et écrasé à chaque build : il n'est versionné que parce que le serveur pull le dépôt, donc on ne le commit qu'après un vrai stewbeet.
Pour retrouver le code d'une commande in-game, grep son texte dans src/ : le chemin de la fonction est le premier argument de write_function.
Switch/
├── ⚙️ beet.yml # Config du projet et du pipeline de build
├── 🚀 upload.py # Publication d'une release GitHub
├── 🧰 tools/ # Scaffolding et garde-fous (voir plus bas)
├── 🐍 src/ # TOUT le code source
│ ├── setup_definitions.py # Étape 1 : items, blocs, matériaux, disques
│ ├── link.py # Étape 2 : appelle tous les générateurs
│ ├── validation.py # Contrôles de cohérence, échoue le build
│ ├── 📦 database/ # Items et comportements de blocs
│ ├── 🎨 resource_pack/ # Langues, sounds.json, shaders, textures GUI, fonts
│ └── 📂 datapack/
│ ├── main.py # Définitions brutes du datapack
│ ├── definitions/ # Advancements, dimensions, loot tables, prédicats, tags...
│ ├── 🎮 modes/ # Un dossier par mini-jeu, + spec/catalogue/emit
│ ├── 🧠 engine/ # Vote, démarrage, arrêt, signaux, pop-ups
│ ├── 🧍 player/ # Layout d'inventaire, practice, jump timer
│ ├── 🏛️ lobby/ # Boards, leaderboards, PNJ, tick hors partie
│ ├── 🗺️ maps/ # Chargement, géométrie, et generation/ (régénération)
│ ├── 🎒 kits/ # Modèle déclaratif des kits (Kit, KitItem, rôles)
│ ├── 🛒 shop/ # Boutiques (consomme le registre des modes)
│ ├── 📊 stats/ # Classements et statistiques
│ ├── 🏆 advancements/ # Succès et pourcentages
│ ├── 🎬 cinematic/ # Cinématiques d'intro
│ ├── 🎵 music/ # Lecteur de musique et Note Block Studio
│ ├── 🌐 translations/ # Textes partagés FR/EN
│ ├── 🔧 utils/ # Primitives appelées par une vingtaine de modes
│ ├── 🛠️ devtools/ # test_mode, lag artificiel, profiling
│ └── 🌱 root/ # load, tick, second et fonctions racine
├── 🖼️ assets/ # Textures, sons, disques, pack.png
├── 📚 libs/ # Packs externes fusionnés au build
├── 🎼 note_block_studio/ # Musiques (midi, datapacks générés)
├── 📤 continuous_delivery/ # Config de la release GitHub
└── ⛔ build/ # SORTIE GÉNÉRÉE, ne jamais éditerL'ordre vient de la clé pipeline de beet.yml. Deux entrées seulement sont du code maison :
src.setup_definitionsremplit la base d'items et de blocs (Item,Block, matériaux deORES_CONFIGS, disques) et charge les définitions écrites à la main. Les plugins StewBeet suivants s'appuient dessus.src.linkappellegenerate_all_modes()puis chaque générateur de sous-système : c'est là que la quasi-totalité des.mcfunctionsont écrites.
Le reste vient de plugins StewBeet : headers, constantes de scoreboard, dépendances, merge Smithed Weld, zip, copie, sha1.
src/datapack/modes/__init__.py importe dynamiquement chaque dossier contenant un main.py et appelle son write_mode(), puis le write_resources() de son resources.py s'il existe. Aucune liste d'imports à maintenir.
| Je veux modifier... | Fichier |
|---|---|
| 🗳️ La liste des mini-jeux votables, descriptions, temps estimé, auteurs | src/datapack/modes/catalogue.py (MODES) |
| 🧩 Les groupes de vote (variantes sous une même entrée) | src/datapack/modes/catalogue.py (GROUPS) |
| 🎮 La logique d'un mini-jeu | src/datapack/modes/<mode>/main.py |
| 🌐 Les messages FR/EN d'un mini-jeu | src/datapack/modes/<mode>/translations.py |
| 📜 Les advancements, prédicats, loot tables, structures d'un mini-jeu | src/datapack/modes/<mode>/resources.py |
| 🔁 Un helper partagé entre plusieurs modes | src/datapack/modes/emit.py ou src/datapack/modes/_common/main.py |
| 🧠 Le vote, le lancement, l'arrêt d'une partie, les signaux | src/datapack/engine/main.py |
| 🌍 Les maps de jeu (dimensions, régénération, zones) | src/datapack/survival_maps/definitions.py |
| 🏁 Les checkpoints de course et les cycles de spawn | src/datapack/maps/main.py |
| 🎒 Un kit ou une classe | src/datapack/kits/ et src/datapack/modes/<mode>/kits.py |
| 🛒 Une boutique | src/datapack/modes/<mode>/shop.py, agrégé par src/datapack/shop/shared_memory.py |
| 📦 Un item ou un bloc custom | src/database/misc_items.py, src/database/blocks_behaviors.py |
| ⛏️ Les matériaux générés (armures, outils) | src/setup_definitions.py (ORES_CONFIGS) |
| 🖼️ Une texture d'item | assets/textures/**/<item_id>.png, détection automatique |
| 🔊 Un son | assets/sounds/, ou src/datapack/modes/<mode>/sounds/ pour un son propre à un mode |
| 🛠️ Un outil de dev (test mode, lag, profiling) | src/datapack/devtools/ |
| ✨ Les shaders | src/resource_pack/shaders.py |
| 🪟 Les textures de GUI et de tooltips | src/resource_pack/textures.py |
python tools/new_mode.py mon_modeLe mode obtenu build et se lance en jeu immédiatement. La découverte est automatique : aucun import à ajouter ailleurs.
src/datapack/modes/mon_mode/
├── __init__.py # vide
├── main.py # write_mode()
└── translations.py # write_translations()Le fichier généré ressemble déjà à ceci, avec les six hooks branchés. Il ne reste qu'à écrire les mécaniques du jeu.
# Imports
from stewbeet import Mem, write_function
from ..emit import write_modes_calls, write_time_xp_bar
from .translations import write_translations
def write_mode():
ns: str = Mem.ctx.project_id
mode: str = "mon_mode"
path: str = f"{ns}:modes/{mode}"
# Écrit /calls/* (le dispatch appelé par le moteur) et /_force_start
write_modes_calls(mode)
write_translations()
# /start : appelé une fois au lancement de la partie
write_function(f"{path}/start", f"""
effect give @a[tag=!detached] saturation infinite 255 true
# Choix de la map parmi une liste (les ids viennent de survival_maps/definitions.py)
scoreboard players set #do_spreadplayers {ns}.data 1
function {ns}:utils/choose_map_for {{id:"{mode}", maps:["switch_space"]}}
scoreboard players set #mon_mode_seconds {ns}.data -6
scoreboard players set #process_end {ns}.data 0
""")
# /tick : chaque tick de jeu
write_function(f"{path}/tick", f"""
function {ns}:utils/on_death_run_function {{function:"{path}/death"}}
""")
# /second : chaque seconde de jeu
write_function(f"{path}/second", f"""
scoreboard players add #mon_mode_seconds {ns}.data 1
function {path}/xp_bar
function {path}/translations/second
""")
# /joined : un joueur rejoint en cours de partie
write_function(f"{path}/joined", f"""
gamemode spectator @s
""")
# /stop : nettoyage en fin de partie
write_function(f"{path}/stop", f"""
scoreboard objectives remove {ns}.temp.mon_score
""")
# /xp_bar : barre d'XP servant de timer
write_time_xp_bar(f"{path}/xp_bar", 300, "#mon_mode_seconds", "#mon_mode_seconds")Les hooks disponibles sont joined, second, start, stop, tick et inventory_changed.
Le moteur les appelle via switch:engine/signals/macro_*, qui redirige vers switch:modes/<mode>/calls/<hook>.
write_modes_calls() génère ces redirections et le garde-fou sur current_game qui empêche un mode de tourner pendant qu'un autre est en cours.
Dans src/datapack/modes/definitions.py, ajoutez une entrée à MODES :
{
"min_players":2, "max_players":-1, "id":"mon_mode", "name_fr":"Mon Mode",
"estimated_time": "2-5 mins", "inspiration": "Épicube", "suggested_by": "Pseudo", "developed_by": "Pseudo",
"description": {
"fr": [{"text":"Première ligne de description.\n"},{"text":"Deuxième ligne.\n"}],
"en": [{"text":"First description line.\n"},{"text":"Second line.\n"}]
},
},| Clé / comportement | Détail |
|---|---|
id |
Exactement le nom du dossier |
max_players |
UNLIMITED quand le mode n'a pas de plafond |
group |
Optionnel : les jeux d'un même groupe forment une entrée de vote, puis un second vote départage (voir GROUPS) |
| Lore, index, pop-ups | Générés automatiquement à partir de cette entrée |
| Entrée commentée | Le mode est quand même généré : testable via _force_start sans polluer le vote |
| Erreur de saisie | Le build échoue avec un message qui nomme le problème et suggère la valeur la plus proche |
/function switch:devtools/test_mode
/function switch:modes/mon_mode/_force_start
Advancements, prédicats, loot tables, item modifiers, tags ou structures vont dans un resources.py exposant write_resources(), appelé automatiquement après write_mode().
| Fichier | Rôle | Appelé par |
|---|---|---|
main.py |
write_mode() : toute la logique, écrit les .mcfunction |
automatique |
translations.py |
write_translations() : messages FR/EN dans <mode>/translations/* |
write_mode() |
resources.py |
write_resources() : advancements, prédicats, loot tables, tags, structures |
automatique |
kits.py |
Kits et classes du mode, construits avec Kit et KitItem |
write_mode() |
shop.py |
Constante SHOP : les upgrades vendues par le mode |
automatique |
structures/ |
Fichiers .nbt, enregistrés via register_structures() |
resources.py |
sounds/ |
Fichiers .ogg propres au mode, via register_sounds() |
resources.py |
_common/: fonctions partagées écrites dansswitch:modes/_common/*(mort en spectateur, fin de partie, barre d'XP, kits communs)._coupdetat/: pseudo-mode utilisé par le moteur, pas un mini-jeu votable.- Les helpers Python partagés vivent dans
src/datapack/modes/emit.py:write_modes_calls,write_server_announce,write_time_xp_bar,write_no_drop,register_structures,register_sounds. - Le namespace n'est jamais écrit en dur : utilisez toujours
ns: str = Mem.ctx.project_id.
- Une map : un
clone_survival(...)oufill_survival(...)danssrc/datapack/survival_maps/definitions.py(coordonnées, id, nom, auteurs,viewde la cinématique). Elle devient utilisable dans lemaps:[...]d'unchoose_map_for. - Un item ou un bloc :
Item(...)ouBlock(...)danssrc/database/misc_items.py. Texture trouvée automatiquement si un.pngdu même nom existe sousassets/textures/; modèle, loot table et recettes sont générés. - Un son : le
.oggdansassets/sounds/, ou dans lesounds/du mode s'il lui est propre. Voirassets/compress_ogg.py,force_mono.pyetmp3_to_ogg.py. - Une boutique : un
shop.pyexposant une constanteSHOP, sur le modèle despleef/shop.py. Le registre la récupère tout seul ; son rang dans la boutique vient deSHOP_ORDER(src/datapack/shop/shared_memory.py).
- Typage strict : pyright en mode
strict(pyrightconfig.json), pas deAny. - Indentation par tabulations, alignement des colonnes par espaces.
- Code auto-explicatif : peu de commentaires, les vraies explications vont dans les docstrings.
- Préférez le déclaratif et le paramétré à la duplication. Le CI lance jscpd sur
src/pour traquer le copier-coller. - Un fichier qui dépasse environ 300 lignes devient un sous-module. Exception assumée : le
main.pyd'un mode de jeu. Toute la logique du mode reste au même endroit, ce qui rend le Ctrl+F immédiat et évite d'avoir à deviner dans quel fichier se trouve une mécanique.build_battle/main.pyfait 935 lignes, et c'est très bien ainsi. - Lint :
ruff check src tools --fix(la config vit dansruff.toml). - Perfs : le serveur tourne à 20 tps avec beaucoup de joueurs. Évitez les
@enon filtrés dans lestick, préférez tags et scores.
Six outils, tous lançables à la main. Les trois premiers tournent aussi en CI.
| Commande | Ce qu'elle garantit |
|---|---|
ruff check src tools |
Style et imports |
pyright |
Typage strict, sans Any |
python tools/check_conventions.py |
Taille des fichiers, pureté du modèle, aucun import descendant vers un mode nommé |
python tools/check_output_drift.py |
Le refactoring n'a rien changé : rebuild puis build/ identique à HEAD |
python tools/report_merged_functions.py |
Aucune fonction n'est écrite par deux émetteurs sans que ce soit déclaré |
python tools/check_rename_only.py "a=b" |
Un diff de build/ est exactement les renommages annoncés, et rien d'autre |
Ces deux derniers vont par paire : check_output_drift.py prouve qu'un refactoring n'a rien changé, check_rename_only.py prouve qu'un déplacement de chemins n'a fait que déplacer. Le plus important reste check_output_drift.py. write_function ajoute à la suite par défaut, donc deux émetteurs visant le même chemin fusionnent silencieusement dans l'ordre d'appel. Après tout déplacement de code, un build/ inchangé est la preuve que rien n'a bougé en jeu.
Deux règles s'appuient sur des listes explicites qui ne doivent que diminuer :
LONG_FILE_DEBTdanstools/check_conventions.py: les fichiers qui dépassent encore 300 lignes.DECLARED_MERGESdanstools/report_merged_functions.py: les fonctions volontairement construites par ajouts successifs.
- Une branche par fonctionnalité, puis Pull Request vers
main. - Commits courts, format conventionnel avec gitmoji après les deux-points :
feat(modes): ✨ Ajout du mode Block Partyfix(race): 🐛 Correction des checkpoints sur rainbow_roadperf(engine): ⚡️ Réduction du coût du tickbuild: 🚀 Built with latest StewBeet version
- Les releases sont publiées avec
python upload.py(nécessite~/stewbeet/credentials.yml).