Guide

Organizzare una mod

Una mod la scrivi una volta e la mantieni per anni. Ciò che aiuta di più a mantenerla non è il linguaggio: è dove sta ogni cosa. Questa guida propone una struttura per CraftScript e una per Kotlin, e spiega perché.

Quattro regole

  1. Una responsabilità per file. Un file per i minerali, un altro per le ricette, un altro per ogni comando. Se per descrivere un file ti serve la parola “e”, probabilmente sono due file.
  2. Raggruppa per tema, non per dimensione. Cartelle come content/, events/, commands/, ui/ e lib/. Chiunque apra la mod sa dove guardare senza leggere il codice.
  3. Tieni i dati separati dal comportamento. Ciò che esiste (oggetti, blocchi, ricette) è un elenco di proprietà. Ciò che succede (eventi, comandi) è codice. Se li mescoli, per cambiare un numero devi leggere della logica.
  4. Un punto di ingresso che si limita a collegare le parti. main.craft o la classe della mod dicono quali parti esistono. Non contengono regole di gioco.

Così, ribilanciare la mod significa modificare content/recipes.craft; correggere un comando significa aprire commands/; e un bug in un evento non si nasconde tra duecento righe di blocchi.

In CraftScript

Struttura consigliataTesto
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/

Dichiara solo la mod ed elenca le sue parti. Leggendolo sai cosa c’è e dove.

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"

Come funzionano gli import

  • import "content/items" esegue quel file una volta. Gli oggetti, i blocchi, gli eventi e i comandi che dichiara diventano parte della mod, come se fossero in main.craft.
  • import { give_rubies } from "/lib/rewards" importa nomi specifici. Un percorso che inizia con / viene letto dalla cartella della mod, quindi funziona allo stesso modo da qualsiasi sottocartella; senza /, è relativo al file che importa. L’estensione .craft è facoltativa.
  • import "lib/rewards" as rewards importa l’intero modulo sotto un nome (rewards.give_rubies(p, 1)). Senza as, il nome è quello del file, quindi due file importati con lo stesso nome si sovrascrivono: usa nomi diversi oppure as.
  • Nessun percorso può uscire dalla cartella della mod, e un import circolare dà un errore che dice dove.

Logica condivisa

Una funzione usata da due file va in lib/. Se la logica ha un suo stato (un’economia, una coda di missioni), usa una classe con i suoi metodi, sempre in lib/; vedi CraftScript. Una variabile usata da un solo file resta in quel file, ma ricorda che si perde al riavvio: tutto ciò che deve durare va nei dati salvati.

Impostazioni e dati salvati in file propri

Due cose che il resto della mod usa ma di cui non dovrebbe conoscere i dettagli interni:

  • Impostazioni (settings.craft, nella radice): un solo config(…) con tutti i valori predefiniti. Gli altri file importano SETTINGS e leggono ciò che serve loro; nessuno scrive direttamente numeri di bilanciamento nel codice.
  • Dati salvati (lib/storage.craft): le chiavi e qualche piccola funzione, come visits(p) e add_visit(p). Il codice che conta le visite chiama add_visit(player) e non sa che esiste la chiave rubies.visits. Se un giorno cambia, la cambi in un solo posto.

Non chiamare quel file config.craft: importato senza as, il suo nome nasconderebbe la funzione config.

In Kotlin

Struttura consigliataTesto
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 di ingresso non fa nulla da sola: crea ciò che è condiviso e chiama ogni parte.

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()
    }
}

Le righe import di ogni file sono omesse per leggibilità.

  • Package per tema: content, events, commands. Un object per file con una funzione register.
  • La classe della mod si limita a collegare le parti: crea ciò che più parti condividono (la configurazione, il ContentBuilder) e lo passa loro. Nessuna parte va a cercare qualcosa da sola, quindi puoi capirne ognuna leggendo solo il suo file.
  • Id come costanti nell’object che li registra (Items.RUBY). Un errore di battitura diventa un errore di compilazione.
  • Dati salvati in storage/: ogni chiave in un object con funzioni come Visits.of(player) e Visits.add(player). Le impostazioni vanno nella loro classe Config, che viene passata a chi ne ha bisogno.

In Java

La struttura è la stessa, con classi al posto di object. Usi l’API tramite Crafting.INSTANCE, e i listener restituiscono 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 esempio completo: Frostlands

Frostlands è una sola mod CraftScript organizzata per parti, e usa quasi tutto ciò che descrive questa guida. Il suo main.craft elenca soltanto le parti:

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 cristallo, il suo minerale e le ricette che lo usano.
  • content/gear: un’armatura, una lama che congela, una bacchetta che lancia un proiettile della mod con un tempo di ricarica, e un effetto della mod.
  • content/blocks: un portale, una lampada accesa con un clic o dalla redstone, una cassa di ghiaccio e una piattaforma di salto.
  • world/frostlands: una dimensione.
  • creatures/drake: una creatura con un modello proprio e animazioni fluide; si può addomesticare, cavalcare e far volare, e ruggisce con un tasto.
  • creatures/wisp: fuochi fatui che sparano ai non morti.
  • creatures/yak: yak da allevare.
  • controls/keys: i tasti di scatto e di ruggito.
  • events/welcome: un primo saluto.
  • commands/kit: un comando per provare tutto.

Un file di creatura dichiara soltanto quella creatura. Ecco il 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] }]
}

Vedi Creature per ognuna di queste proprietà.

Quando dividere

Segnale Cosa fare
Un file supera circa 150 righe Trova i temi che mescola e separali.
Copi la stessa funzione in due file Spostala in lib/ e importala.
Un numero di bilanciamento compare in più punti Un’impostazione in settings.craft (o nella classe Config in Kotlin), o una costante se non deve essere modificabile.
Scrivi la stessa chiave di dati in più file Spostala in lib/storage.craft con due funzioni per leggere e salvare.
Un evento fa tre cose diverse Tre listener per lo stesso evento, ognuno nel file del suo tema.