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
- 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.
- Raggruppa per tema, non per dimensione. Cartelle come
content/,events/,commands/,ui/elib/. Chiunque apra la mod sa dove guardare senza leggere il codice. - 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.
- Un punto di ingresso che si limita a collegare le parti.
main.crafto 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
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/
Dichiara solo la mod ed elenca le sue parti. Leggendolo sai cosa c’è e dove.
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 inmain.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 rewardsimporta l’intero modulo sotto un nome (rewards.give_rubies(p, 1)). Senzaas, il nome è quello del file, quindi due file importati con lo stesso nome si sovrascrivono: usa nomi diversi oppureas.- 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 soloconfig(…)con tutti i valori predefiniti. Gli altri file importanoSETTINGSe leggono ciò che serve loro; nessuno scrive direttamente numeri di bilanciamento nel codice. - Dati salvati (
lib/storage.craft): le chiavi e qualche piccola funzione, comevisits(p)eadd_visit(p). Il codice che conta le visite chiamaadd_visit(player)e non sa che esiste la chiaverubies.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
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 di ingresso non fa nulla da sola: crea ciò che è condiviso e chiama ogni parte.
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. Unobjectper file con una funzioneregister. - 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 comeVisits.of(player)eVisits.add(player). Le impostazioni vanno nella loro classeConfig, 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:
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 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:
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 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:
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. |