Guides

Organizing a mod

You write a mod once and maintain it for years. What helps most with maintenance isn't the language: it's where everything lives. This guide suggests one structure for CraftScript and one for Kotlin, and explains why.

Four rules

  1. One responsibility per file. One file for ores, another for recipes, another for each command. If you need the word “and” to describe a file, it's probably two files.
  2. Group by topic, not by size. Folders like content/, events/, commands/, ui/ and lib/. Anyone who opens the mod knows where to look without reading the code.
  3. Keep data apart from behavior. What exists (items, blocks, recipes) is a list of properties. What happens (events, commands) is code. Mixing them means changing a number forces you to read logic.
  4. An entry point that only wires things up. main.craft or the mod class say which parts exist. They don't contain game rules.

With this, rebalancing the mod means editing content/recipes.craft; fixing a command means opening commands/; and a bug in an event isn't hidden among two hundred lines of blocks.

In CraftScript

Recommended structureText
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/

Only declares the mod and lists its parts. Reading it tells you what there is and where.

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"

How imports work

  • import "content/items" runs that file once. The items, blocks, events and commands it declares become part of the mod, as if they were in main.craft.
  • import { give_rubies } from "/lib/rewards" brings in specific names. A path that starts with / is read from the mod folder, so it works the same from any subfolder; without /, it's relative to the importing file. The .craft extension is optional.
  • import "lib/rewards" as rewards brings in the whole module under a name (rewards.give_rubies(p, 1)). Without as, the name is the file name, so two imported files with the same name overwrite each other: use different names or as.
  • No path can leave the mod folder, and a circular import gives an error that says where.

Shared logic

A function used by two files goes in lib/. If the logic has its own state (an economy, a quest queue), use a class with its methods, also in lib/; see CraftScript. A variable used by only one file stays in that file, but remember it's lost on restart: anything that must last goes in saved data.

Settings and saved data in their own files

Two things the rest of the mod uses but shouldn't know the internals of:

  • Settings (settings.craft, at the root): a single config(…) with all the default values. Other files import SETTINGS and read what they need; none of them hard-code balance numbers.
  • Saved data (lib/storage.craft): the keys and a few small functions, like visits(p) and add_visit(p). Code that counts visits calls add_visit(player) and doesn't know the rubies.visits key exists. If it changes one day, you change it in one place.

Don't name that file config.craft: when imported without as, its name would shadow the config function.

In Kotlin

Recommended structureText
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/

The entry class does nothing by itself: it creates what is shared and calls each part.

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

Each file's import lines are omitted for readability.

  • Packages by topic: content, events, commands. One object per file with a register function.
  • The mod class only wires things up: it creates what several parts share (the configuration, the ContentBuilder) and passes it to them. No part fetches anything on its own, so you can understand each one by reading only its file.
  • Ids as constants in the object that registers them (Items.RUBY). A typo becomes a compile error.
  • Saved data in storage/: each key in an object with functions like Visits.of(player) and Visits.add(player). Settings go in their Config class, which is passed to whoever needs them.

In Java

The structure is the same, with classes instead of object. You use the API through Crafting.INSTANCE, and listeners return 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;
        });
    }
}

A complete example: Frostlands

Frostlands is a single CraftScript mod organized by part, and it uses most of what this guide describes. Its main.craft only lists the parts:

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: a crystal, its ore and the recipes made from it.
  • content/gear: armor, a freezing blade, a wand that shoots a mod projectile with a cooldown, and a mod effect.
  • content/blocks: a portal gate, a lamp lit by a click or by redstone, an ice chest and a jump pad.
  • world/frostlands: a dimension.
  • creatures/drake: a creature with its own model and smooth animations; you can tame it, ride it and fly it, and it roars on a key.
  • creatures/wisp: wisps that shoot the undead.
  • creatures/yak: yaks that breed.
  • controls/keys: the dash and roar keys.
  • events/welcome: a first greeting.
  • commands/kit: a command to try everything out.

A creature file declares only that creature. This is the 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] }]
}

See Creatures for each of those properties.

When to split

Sign What to do
A file goes past about 150 lines Find the topics it mixes and separate them.
You copy the same function into two files Move it to lib/ and import it.
A balance number appears in several places A setting in settings.craft (or in the Config class in Kotlin), or a constant if it shouldn't be changeable.
You write the same data key in several files Move it to lib/storage.craft with two functions to read and save.
An event does three different things Three listeners for the same event, each in its topic's file.