Skip to content
On this page

Choosing an Entrypoint Phase ​

Every entrypoint answers two separate questions:

  1. The pack directory selects its scope: GLOBAL, WORLD, or synchronized SERVER_CACHE.
  2. The annotation selects the execution phase and replay policy.

Katton rejects an incompatible combination. It does not infer an AUTO phase.

Pick the Phase by Available State ​

You needPack scopeAnnotation
Process-level setup without a serverGLOBAL@ServerScriptEntrypoint(ServerPhase.BOOTSTRAP)
A running server or its worldsGLOBAL or WORLD@ServerScriptEntrypoint(ServerPhase.READY)
One-time client initialization without a connectionGLOBAL@ClientScriptEntrypoint(ClientPhase.READY)
Client registrations before registry validationWORLD or SERVER_CACHE@ClientScriptEntrypoint(ClientPhase.REGISTRY_SETUP)
A local player and client levelWORLD or SERVER_CACHE@ClientScriptEntrypoint(ClientPhase.JOINED)

The annotation defaults—server BOOTSTRAP and client READY—are intentionally global-only. Always write the phase explicitly in a world pack.

Global Bootstrap ​

Use bootstrap for process-lifetime setup that cannot depend on a server, world, player, or Paper plugin.

kotlin
import top.katton.api.BootstrapContext
import top.katton.api.ServerPhase
import top.katton.api.ServerScriptEntrypoint

// GLOBAL pack only. Runs once for the process and never replays.
@ServerScriptEntrypoint(ServerPhase.BOOTSTRAP)
fun bootstrap(context: BootstrapContext) {
    println("Bootstrapping ${context.packId} on ${context.platform}")
}

Global entrypoints never replay during hot reload.

Server Ready ​

Commands, managed events, datapack changes, and most server gameplay setup belong at ServerPhase.READY.

kotlin
import top.katton.api.ServerPhase
import top.katton.api.ServerReadyContext
import top.katton.api.ServerScriptEntrypoint

// WORLD packs replay this entrypoint after hot reload by default.
@ServerScriptEntrypoint(ServerPhase.READY)
fun registerServerFeatures(context: ServerReadyContext) {
    println("${context.reason}: ${context.cause}")
    println("Players online: ${context.server.playerCount}")
}

The context exposes server and the common metadata fields:

  • reason: INITIAL_LOAD or HOT_RELOAD
  • cause: SERVER_START, COMMAND, DATAPACK_RELOAD, SERVER_PACK_SYNC, or CLIENT_JOIN
  • packId, scope, and platform

Set replay = false only when a world entrypoint must not run again:

kotlin
@ServerScriptEntrypoint(
    phase = ServerPhase.READY,
    replay = false
)
fun initializeOncePerWorldSession() {
    // Initial load only for a WORLD pack.
}

replay cannot override scope policy: global packs never replay and server-cache packs always replay.

Client Registry Setup ​

Built-in registry objects and renderers needed during connection setup must exist before the server's registry data is checked.

kotlin
import top.katton.api.ClientPhase
import top.katton.api.ClientRegistryContext
import top.katton.api.ClientScriptEntrypoint
import top.katton.api.registry.registerEntityRenderer

// WORLD and SERVER_CACHE packs use this phase for registry-sensitive setup.
@ClientScriptEntrypoint(ClientPhase.REGISTRY_SETUP)
fun registerClientContent(context: ClientRegistryContext) {
    registerEntityRenderer("example:clockwork_golem") { rendererContext ->
        ClockworkGolemRenderer(rendererContext)
    }
}

For a built-in registry entry shared by both logical sides, put both annotations on the same no-argument function:

kotlin
import net.minecraft.world.item.Item
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.registerNativeItem
import top.katton.registry.RegisterMode

@ServerScriptEntrypoint(ServerPhase.READY)
@ClientScriptEntrypoint(ClientPhase.REGISTRY_SETUP)
fun registerContent() {
    registerNativeItem("example:token", RegisterMode.RELOADABLE) {
        Item(it)
    }
}

A dual-side function should remain parameterless because the server and client phases use different context types.

Client Joined ​

Use JOINED when code needs the local player, client level, or in-world HUD state. Katton delays the invocation until those objects are available.

kotlin
import top.katton.api.ClientJoinedContext
import top.katton.api.ClientPhase
import top.katton.api.ClientScriptEntrypoint
import top.katton.api.drawHudText
import top.katton.api.registerHudRenderer

// Katton waits until the local player and client level both exist.
@ClientScriptEntrypoint(ClientPhase.JOINED)
fun registerHud(context: ClientJoinedContext) {
    registerHudRenderer("example:welcome") { hud ->
        drawHudText(hud, "Hello ${context.player.name.string}", 8, 8)
    }
}

On a live multiplayer update, Katton activates the complete candidate snapshot and then replays every active SERVER_CACHE entrypoint. Unchanged synchronized packs replay too, ensuring their handlers match the server-owned snapshot.

For the complete lifecycle contract, see Script Loading Lifecycle.