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

  1. 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.
  2. Nach Thema gruppieren, nicht nach Größe. Ordner wie content/, events/, commands/, ui/ und lib/. Wer die Mod öffnet, weiß, wo er suchen muss, ohne den Code zu lesen.
  3. 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.
  4. Ein Einstiegspunkt, der nur verbindet. main.craft oder 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

Empfohlene StrukturText
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/

Deklariert nur die Mod und listet ihre Teile auf. Beim Lesen weißt du, was es gibt und wo.

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"

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 in main.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 .craft ist optional.
  • import "lib/rewards" as rewards holt das ganze Modul unter einem Namen herein (rewards.give_rubies(p, 1)). Ohne as ist der Name der Dateiname, also überschreiben sich zwei importierte Dateien mit demselben Namen: Nimm verschiedene Namen oder as.
  • 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 einziges config(…) mit allen Standardwerten. Andere Dateien importieren SETTINGS und 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 wie visits(p) und add_visit(p). Code, der Besuche zählt, ruft add_visit(player) auf und weiß nicht, dass es den Schlüssel rubies.visits gibt. Ä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

Empfohlene StrukturText
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/

Die Einstiegsklasse tut selbst nichts: Sie erstellt das Gemeinsame und ruft jeden Teil auf.

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

Die import-Zeilen jeder Datei sind der Lesbarkeit halber weggelassen.

  • Pakete nach Thema: content, events, commands. Ein object pro Datei mit einer Funktion register.
  • 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 wie Visits.of(player) und Visits.add(player). Einstellungen kommen in ihre Klasse Config, 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:

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

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:

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: 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:

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] }]
}

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.