Skip to content
On this page

Registry

Adding custom content to the game is the core of modding. Katton lets you register nine types of native Minecraft objects — all from Kotlin scripts, all hot-reloadable.

WARNING

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

Register Modes

Every register function accepts a registerMode parameter:

ModeBehavior
RegisterMode.GLOBALRegistered once at mod init. Not tracked for reload. Survives all reloads.
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.
RegisterMode.AUTOActs as GLOBAL during mod init, then RELOADABLE after server starts. Use this for "just works" behavior.

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

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 Types

1. Items

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

kotlin
registerNativeItem(
    id = "qwq:hello",     // The identifier of the item, must be unique across all mods
    registerMode = RegisterMode.RELOADABLE, // TODO
    configure = {
        // Configure the item properties
        // For datapack developers, you can think of it as data components
        setName(Component.literal("Hello"))
        stacksTo(1)
        setModel(Identifier.fromNamespaceAndPath("minecraft", "diamond"))
        food(FoodProperties.Builder().nutrition(1).saturationModifier(0.1f).build())
    }
) {
    // Here to define the item class, just do what you will do in a normal mod.
    object : Item(it) {
        // Executed when the player uses the item
        override fun use(level: Level, player: Player, hand: InteractionHand): InteractionResult {
            // Note that the method defined here won't be hot-reloadable
            // Wrap the logic you want to hot-reload in a separate function
            return useHelloItem(player)
        }
    }
}

fun useHelloItem(player: Player): InteractionResult{
    (player as? ServerPlayer)?.let { p -> tell(p , "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
 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
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 {
            return tickCount % 20 == 0
        }
    }
}

4. Commands

Script-registered commands use ScriptCommandRegistry — they're auto-cleaned on reload.

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.registry.registerCommand

@ServerScriptEntrypoint
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.registry.registerNativeSoundEvent
import top.katton.api.registry.createVariableRangeSoundEvent
import top.katton.registry.RegisterMode
import top.katton.api.ServerScriptEntrypoint

@ServerScriptEntrypoint
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.registry.registerNativeParticleType
import top.katton.registry.RegisterMode
import top.katton.api.ServerScriptEntrypoint

@ServerScriptEntrypoint
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 net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.entity.BlockEntityType
import top.katton.api.registry.registerNativeBlockEntityType
import top.katton.registry.RegisterMode
import top.katton.api.ServerScriptEntrypoint

@ServerScriptEntrypoint
fun main() {
    // Register a block entity type — you'll typically pair it with a custom BlockEntity class
    registerNativeBlockEntityType(
        id = "mymod:my_block_entity",
        registerMode = RegisterMode.RELOADABLE
    ) {
        BlockEntityType.Builder.of(
            ::MyBlockEntity,  // your BlockEntity class
            MyBlocks.MY_BLOCK // the block(s) that host this entity
        ).build(null)
    }
}

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.registry.registerNativeCreativeTab
import top.katton.registry.RegisterMode
import top.katton.api.ServerScriptEntrypoint

@ServerScriptEntrypoint
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 net.minecraft.core.component.DataComponentType
import top.katton.api.registry.registerNativeDataComponentType
import top.katton.registry.RegisterMode
import top.katton.api.ServerScriptEntrypoint

@ServerScriptEntrypoint
fun main() {
    registerNativeDataComponentType<String>(
        id = "mymod:custom_data",
        registerMode = RegisterMode.RELOADABLE
    ) {
        DataComponentType.builder<String>()
            .persistent(Codec.STRING)
            .build()
    }
}

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.world.entity.EntityType
import net.minecraft.world.entity.MobCategory
import top.katton.api.registry.registerNativeEntityType
import top.katton.registry.RegisterMode
import top.katton.api.ServerScriptEntrypoint

@ServerScriptEntrypoint
fun main() {
    // Simple entity type registration (no custom attributes/behavior)
    registerNativeEntityType(
        id = "mymod:my_entity",
        registerMode = RegisterMode.RELOADABLE
    ) {
        EntityType.Builder.of(::MyEntity, MobCategory.CREATURE)
            .sized(0.6f, 1.8f)
            .build("mymod:my_entity")
    }
}

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

Complete entity registration including attributes and spawn configuration.

kotlin
import net.minecraft.world.entity.EntityType
import net.minecraft.world.entity.MobCategory
import net.minecraft.world.entity.ai.attributes.Attributes
import net.minecraft.world.entity.monster.zombie.Zombie
import net.minecraft.world.level.levelgen.Heightmap
import net.minecraft.world.entity.SpawnPlacementType
import top.katton.api.registry.registerNativeEntityTypeWithProperties
import top.katton.registry.KattonEntityProperties
import top.katton.registry.RegisterMode
import top.katton.api.ServerScriptEntrypoint

@ServerScriptEntrypoint
fun main() {
    val props = KattonEntityProperties(id("mymod:my_mob_full")).apply {
        // Set entity dimensions
        dimensions(0.6f, 1.95f)
        // Set mob category
        mobCategory = MobCategory.MONSTER
        // Configure attributes (health, speed, damage, etc.)
        attributes {
            it.add(Attributes.MAX_HEALTH, 20.0)
            it.add(Attributes.MOVEMENT_SPEED, 0.23)
            it.add(Attributes.ATTACK_DAMAGE, 3.0)
            it.add(Attributes.FOLLOW_RANGE, 35.0)
        }
        // Enable spawn egg in creative inventory
        spawnEgg = true
        // Configure natural spawn placement
        spawnPlacementType = SpawnPlacementType.ON_GROUND
        spawnHeightmap = Heightmap.Types.MOTION_BLOCKING_NO_LEAVES
    }

    registerNativeEntityTypeWithProperties(
        id = "mymod:my_mob_full",
        registerMode = RegisterMode.RELOADABLE,
        properties = props
    ) { p ->
        EntityType.Builder.of(::Zombie, MobCategory.MONSTER)
            .sized(p.width, p.height)
            .build("mymod:my_mob_full")
    }
}

TIP

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