Skip to content
On this page

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.

kotlin
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.

kotlin
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 patternKatton replacement
Delay work by a few ticksStore a countdown and update it from a tick event
Poll until a condition changesUse a specific event if one exists
Run a repeating systemUse 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.