Anleitungen
Eine Mod organisieren
Du schreibst eine Mod einmal und pflegst sie jahrelang. Was bei der Pflege am meisten hilft, ist nicht die Sprache, sondern wo alles liegt. Diese Anleitung schlägt eine Struktur für CraftScript und eine für Kotlin vor und erklärt, warum.
Vier Regeln
- Eine Aufgabe pro Datei. Eine Datei für Erze, eine für Rezepte, eine für jeden Befehl. Brauchst du das Wort „und“, um eine Datei zu beschreiben, sind es wahrscheinlich zwei Dateien.
- Nach Thema gruppieren, nicht nach Größe. Ordner wie
content/,events/,commands/,ui/undlib/. Wer die Mod öffnet, weiß, wo er suchen muss, ohne den Code zu lesen. - Daten vom Verhalten trennen. Was es gibt (Gegenstände, Blöcke, Rezepte), ist eine Liste von Eigenschaften. Was passiert (Events, Befehle), ist Code. Vermischst du beides, musst du Logik lesen, um eine Zahl zu ändern.
- Ein Einstiegspunkt, der nur verbindet.
main.craftoder die Mod-Klasse sagen, welche Teile es gibt. Sie enthalten keine Spielregeln.
So bedeutet ein neues Balancing der Mod, content/recipes.craft zu bearbeiten, einen Befehl zu reparieren heißt, commands/ zu öffnen, und ein Fehler in einem Event versteckt sich nicht zwischen zweihundert Zeilen Blöcken.
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/
Deklariert nur die Mod und listet ihre Teile auf. Beim Lesen weißt du, was es gibt und wo.
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"Wie Imports funktionieren
import "content/items"führt diese Datei einmal aus. Die Gegenstände, Blöcke, Events und Befehle, die sie deklariert, werden Teil der Mod, als stünden sie inmain.craft.import { give_rubies } from "/lib/rewards"holt bestimmte Namen herein. Ein Pfad, der mit/beginnt, wird vom Mod-Ordner aus gelesen und funktioniert daher aus jedem Unterordner gleich. Ohne/ist er relativ zur importierenden Datei. Die Endung.craftist optional.import "lib/rewards" as rewardsholt das ganze Modul unter einem Namen herein (rewards.give_rubies(p, 1)). Ohneasist der Name der Dateiname, also überschreiben sich zwei importierte Dateien mit demselben Namen: Nimm verschiedene Namen oderas.- Kein Pfad kann den Mod-Ordner verlassen, und ein zirkulärer Import gibt einen Fehler, der sagt, wo.
Gemeinsame Logik
Eine Funktion, die zwei Dateien nutzen, gehört nach lib/. Hat die Logik einen eigenen Zustand (eine Wirtschaft, eine Quest-Warteschlange), nimm eine Klasse mit ihren Methoden, ebenfalls in lib/, siehe CraftScript. Eine Variable, die nur eine Datei nutzt, bleibt in dieser Datei, aber denk daran, dass sie beim Neustart verloren geht: Was bleiben muss, gehört in die Speicherdaten.
Einstellungen und Speicherdaten in eigenen Dateien
Zwei Dinge, die der Rest der Mod nutzt, deren Innenleben er aber nicht kennen sollte:
- Einstellungen (
settings.craft, im Hauptordner): ein einzigesconfig(…)mit allen Standardwerten. Andere Dateien importierenSETTINGSund lesen, was sie brauchen. Keine von ihnen schreibt Balancing-Zahlen fest in den Code. - Speicherdaten (
lib/storage.craft): die Schlüssel und ein paar kleine Funktionen wievisits(p)undadd_visit(p). Code, der Besuche zählt, ruftadd_visit(player)auf und weiß nicht, dass es den Schlüsselrubies.visitsgibt. Ändert er sich eines Tages, änderst du ihn an einer Stelle.
Nenn diese Datei nicht config.craft: Ohne as importiert, würde ihr Name die Funktion config verdecken.
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/
Die Einstiegsklasse tut selbst nichts: Sie erstellt das Gemeinsame und ruft jeden Teil auf.
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()
}
}Die import-Zeilen jeder Datei sind der Lesbarkeit halber weggelassen.
- Pakete nach Thema:
content,events,commands. Einobjectpro Datei mit einer Funktionregister. - Die Mod-Klasse verbindet nur: Sie erstellt, was mehrere Teile gemeinsam nutzen (die Konfiguration, den
ContentBuilder), und gibt es ihnen weiter. Kein Teil holt sich selbst etwas, also verstehst du jeden, indem du nur seine Datei liest. - IDs als Konstanten in dem Objekt, das sie registriert (
Items.RUBY). Ein Tippfehler wird zum Kompilierfehler. - Speicherdaten in
storage/: jeder Schlüssel in einem Objekt mit Funktionen wieVisits.of(player)undVisits.add(player). Einstellungen kommen in ihre KlasseConfig, die an alle weitergegeben wird, die sie brauchen.
In Java
Die Struktur ist dieselbe, mit Klassen statt object. Du nutzt die API über Crafting.INSTANCE, und Listener geben Unit.INSTANCE zurück:
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;
});
}
}Ein vollständiges Beispiel: Frostlands
Frostlands ist ein einzelner CraftScript-Mod, nach Teilen geordnet, und nutzt das meiste von dem, was dieser Leitfaden beschreibt. Seine main.craft listet nur die Teile auf:
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: ein Kristall, sein Erz und die Rezepte daraus.content/gear: Rüstung, eine Klinge, die einfriert, ein Stab, der mit Abklingzeit ein Mod-Projektil schießt, und ein Mod-Effekt.content/blocks: ein Portal, eine Lampe, die per Klick oder per Redstone leuchtet, eine Eistruhe und ein Sprungfeld.world/frostlands: eine Dimension.creatures/drake: eine Kreatur mit eigenem Modell und weichen Animationen; man kann sie zähmen, reiten und fliegen, und sie brüllt auf Tastendruck.creatures/wisp: Irrlichter, die auf Untote schießen.creatures/yak: Yaks, die sich vermehren.controls/keys: die Tasten für Sprint und Brüllen.events/welcome: eine erste Begrüßung.commands/kit: ein Befehl, um alles auszuprobieren.
Eine Kreaturdatei deklariert nur diese Kreatur. Das ist der Drache:
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] }]
}Zu jeder dieser Eigenschaften siehe Kreaturen.
Wann aufteilen
| Anzeichen | Was zu tun ist |
|---|---|
| Eine Datei wird länger als etwa 150 Zeilen | Finde die Themen, die sie vermischt, und trenne sie. |
| Du kopierst dieselbe Funktion in zwei Dateien | Verschieb sie nach lib/ und importiere sie. |
| Eine Balancing-Zahl taucht an mehreren Stellen auf | Eine Einstellung in settings.craft (oder in der Klasse Config in Kotlin), oder eine Konstante, wenn sie nicht änderbar sein soll. |
| Du schreibst denselben Datenschlüssel in mehrere Dateien | Verschieb ihn nach lib/storage.craft, mit zwei Funktionen zum Lesen und Speichern. |
| Ein Event macht drei verschiedene Dinge | Drei Listener für dasselbe Event, jeder in der Datei seines Themas. |