Guides
Kotlin and Java
For large mods. The same API as CraftScript, with static types, the full IDE and any Java library, plus a few parts that only exist here for now. This guide uses Kotlin; Java works the same way and is shown at the end.
What you can do
| Area | In the API | In CraftScript |
|---|---|---|
| Items, blocks, recipes | ContentBuilder: item, block, shaped, shapeless, smelting |
Yes |
| Creatures | ContentBuilder.creature: body, texture, stats, Behaviour, TargetRule, loot, natural spawning, taming, breeding, riding, custom behaviours |
Yes |
| Sounds and particles | ContentBuilder.sound, ContentBuilder.particle |
Yes |
| World generation | ContentBuilder: ore, tree, patch, structure (pools, plan/build), biome, dimension, feature, terrain, biomes, carver, tag. See World generation and Generation by code |
All but feature, tag, salt and aboveBottom |
| Interface | Form and player.showForm; server.bossBar; player.setSidebar |
Yes |
| Events | Crafting.events.on<E> { }, with priority and cancellation |
Yes |
| Effects, attributes, equipment | addEffect, attribute, setAttribute, equipment, setEquipment, EquipmentSlot |
Yes |
| Keys | ContentBuilder.key, PlayerKeyEvent |
Yes |
| Commands | Crafting.commands.command(…): subcommands, typed arguments, permissions, aliases |
Yes |
| World and players | World, Block, Entity, Player, Inventory, Server |
Yes |
| Tasks | Crafting.scheduler: runLater, runRepeating, runAsync, runSync |
No background work |
| File settings | Typed Config in TOML, can sync with clients |
Yes (config) |
| Networking | Crafting.network.channel(…), with custom codecs |
Yes (listen, send) |
| Saved data | player.data["rubies.visits"] = n, data.getInt(…); also on entities, blocks and worlds |
Yes |
| Java libraries | Any you want | No |
What the API doesn't have yet doesn't exist in any language. Don't work around it by calling Minecraft classes: the mod would lose the guarantee that it keeps working when the game changes.
Create the project
crafting new rubies --kind kotlin
cd rubies && gradle buildThe API isn't in a public Maven repository yet. Install it into your local Maven from the Crafting source and declare it as compileOnly: Crafting provides it to the mod at runtime.
cd crafting && ./gradlew :crafting-api:publishToMavenLocalplugins { kotlin("jvm") version "2.4.21" }
repositories { mavenCentral(); mavenLocal() }
dependencies {
compileOnly("dev.crafting:crafting-api:0.1.0")
}
kotlin { jvmToolchain(25) } // Minecraft 26.x runs on Java 25crafting new generates this file and a project already split by responsibility: <Name>Mod.kt, which only wires the parts together, content/ModContent.kt, events/Welcome.kt and commands/GemCommand.kt.
Structure
Packages by topic, an entry class that only wires the parts together, and one file per part. The Organizing a mod guide explains why; here is the full example mod:
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()
}
}The manifest
{
"id": "rubies",
"name": "Rubies",
"version": "1.0.0",
"kind": "mod",
"side": "both",
"entrypoints": ["com.example.rubies.RubiesMod"],
"depends": { "crafting": ">=0.1.0" }
}| Field | What it is |
|---|---|
id |
2–64 characters: lowercase letters, digits, _ or -, starting with a letter. It's also the namespace for your items and blocks. |
kind |
mod or plugin (server only, reloadable; see below). |
side |
both, client or server. A client mod is silently ignored on a dedicated server. |
entrypoints |
The classes that implement CraftingMod. |
depends, optional, incompatible |
Id → version range: >=1.2 <2, ^1.4, ~1.4.2, 1.20.x, [1.0,2.0). If something fails, Crafting reports every problem at once, with a suggestion. |
loadAfter, loadBefore |
Load order without a hard dependency. |
Lifecycle
interface CraftingMod {
fun onLoad(ctx: ModContext)
fun onReady(ctx: ModContext) {}
fun onShutdown(ctx: ModContext) {}
}onLoad: register content, events and commands.- Crafting creates the blocks and items in the game. From this point on, registering content throws
RegistryFrozenException. onReady: all mods are loaded;Crafting.runtime.isModLoaded("other_mod")tells you whether another mod is present.onShutdown: on exit.
Server plugins
A plugin is a CraftingMod with "kind": "plugin" and "side": "server"; crafting new chatguard --kind plugin creates the entry class, events/ChatFilter.kt and commands/HelloCommand.kt. It doesn't add blocks or items, so players with unmodded Minecraft can join a server that only has plugins. You enable, disable and reload it without restarting: onLoad and onReady run when it's enabled, onShutdown when it's disabled or reloaded.
crafting new chatguard --kind pluginpackage com.example.chatguard
class ChatGuardPlugin : CraftingMod {
override fun onLoad(ctx: ModContext) {
ChatFilter.register()
Crafting.commands.command("guard") {
permission = "chatguard.admin"
executes { it.reply("ChatGuard is active") }
}
}
override fun onShutdown(ctx: ModContext) = ctx.logger.info("ChatGuard disabled")
}package com.example.chatguard.events
object ChatFilter {
private val blocked = listOf("spam", "scam")
fun register() {
Crafting.events.on<PlayerChatEvent> { e ->
if (blocked.any { it in e.message.lowercase() }) {
e.cancel()
e.player.send("&cPlease keep it friendly")
}
}
}
}Everything a plugin registers is tied to it. /crafting reload chatguard calls onShutdown, removes its listeners, commands and tasks, reads the jar again and calls onLoad. In development mode this happens automatically when you replace the jar.
| Mod | Plugin | |
|---|---|---|
| Folder | mods/ |
plugins/ |
| Side | Client and/or server | Server only |
| New blocks and items | Yes | No |
| Reload without restart | No | Yes |
| Unmodded clients can join | No, if it adds content | Yes |
Java
Same API, same structure. From Java, the entry point is Crafting.INSTANCE, you pass every parameter that has a default value, and a listener returns 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;
});
}
}Old ways
They still work, but they're marked as deprecated and have a replacement:
| Before | Now |
|---|---|
class X : CraftingPlugin() with onEnable/onDisable |
CraftingMod with "kind": "plugin" |
Methods with @Subscribe and events.register(obj) |
events.on<E> { } (Java: events.subscribe(E.class, …)) |
@Mod("id") on the class |
The class in the manifest's entrypoints |
EntityDamageEvent.source |
damageType (an id, such as minecraft:fall) |
world.setBlockState(x, y, z, "…[open=true]") |
world.setBlock(x, y, z, type, mapOf("open" to true)) |
Rules
- Don't import anything from
net.minecraft. That's what lets the mod keep working when the game changes. - Register content only in
onLoad. - A
runAsynctask doesn't touch the world; go back to the game thread withrunSync.