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:
| Mode | Behavior |
|---|---|
RegisterMode.GLOBAL | Registered once at mod init. Not tracked for reload. Survives all reloads. |
RegisterMode.WORLD | Only registered when join a world. In-world reload will not affect these registrations. Cleared after leaving a world. |
RegisterMode.RELOADABLE | Tracked 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:
beginReload()called on each registry — clears ownership tracking- Scripts re-execute —
ensureRegisteredreturns the same instance if already registered markManaged()re-tracks the entry for the current script- 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.
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.
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.
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.
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.
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.jsonin your resource pack to map the sound event to an actual audio file.
6. Particle Types
Add custom visual particles.
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).
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.
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.
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.
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.
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.
