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

  1. 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.
  2. Regroupe par thème, pas par taille. Des dossiers comme content/, events/, commands/, ui/ et lib/. Quiconque ouvre le mod sait où chercher sans lire le code.
  3. 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.
  4. Un point d’entrée qui ne fait que relier. main.craft ou 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

Structure recommandéeTexte
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.png

mods/rubies/

Déclare seulement le mod et liste ses parties. En le lisant, tu sais ce qu’il y a et où.

mods/rubies/main.craftCraftScript
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 dans main.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 .craft est facultative.
  • import "lib/rewards" as rewards importe tout le module sous un nom (rewards.give_rubies(p, 1)). Sans as, le nom est celui du fichier, donc deux fichiers importés qui portent le même nom s’écrasent : utilise des noms différents ou as.
  • 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 seul config(…) avec toutes les valeurs par défaut. Les autres fichiers importent SETTINGS et 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, comme visits(p) et add_visit(p). Le code qui compte les visites appelle add_visit(player) et ne sait pas que la clé rubies.visits existe. 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

Structure recommandéeTexte
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.png

src/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.

src/main/kotlin/com/example/rubies/RubiesMod.ktKotlin
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. Un object par fichier avec une fonction register.
  • 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 comme Visits.of(player) et Visits.add(player). Les réglages vont dans leur classe Config, 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 :

src/main/java/com/example/rubies/RubiesMod.javaJava
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();
    }
}
src/main/java/com/example/rubies/events/WelcomeEvents.javaJava
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 :

mods/frostlands/main.craftCraftScript
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 out
  • content/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 :

mods/frostlands/creatures/drake.craftCraftScript
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.