Katton is a Minecraft Fabric mod that brings Kotlin scripting to datapacks. It lets you interact directly with Minecraft server internals and Fabric APIs from scripts, making it easy to build custom mechanics, commands, and items with Kotlin's concise syntax. Katton also supports hot reloading (similar to vanilla functions), so you can iterate quickly without restarting the server.
- Basic Kotlin script support
- Script hot reloading
- Remote JVM debugging support
- Simple APIs for common tasks (registering commands, items)
- Documentation and usage examples
Warning
Katton is still in early development. Bugs and incomplete features are expected. Feedback and contributions are welcome.
Add Katton to your Fabric modpack, then create a datapack in your world's datapacks directory. Inside your datapack namespace folder, create a scripts subdirectory and place your Kotlin script files there. Katton compiles these scripts automatically when the datapack is loaded.
A ready-to-use example project (with dependencies and basic configuration) is available at Katton-Example.
In IDEs such as IntelliJ IDEA, you may see unresolved references for Minecraft/Fabric classes because those types are provided by the game runtime. To fix this and enable completion, create a minimal Gradle project for script development.
Note
Even though these are "Kotlin Script" files, using the .kt extension usually provides better IDE support. So Katton only processes .kt files as scripts, and you can use regular Kotlin syntax without worrying about script-specific limitations.
Some IDE inspections may complain about top-level statements in .kt files. In that case, use fun main() as the script entry point, move your top-level logic into main, and invoke it at the end of the file with val __entrypoint__ = main().
Katton supports debugging datapack Kotlin scripts through standard JVM remote debugging.
-
Start Minecraft (or the dedicated server) with a debug agent, for example:
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -
In IntelliJ IDEA, create an Attach to remote JVM run configuration and connect to the same host and port.
-
Set breakpoints in the actual datapack script file (for example,
data/<namespace>/scripts/*.kt). -
Enjoy debugging your scripts with the IDE's standard debugging tools.
Note
- A successful debugger attachment does not guarantee that script breakpoints will be hit; source mapping must match the runtime-compiled script source name.
- When the same script is executed again, Katton replaces event handlers previously registered by that script owner, preventing duplicate registrations during reload/debug iteration.
- If breakpoints still do not trigger, restart the target JVM process and attach again to avoid stale classes from previous runs.
Katton supports registering native Minecraft Items from scripts with hot-reload capability. Items registered this way have the same capabilities as items registered by regular Fabric mods.
Use registerNativeItem to register a custom Item:
import net.minecraft.world.item.Item
import top.katton.api.*
registerNativeItem(
id = "mymod:custom_item",
registerMode = RegisterMode.RELOADABLE,
configure = {
stacksTo(64) // max stack size
}
) {
Item(it) // 'it' is the configured Item.Properties
}To create an item with custom behavior (like right-click actions), create an anonymous object extending Item:
import net.minecraft.world.item.Item
import net.minecraft.world.InteractionResult
import net.minecraft.world.InteractionHand
import net.minecraft.world.entity.player.Player
import net.minecraft.world.level.Level
import top.katton.api.*
registerNativeItem(
id = "mymod:magic_wand",
registerMode = RegisterMode.RELOADABLE,
configure = {
stacksTo(1)
}
) { props ->
object : Item(props) {
override fun use(level: Level, player: Player, hand: InteractionHand): InteractionResult {
if (level.isClientSide) return InteractionResult.SUCCESS
// Custom server-side logic here
return InteractionResult.SUCCESS
}
}
}RegisterMode.AUTO- Automatically choose the best mode based on current game stateRegisterMode.RELOADABLE- Item can be hot-reloaded; changes take effect after/reloadRegisterMode.PERSISTENT- Item persists across reloads; use for items that should always exist
Warning
-
Item Construction Timing: The
itemFactorylambda is called during the registration window when the registry is temporarily unfrozen. Do not construct Item instances outside this lambda. -
Hot Reload Behavior: When using
RELOADABLEmode, the item's behavior can be updated by modifying the script and running/reload. The item instance itself remains registered in the game's registry. -
Accessing Registered Items: Use the
ITEMSregistry to get your registered items:
Run /katton reload to reload all scripts without restarting the server. This will:
- Re-read all script files from datapacks
- Re-compile and execute the scripts
- Update event handlers and item behaviors
Note
Items registered with RELOADABLE mode will have their behavior updated on reload, but the item instance itself remains in the game registry. To add new items, simply add new registerNativeItem calls in your scripts.
Warning
top.katton.api.unsafe is intentionally dangerous. It can redefine classes at runtime and hook arbitrary methods. Use only if you fully understand JVM instrumentation side effects.
Katton provides an experimental unsafe API for dynamic runtime method hooks:
- Before hook: run callback before target method body
- After hook: run callback after target method body (with result/throwable)
- Rollback by handle or owner
The internal implementation is in UnsafeInjectionManager.
- On first injection for a method, Katton installs ByteBuddy agent and redefines the target class.
- A universal Advice bridge is woven into method enter/exit.
- Advice forwards runtime calls to Katton's before/after handler registries.
- During
/katton reload, Katton clears unsafe hook registries before re-running scripts.
Unsafe script API is provided by UnsafeApi.kt:
Both string-based and Method-based overloads are supported.
import top.katton.api.unsafe.*
import net.minecraft.server.level.ServerPlayer
val m = ServerPlayer::class.java.getDeclaredMethod("getName")
val handle = injectBefore(m) { ctx ->
println("before: ${ctx.method.declaringClass.simpleName}.${ctx.method.name}")
}
injectAfter(m) { ctx, result, throwable ->
if (throwable != null) {
println("after with error: ${throwable.message}")
} else {
println("after result: $result")
}
}
// rollback one
rollbackUnsafe(handle)Warning
-
Hook execution exceptions are caught and logged, so they do not directly break the target method invocation chain.
-
This API does not add whitelist/permission sandbox by default.
-
Runtime redefinition may conflict with other transformers/mods depending on load order and target classes.