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
- 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.
- Group by topic, not by size. Folders like
content/,events/,commands/,ui/andlib/. Anyone who opens the mod knows where to look without reading the code. - 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.
- An entry point that only wires things up.
main.craftor 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
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/
Only declares the mod and lists its parts. Reading it tells you what there is and where.
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 inmain.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.craftextension is optional.import "lib/rewards" as rewardsbrings in the whole module under a name (rewards.give_rubies(p, 1)). Withoutas, the name is the file name, so two imported files with the same name overwrite each other: use different names oras.- 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 singleconfig(…)with all the default values. Other files importSETTINGSand read what they need; none of them hard-code balance numbers. - Saved data (
lib/storage.craft): the keys and a few small functions, likevisits(p)andadd_visit(p). Code that counts visits callsadd_visit(player)and doesn't know therubies.visitskey 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
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/
The entry class does nothing by itself: it creates what is shared and calls each part.
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. Oneobjectper file with aregisterfunction. - 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 likeVisits.of(player)andVisits.add(player). Settings go in theirConfigclass, 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:
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;
});
}
}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:
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: 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:
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. |