Guides
Organiser un mod
Tu écris un mod une fois et tu l’entretiens pendant des années. Ce qui aide le plus à l’entretenir, ce n’est pas le langage : c’est l’endroit où vit chaque chose. Ce guide propose une structure pour CraftScript et une pour Kotlin, et explique pourquoi.
Quatre règles
- Une responsabilité par fichier. Un fichier pour les minerais, un autre pour les recettes, un autre pour chaque commande. Si tu as besoin du mot « et » pour décrire un fichier, ce sont probablement deux fichiers.
- Regroupe par thème, pas par taille. Des dossiers comme
content/,events/,commands/,ui/etlib/. Quiconque ouvre le mod sait où chercher sans lire le code. - Sépare les données du comportement. Ce qui existe (objets, blocs, recettes) est une liste de propriétés. Ce qui se passe (événements, commandes) est du code. Si tu les mélanges, changer un nombre t’oblige à lire de la logique.
- Un point d’entrée qui ne fait que relier.
main.craftou la classe du mod disent quelles parties existent. Ils ne contiennent pas de règles du jeu.
Ainsi, rééquilibrer le mod revient à modifier content/recipes.craft ; corriger une commande, à ouvrir commands/ ; et un bug dans un événement ne se cache pas au milieu de deux cents lignes de blocs.
En CraftScript
mods/rubies/
├── main.craft the mod and the list of its parts
├── settings.craft settings for the server owner
├── content/ data: what exists
│ ├── items.craft
│ ├── ores.craft
│ ├── world.craft
│ ├── dimensions.craft
│ ├── creatures.craft
│ ├── sounds.craft
│ ├── particles.craft
│ └── recipes.craft
├── events/ behaviour: what happens
│ ├── mining.craft
│ ├── hunting.craft
│ └── welcome.craft
├── ui/ forms: what the player sees
│ └── shop.craft
├── commands/
│ ├── realm.craft
│ ├── rubies.craft
│ └── shop.craft
├── lib/ shared logic
│ ├── rewards.craft
│ └── storage.craft saved data: the keys, in one place
├── sounds/
│ └── ruby/chime.ogg
├── structures/
│ └── ruby_shrine.nbt
└── textures/
├── item/ruby.png
├── block/ruby_ore.png, ruby_log.png, ruby_leaves.png, ruby_flower.png
├── entity/ruby_golem.png
└── particle/ruby_spark_0.png, ruby_spark_1.pngmods/rubies/
Déclare seulement le mod et liste ses parties. En le lisant, tu sais ce qu’il y a et où.
mod "rubies" { name: "Rubies", version: "1.0.0", author: "you" }
// What exists
import "content/items"
import "content/ores"
import "content/world"
import "content/dimensions"
import "content/creatures"
import "content/sounds"
import "content/particles"
import "content/recipes"
// What happens
import "events/mining"
import "events/hunting"
import "events/welcome"
import "commands/realm"
import "commands/rubies"
import "commands/shop"Comment fonctionnent les imports
import "content/items"exécute ce fichier une fois. Les objets, blocs, événements et commandes qu’il déclare font partie du mod, comme s’ils étaient dansmain.craft.import { give_rubies } from "/lib/rewards"importe des noms précis. Un chemin qui commence par/est lu depuis le dossier du mod, il fonctionne donc pareil depuis n’importe quel sous-dossier ; sans/, il est relatif au fichier qui importe. L’extension.craftest facultative.import "lib/rewards" as rewardsimporte tout le module sous un nom (rewards.give_rubies(p, 1)). Sansas, le nom est celui du fichier, donc deux fichiers importés qui portent le même nom s’écrasent : utilise des noms différents ouas.- Aucun chemin ne peut sortir du dossier du mod, et un import circulaire donne une erreur qui dit où.
Logique partagée
Une fonction utilisée par deux fichiers va dans lib/. Si la logique a son propre état (une économie, une file de quêtes), utilise une classe avec ses méthodes, elle aussi dans lib/ ; voir CraftScript. Une variable utilisée par un seul fichier reste dans ce fichier, mais n’oublie pas qu’elle est perdue au redémarrage : tout ce qui doit durer va dans les données enregistrées.
Réglages et données enregistrées dans leurs propres fichiers
Deux choses que le reste du mod utilise sans devoir en connaître les détails :
- Les réglages (
settings.craft, à la racine) : un seulconfig(…)avec toutes les valeurs par défaut. Les autres fichiers importentSETTINGSet lisent ce dont ils ont besoin ; aucun n’écrit en dur des nombres d’équilibrage. - Les données enregistrées (
lib/storage.craft) : les clés et quelques petites fonctions, commevisits(p)etadd_visit(p). Le code qui compte les visites appelleadd_visit(player)et ne sait pas que la clérubies.visitsexiste. Si elle change un jour, tu la changes à un seul endroit.
N’appelle pas ce fichier config.craft : importé sans as, son nom masquerait la fonction config.
En Kotlin
rubies/
├── build.gradle.kts
├── settings.gradle.kts
└── src/main/
├── kotlin/com/example/rubies/
│ ├── RubiesMod.kt entry point: only wires the parts
│ ├── RubiesConfig.kt player settings
│ ├── content/
│ │ ├── Items.kt
│ │ ├── Blocks.kt
│ │ ├── Creatures.kt
│ │ ├── Sounds.kt
│ │ ├── Particles.kt
│ │ └── Recipes.kt
│ ├── storage/
│ │ └── Visits.kt saved data: the keys, in one place
│ ├── events/
│ │ ├── MiningEvents.kt
│ │ ├── CombatEvents.kt
│ │ └── WelcomeEvents.kt
│ ├── ui/
│ │ └── ShopForm.kt forms: what the player sees
│ └── commands/
│ ├── RubyCommand.kt
│ └── ShopCommand.kt
└── resources/
├── crafting.mod.json
└── assets/rubies/
├── sounds/ruby/chime.ogg
└── textures/
├── item/ruby.png
├── entity/ruby_golem.png
└── particle/ruby_spark_0.png, ruby_spark_1.pngsrc/main/kotlin/com/example/rubies/
La classe d’entrée ne fait rien elle-même : elle crée ce qui est partagé et appelle chaque partie.
package com.example.rubies
class RubiesMod : CraftingMod {
override fun onLoad(ctx: ModContext) {
val config = Crafting.configs.load(RubiesConfig())
val content = ContentBuilder(ctx.metadata.id, Crafting.registries)
Items.register(content)
Blocks.register(content)
Creatures.register(content)
Sounds.register(content)
Particles.register(content)
Recipes.register(content)
MiningEvents.register(config)
CombatEvents.register(config)
WelcomeEvents.register()
RubyCommand.register()
ShopCommand.register()
}
}Les lignes import de chaque fichier sont omises pour la lisibilité.
- Des packages par thème :
content,events,commands. Unobjectpar fichier avec une fonctionregister. - La classe du mod ne fait que relier : elle crée ce que plusieurs parties partagent (la configuration, le
ContentBuilder) et le leur transmet. Aucune partie ne va chercher quoi que ce soit d’elle-même, tu peux donc comprendre chacune en lisant seulement son fichier. - Les ids en constantes dans l’objet qui les enregistre (
Items.RUBY). Une faute de frappe devient une erreur de compilation. - Les données enregistrées dans
storage/: chaque clé dans un objet avec des fonctions commeVisits.of(player)etVisits.add(player). Les réglages vont dans leur classeConfig, transmise à qui en a besoin.
En Java
La structure est la même, avec des classes au lieu d’object. Tu utilises l’API via Crafting.INSTANCE, et les écouteurs renvoient Unit.INSTANCE :
package com.example.rubies;
public final class RubiesMod implements CraftingMod {
@Override
public void onLoad(ModContext ctx) {
ContentBuilder content = new ContentBuilder(ctx.getMetadata().getId(), Crafting.INSTANCE.getRegistries());
Items.register(content);
WelcomeEvents.register();
}
}package com.example.rubies.events;
public final class WelcomeEvents {
public static void register() {
Crafting.INSTANCE.getEvents().subscribe(PlayerJoinEvent.class, EventPriority.NORMAL, false, null, e -> {
e.getPlayer().send("&aWelcome, " + e.getPlayer().getName() + "!");
return Unit.INSTANCE;
});
}
}Un exemple complet : Frostlands
Frostlands est un seul mod CraftScript organisé par parties, et il utilise presque tout ce que décrit ce guide. Son main.craft ne fait que lister les parties :
mod "frostlands" { name: "Frostlands", version: "1.0.0" }
// One file per part of the mod. Each part only declares what it is about.
import "content/materials" // frost crystals: their ore, the recipes made from them
import "content/gear" // armor, blade, wand, the frozen effect and the frost bolt
import "content/blocks" // the gate, a lamp, an ice chest, a frost pad
import "world/frostlands" // the frozen dimension
import "creatures/drake" // tame, ride and fly a frost drake
import "creatures/wisp" // wisps that shoot frost bolts at the undead
import "creatures/yak" // yaks to breed
import "controls/keys" // dash and roar keys
import "events/welcome" // a first greeting
import "commands/kit" // /frostkit for trying it all outcontent/materials: un cristal, son minerai et les recettes qui l’utilisent.content/gear: une armure, une lame qui gèle, une baguette qui tire un projectile du mod avec un temps de recharge, et un effet du mod.content/blocks: un portail, une lampe allumée par un clic ou par la redstone, un coffre de glace et un tremplin.world/frostlands: une dimension.creatures/drake: une créature avec son propre modèle et des animations fluides ; on peut l’apprivoiser, la monter et la faire voler, et elle rugit sur une touche.creatures/wisp: des feux follets qui tirent sur les morts-vivants.creatures/yak: des yaks qu’on peut élever.controls/keys: les touches d’élan et de rugissement.events/welcome: un premier message d’accueil.commands/kit: une commande pour tout essayer.
Un fichier de créature ne déclare que cette créature. Voici le drake :
creature "drake" {
body: "minecraft:bee"
model: "entity/drake"
texture: "entity/drake"
size: [1.4, 1.1]
health: 30
speed: 0.4
tame_item: "frostlands:frost_crystal"
rideable: true
behaviour: [{ follow_owner: 1.4, distance: 10 }, { fly_around: 1 }, { look_at_players: 8 }, "look_around"]
spawns: [{ biomes: ["minecraft:ice_spikes"], weight: 3, group: [1, 1] }]
}Voir Créatures pour chacune de ces propriétés.
Quand découper
| Signe | Que faire |
|---|---|
| Un fichier dépasse environ 150 lignes | Repère les thèmes qu’il mélange et sépare-les. |
| Tu copies la même fonction dans deux fichiers | Déplace-la dans lib/ et importe-la. |
| Un nombre d’équilibrage apparaît à plusieurs endroits | Un réglage dans settings.craft (ou dans la classe Config en Kotlin), ou une constante s’il ne doit pas être modifiable. |
| Tu écris la même clé de données dans plusieurs fichiers | Déplace-la dans lib/storage.craft avec deux fonctions pour lire et enregistrer. |
| Un événement fait trois choses différentes | Trois écouteurs pour le même événement, chacun dans le fichier de son thème. |