Replacing Load, Tick, and Schedule
Datapacks use #load, #tick, and schedule function because everything is a function call. Katton can express those same ideas through entrypoints, events, and normal Kotlin control flow.
#load
Use a world-scoped ServerPhase.READY entrypoint for replayable script setup. It runs on initial world/server readiness and again when its reload policy permits.
import top.katton.api.ServerPhase
import top.katton.api.ServerScriptEntrypoint
@ServerScriptEntrypoint(ServerPhase.READY)
fun load() {
println("Loaded script logic")
}Use the entrypoint to register event listeners, commands, and initial state. Avoid putting all gameplay logic directly in the entrypoint.
#tick
Use server tick events for logic that truly must run every tick.
import net.minecraft.network.chat.Component
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.resources.Identifier
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.projectile.arrow.Arrow
import net.minecraft.world.level.Level
import top.katton.api.ServerPhase
import top.katton.api.ServerScriptEntrypoint
import top.katton.api.dpcaller.getEntityNbt
import top.katton.api.dpcaller.nbt
import top.katton.api.dpcaller.tell
import top.katton.api.event.ServerTickArg
import top.katton.api.event.ServerEvent.onStartServerTick
import top.katton.util.EntitySelectorBuilder
import java.util.UUID
@ServerScriptEntrypoint(ServerPhase.READY)
fun main() {
val arrowType = BuiltInRegistries.ENTITY_TYPE
.getOptional(Identifier.parse("minecraft:arrow"))
.orElseThrow()
val arrowSelector = EntitySelectorBuilder.allEntities()
.type(arrowType)
.create()
val knownArrowIds = HashSet<UUID>()
val tntArrows = HashSet<Arrow>()
// Executed every server tick on Fabric, NeoForge, and Paper.
onStartServerTick += tick@
fun(arg: ServerTickArg) {
val arrows = arrowSelector
.findEntities(arg.server.createCommandSourceStack())
.filterIsInstance<Arrow>()
val liveArrowIds = arrows.mapTo(HashSet()) { it.uuid }
knownArrowIds.retainAll(liveArrowIds)
arrows.forEach { arrow ->
if (knownArrowIds.add(arrow.uuid)) {
(arrow.owner as? ServerPlayer)?.let { player ->
onArrowShot(player, arrow, tntArrows)
}
}
}
// Check if a TNT arrow has hit the ground and make it explode.
processTNTArrows(tntArrows)
}
}
fun onArrowShot(player: ServerPlayer, arrow: Arrow, tntArrows: MutableSet<Arrow>) {
tell(
player,
Component.literal("The weapon in your hand is: ").append(player.mainHandItem.itemName)
)
//this arrow is shot by a tnt bow, make it explode
if (player.mainHandItem.nbt.getBooleanOr("tnt", false)) {
tntArrows.add(arrow)
}
}
fun processTNTArrows(tntArrows: MutableSet<Arrow>) {
val iterator = tntArrows.iterator()
while (iterator.hasNext()) {
val arrow = iterator.next()
// Check if the arrow has hit the ground by checking its NBT data.
if (getEntityNbt(arrow).getBooleanOr("inGround", false)) {
// Make the arrow explode
// This method is from vanilla code
arrow.level().explode(
arrow,
arrow.damageSources().explosion(arrow, arrow.owner),
null,
arrow.position(),
16.0f,
false,
Level.ExplosionInteraction.TNT
)
iterator.remove()
// Remove the arrow entity after explosion
arrow.kill(arrow.level() as ServerLevel)
}
}
}Tick code can become expensive quickly. Prefer event hooks when the logic is really "when a player joins", "when an entity is hurt", or "when a block changes".
schedule function
Scheduled functions usually mean one of three things:
| Datapack pattern | Katton replacement |
|---|---|
| Delay work by a few ticks | Store a countdown and update it from a tick event |
| Poll until a condition changes | Use a specific event if one exists |
| Run a repeating system | Use tick events, but keep the handler small |
Reload Cleanup
Katton tracks script-owned event handlers and clears them on reload. Register listeners inside entrypoints so Katton knows which script pack owns them.
For more detail, see Events and Script Loading Lifecycle.
