Skip to content
On this page

Registry

Adding custom content to the game is the core of modding. Katton lets you register native Minecraft objects from Kotlin scripts, all hot-reloadable on Fabric and NeoForge.

WARNING

The registry system is actively developed. APIs are stable for RELOADABLE mode but may evolve. When in doubt, check the API docs.

IMPORTANT

Paper disables registry operations. A vanilla client connected to a Paper server cannot receive custom item, block, entity, or component registry entries. Use datapack mutations, Bukkit APIs, and events on Paper instead.

Register Modes

Every register function accepts a registerMode parameter:

ModeBehavior
RegisterMode.GLOBALRegistered once at mod init. Not tracked for reload. Survives all reloads.
RegisterMode.WORLDOnly registered when join a world. In-world reload will not affect these registrations. Cleared after leaving a world.
RegisterMode.RELOADABLETracked by Katton. Ownership is cleared on reload and the script can re-register. The Minecraft registry entry is preserved (soft-retained) to prevent holder crashes.

TIP

Use RELOADABLE for content you iterate on. Use GLOBAL for content that must persist unchanged across reloads.

Reload Lifecycle

Running /katton reload triggers this sequence:

  1. beginReload() called on each registry — clears ownership tracking
  2. Scripts re-execute — ensureRegistered returns the same instance if already registered
  3. markManaged() re-tracks the entry for the current script
  4. Stale entries (no longer registered by any script) remain in Minecraft's registry until restart

Run Registry Code on Both Sides

For built-in content on Fabric/NeoForge, the server registers at ServerPhase.READY and the client registers the matching IDs at ClientPhase.REGISTRY_SETUP. The examples below therefore put both annotations on the same no-argument function. The client phase runs before multiplayer registry validation.

Commands are server-only and use only ServerPhase.READY. Paper disables all registry examples on this page.

Registry Diagnostics

Use /katton registry to see a summary per registry: how many entries Katton tracks, how many are managed by scripts, and how many are stale (still in Minecraft's registry but no longer owned by any script).

Use /katton registry stale to show only registries with stale entries.

All Native APIs

1. Items

The most common registry call. You can register simple items, custom-behavior items, and food items.

kotlin
import net.minecraft.network.chat.Component
import net.minecraft.resources.Identifier
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.InteractionHand
import net.minecraft.world.InteractionResult
import net.minecraft.world.entity.player.Player
import net.minecraft.world.food.FoodProperties
import net.minecraft.world.item.Item
import net.minecraft.world.level.Level
import top.katton.api.ClientPhase
import top.katton.api.ClientScriptEntrypoint
import top.katton.api.ServerPhase
import top.katton.api.ServerScriptEntrypoint
import top.katton.api.dpcaller.tell
import top.katton.api.registry.registerNativeItem
import top.katton.registry.RegisterMode

// Built-in registry entries must be created on both logical sides.
@ServerScriptEntrypoint(ServerPhase.READY)
@ClientScriptEntrypoint(ClientPhase.REGISTRY_SETUP)
fun registerHelloItem() {
    registerNativeItem(
        id = "qwq:hello",
        registerMode = RegisterMode.RELOADABLE,
        configure = {
            setName(Component.literal("Hello"))
            stacksTo(1)
            setModel(Identifier.fromNamespaceAndPath("minecraft", "diamond"))
            food(FoodProperties.Builder().nutrition(1).saturationModifier(0.1f).build())
        }
    ) {
        // The item class itself is retained by Minecraft's registry. Delegate
        // behavior to a normal function so the implementation can be reloaded.
        object : Item(it) {
            override fun use(
                level: Level,
                player: Player,
                hand: InteractionHand
            ): InteractionResult = useHelloItem(player)
        }
    }
}

fun useHelloItem(player: Player): InteractionResult {
    (player as? ServerPlayer)?.let { tell(it, "Used the hello item!") }
    return InteractionResult.SUCCESS
}

CAUTION

When the client connects to a server, only item data components sync — not the Kotlin logic in your item class. Client-side interaction logic may need extra handling.

2. Blocks

Register a custom block with strength, tool requirements, and custom behavior.

kotlin
import net.minecraft.world.level.block.Block
import top.katton.api.ClientPhase
import top.katton.api.ClientScriptEntrypoint
import top.katton.api.ServerPhase
import top.katton.api.ServerScriptEntrypoint
import top.katton.api.registry.registerNativeBlock
import top.katton.registry.RegisterMode

@ServerScriptEntrypoint(ServerPhase.READY)
@ClientScriptEntrypoint(ClientPhase.REGISTRY_SETUP)
fun registerTestBlock() {
    registerNativeBlock(
        id = "qwq:test_block",
        registerMode = RegisterMode.RELOADABLE
    ) { props ->
        Block(
            props
                .strength(3.0f, 6.0f)
                .requiresCorrectToolForDrops()
        )
    }
}

NOTE

You need a blockstate JSON and a model JSON in your resource pack for the block to render properly.

3. Mob Effects

Create custom potion effects — beneficial or harmful.

kotlin
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.effect.MobEffect
import net.minecraft.world.effect.MobEffectCategory
import net.minecraft.world.entity.LivingEntity
import top.katton.api.ClientPhase
import top.katton.api.ClientScriptEntrypoint
import top.katton.api.ServerPhase
import top.katton.api.ServerScriptEntrypoint
import top.katton.api.registry.registerNativeEffect
import top.katton.registry.RegisterMode

@ServerScriptEntrypoint(ServerPhase.READY)
@ClientScriptEntrypoint(ClientPhase.REGISTRY_SETUP)
fun registerTestEffect() {
    registerNativeEffect(
        id = "qwq:test_qwq",
        registerMode = RegisterMode.RELOADABLE
    ) {
        object : MobEffect(MobEffectCategory.BENEFICIAL, 0x55FF55) {
            override fun applyEffectTick(
                serverLevel: ServerLevel,
                mob: LivingEntity,
                amplification: Int
            ): Boolean {
                mob.hurtServer(serverLevel, mob.damageSources().wither(), 1.0F)
                return super.applyEffectTick(serverLevel, mob, amplification)
            }

            override fun shouldApplyEffectTickThisTick(
                tickCount: Int,
                amplification: Int
            ): Boolean = tickCount % 20 == 0
        }
    }
}

4. Commands

Script-registered commands use ScriptCommandRegistry — they are auto-cleaned on reload. This is not a Minecraft built-in registry, but it follows the same reload ownership model.

kotlin
import com.mojang.brigadier.arguments.IntegerArgumentType.getInteger
import com.mojang.brigadier.arguments.IntegerArgumentType.integer
import com.mojang.brigadier.arguments.StringArgumentType.getString
import com.mojang.brigadier.arguments.StringArgumentType.word
import net.minecraft.commands.SharedSuggestionProvider.suggest
import net.minecraft.network.chat.Component
import top.katton.api.ServerPhase
import top.katton.api.ServerScriptEntrypoint
import top.katton.registry.registerCommand

@ServerScriptEntrypoint(ServerPhase.READY)
fun commandTest() {
    registerCommand("demo") {
        literal("ping") {
            executes { ctx ->
                ctx.source.sendSuccess(
                    { Component.literal("[demo] pong") },
                    false
                )
                1
            }
        }

        literal("echo") {
            argument("text", word()) {
                suggests { _, builder ->
                    suggest(listOf("hello", "world", "katton"), builder)
                }
                executes { ctx ->
                    val text = getString(ctx, "text")
                    ctx.source.sendSuccess(
                        { Component.literal("[demo] $text") },
                        false
                    )
                    1
                }
            }
        }

        literal("add") {
            argument("a", integer()) {
                argument("b", integer()) {
                    executes { ctx ->
                        val a = getInteger(ctx, "a")
                        val b = getInteger(ctx, "b")
                        ctx.source.sendSuccess(
                            { Component.literal("[demo] $a + $b = ${a + b}") },
                            false
                        )
                        1
                    }
                }
            }
        }
    }
}

5. Sound Events

Register custom sounds. Pair with a sounds.json in your resource pack.

kotlin
import top.katton.api.ClientPhase
import top.katton.api.ClientScriptEntrypoint
import top.katton.api.ServerPhase
import top.katton.api.ServerScriptEntrypoint
import top.katton.api.registry.registerNativeSoundEvent
import top.katton.api.registry.createVariableRangeSoundEvent
import top.katton.registry.RegisterMode

@ServerScriptEntrypoint(ServerPhase.READY)
@ClientScriptEntrypoint(ClientPhase.REGISTRY_SETUP)
fun main() {
    registerNativeSoundEvent(
        id = "mymod:my_sound",
        registerMode = RegisterMode.RELOADABLE
    ) {
        createVariableRangeSoundEvent("mymod:my_sound")
    }
}

You also need a sounds.json in your resource pack to map the sound event to an actual audio file.

6. Particle Types

Add custom visual particles.

kotlin
import net.minecraft.core.particles.SimpleParticleType
import top.katton.api.ClientPhase
import top.katton.api.ClientScriptEntrypoint
import top.katton.api.ServerPhase
import top.katton.api.ServerScriptEntrypoint
import top.katton.api.registry.registerNativeParticleType
import top.katton.registry.RegisterMode

@ServerScriptEntrypoint(ServerPhase.READY)
@ClientScriptEntrypoint(ClientPhase.REGISTRY_SETUP)
fun main() {
    registerNativeParticleType(
        id = "mymod:my_particle",
        registerMode = RegisterMode.RELOADABLE
    ) {
        object : SimpleParticleType(false) {}
    }
}

7. Block Entity Types

For blocks that store data (chests, furnaces, custom machines).

kotlin
import top.katton.api.ClientPhase
import top.katton.api.ClientScriptEntrypoint
import top.katton.api.ServerPhase
import top.katton.api.ServerScriptEntrypoint
import top.katton.api.registry.registerNativeBlockEntityType
import top.katton.registry.RegisterMode

@ServerScriptEntrypoint(ServerPhase.READY)
@ClientScriptEntrypoint(ClientPhase.REGISTRY_SETUP)
fun main() {
    // createMachineBlockEntityType() is project support code that returns
    // a fresh, unregistered BlockEntityType for MyBlockEntity and its block.
    // Its implementation is loader-specific; the Katton registration call is shared.
    registerNativeBlockEntityType(
        id = "mymod:my_block_entity",
        registerMode = RegisterMode.RELOADABLE
    ) {
        createMachineBlockEntityType()
    }
}

8. Creative Mode Tabs

Organize your items in the creative inventory.

kotlin
import net.minecraft.network.chat.Component
import net.minecraft.world.item.CreativeModeTab
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.Items
import top.katton.api.ClientPhase
import top.katton.api.ClientScriptEntrypoint
import top.katton.api.ServerPhase
import top.katton.api.ServerScriptEntrypoint
import top.katton.api.registry.registerNativeCreativeTab
import top.katton.registry.RegisterMode

@ServerScriptEntrypoint(ServerPhase.READY)
@ClientScriptEntrypoint(ClientPhase.REGISTRY_SETUP)
fun main() {
    registerNativeCreativeTab(
        id = "mymod:my_tab",
        registerMode = RegisterMode.RELOADABLE
    ) {
        CreativeModeTab.Builder(CreativeModeTab.Row.TOP, 0)
            .title(Component.literal("My Custom Tab"))
            .icon { ItemStack(Items.DIAMOND) }
            .displayItems { _, items ->
                items.accept(Items.DIAMOND)
                items.accept(Items.EMERALD)
            }
            .build()
    }
}

9. Data Component Types

Type-safe custom data on item stacks — think of it as structured NBT.

kotlin
import com.mojang.serialization.Codec
import top.katton.api.ClientPhase
import top.katton.api.ClientScriptEntrypoint
import top.katton.api.ServerPhase
import top.katton.api.ServerScriptEntrypoint
import top.katton.api.registry.registerNativePersistentDataComponentType
import top.katton.registry.RegisterMode

@ServerScriptEntrypoint(ServerPhase.READY)
@ClientScriptEntrypoint(ClientPhase.REGISTRY_SETUP)
fun main() {
    registerNativePersistentDataComponentType(
        id = "mymod:custom_data",
        registerMode = RegisterMode.RELOADABLE,
        codec = Codec.STRING
    )
}

Data components let you attach custom data to item stacks — like a built-in NBT but type-safe!

10. Entity Types (Basic)

Register a bare entity type — no attributes, no renderer, no spawn egg.

kotlin
import net.minecraft.core.registries.Registries
import net.minecraft.resources.Identifier
import net.minecraft.resources.ResourceKey
import net.minecraft.world.entity.EntityType
import net.minecraft.world.entity.Marker
import net.minecraft.world.entity.MobCategory
import top.katton.api.ClientPhase
import top.katton.api.ClientScriptEntrypoint
import top.katton.api.ServerPhase
import top.katton.api.ServerScriptEntrypoint
import top.katton.api.registry.registerNativeEntityType
import top.katton.registry.RegisterMode

@ServerScriptEntrypoint(ServerPhase.READY)
@ClientScriptEntrypoint(ClientPhase.REGISTRY_SETUP)
fun main() {
    // A minimal marker-based entity type with no attributes or spawn egg.
    registerNativeEntityType(
        id = "mymod:my_entity",
        registerMode = RegisterMode.RELOADABLE
    ) {
        val key = ResourceKey.create(
            Registries.ENTITY_TYPE,
            Identifier.parse("mymod:my_entity")
        )
        EntityType.Builder.of(::Marker, MobCategory.MISC)
            .sized(0.6f, 1.8f)
            .build(key)
    }
}

11. Entity Types (Full — with attributes + spawn egg)

Complete entity registration including attributes and spawn configuration.

kotlin
import net.minecraft.core.registries.Registries
import net.minecraft.resources.ResourceKey
import net.minecraft.world.entity.EntityType
import net.minecraft.world.entity.MobCategory
import net.minecraft.world.entity.SpawnPlacementTypes
import net.minecraft.world.entity.monster.zombie.Zombie
import top.katton.api.ClientPhase
import top.katton.api.ClientScriptEntrypoint
import top.katton.api.ServerPhase
import top.katton.api.ServerScriptEntrypoint
import top.katton.api.registry.registerNativeEntity
import top.katton.registry.RegisterMode

@ServerScriptEntrypoint(ServerPhase.READY)
@ClientScriptEntrypoint(ClientPhase.REGISTRY_SETUP)
fun main() {
    registerNativeEntity(
        id = "mymod:my_mob_full",
        registerMode = RegisterMode.RELOADABLE,
        configure = {
            dimensions(0.6f, 1.95f)
            category = MobCategory.MONSTER
            maxHealth(20.0)
            movementSpeed(0.23)
            attackDamage(3.0)
            followRange(35.0)
            withSpawnEgg()
            spawnPlacement(SpawnPlacementTypes.ON_GROUND)
        }
    ) { p ->
        val key = ResourceKey.create(Registries.ENTITY_TYPE, p.id)
        EntityType.Builder.of(::Zombie, p.category)
            .sized(p.dimensions.width, p.dimensions.height)
            .build(key)
    }
}

TIP

For a complete walkthrough of creating a custom animated entity with BlockBench models and animations, see the Entity Tutorial.