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

Terminal
crafting new rubies --kind kotlin
cd rubies && gradle build

The 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.

Terminal
cd crafting && ./gradlew :crafting-api:publishToMavenLocal
build.gradle.ktsKotlin
plugins { 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 25

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

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

The manifest

src/main/resources/crafting.mod.jsonJSON
{
  "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

dev.crafting.api.mod.CraftingModKotlin
interface CraftingMod {
    fun onLoad(ctx: ModContext)
    fun onReady(ctx: ModContext) {}
    fun onShutdown(ctx: ModContext) {}
}
  1. onLoad: register content, events and commands.
  2. Crafting creates the blocks and items in the game. From this point on, registering content throws RegistryFrozenException.
  3. onReady: all mods are loaded; Crafting.runtime.isModLoaded("other_mod") tells you whether another mod is present.
  4. 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.

Terminal
crafting new chatguard --kind plugin
src/main/kotlin/com/example/chatguard/ChatGuardPlugin.ktKotlin
package 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")
}
src/main/kotlin/com/example/chatguard/events/ChatFilter.ktKotlin
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:

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

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 runAsync task doesn't touch the world; go back to the game thread with runSync.