Skip to content
On this page

脚本包

Katton 把你的 Kotlin 代码组织成脚本包。一个脚本包就是一个包含 manifest.json 和一些 .kt 文件的文件夹(或 .java 文件)。脚本包是不分端的——同一个包可以同时给服务端和客户端用(Fabric 和 NeoForge),而且全部支持 /katton reload 热重载

NOTE

在 Paper 上,脚本包仅为服务端。没有客户端,因此 @ClientScriptEntrypoint 和客户端 API 不可用。脚本从 <serverDir>/kattonpacks/<worldDir>/kattonpacks/ 加载。

脚本包

目录结构

脚本包放在 kattonpacks 目录下,Katton 会自动扫描:

# 全局 — 所有世界共享,加入或者退出世界并不会影响:
<游戏目录>/kattonpacks/<包名>/manifest.json
<游戏目录>/kattonpacks/<包名>/**/*.kt

# 世界 — 每个存档独立,首次重载时自动创建目录:
<世界目录>/kattonpacks/<包名>/manifest.json
<世界目录>/kattonpacks/<包名>/**/*.kt
  • 全局包提供进程级 BOOTSTRAP/READY 入口,所有存档共享,热重载时不会重放入口。
  • 世界包在服务端/客户端世界阶段运行并可重放——适合做地图专属脚本。

包内的 .kt 文件可以随意嵌套子目录,Katton 会递归扫描整个目录树。入口函数可以放在根目录、src/main/qwq/等脚本包的任意位置。在常规开发中,文件夹的结构一般决定了你的代码组织方式和包的模块划分。

脚本配置(manifest.json

每个包需要在根目录放一个 manifest.json 才能被识别。dependencies 数组为必填项;不依赖其他模组或插件时写空数组。其余字段由 Katton 提供默认值:

json
{
  "name": "My Awesome Pack",
  "id": "my_pack",
  "version": "1.0.0",
  "authors": ["YourName"],
  "description": "A cool Katton script pack!",
  "enabled": true,
  "dependencies": []
}
字段默认值说明
dependencies必填模组/插件依赖;没有依赖时写 []
id文件夹唯一包标识符——用于同步、日志和命令
nameid给人看的显示名称(在包管理界面里显示)
version"unknown"版本号
description""包的功能描述
authors[]作者名的数组
enabledtruetrue = 重载时加载,false = 跳过
clientSynctrue服务端是否会把该包同步给客户端(仅限 Fabric/NeoForge)
signature自动生成无需填写。远程客户端同步包的可选 Ed25519 签名元数据
config{}字符串/数字/布尔类型的简单包配置

依赖字段、平台解析、签名细节和哈希输入见清单、依赖与签名。脚本包所在目录决定 GLOBALWORLDSERVER_CACHE 作用域;入口注解只决定执行阶段。

如何签名脚本包

Katton 提供了 top.katton.sign Gradle 插件,用来把 Ed25519 签名元数据写入 manifest.json。在已应用该插件的 Gradle 项目中,先生成一对 key:

bash
./gradlew generateKattonSigningKey \
  -PkattonPrivateKey=keys/katton-signing-key.pem \
  -PkattonPublicKey=keys/katton-signing-key.pub

然后签名某个脚本包目录:

bash
./gradlew signKattonPack \
  -PkattonPackDir=kattonpacks/example_pack \
  -PkattonPrivateKey=keys/katton-signing-key.pem \
  -PkattonPublicKey=keys/katton-signing-key.pub \
  -PkattonKeyId=example-server-key \
  -PkattonScope=world

signKattonPack 会重写该包的 manifest.json:移除旧的 signature,对当前 manifest 和排序后的 .kt / .java 文件签名,然后写入 algorithmkeyIdpublicKeysignature

私钥不要提交到版本控制。kattonScope 必须和同步时的包作用域一致(worldglobal),因为签名载荷里包含 pack 的 sync id。

状态文件(.kattonpack.state.json

当你在游戏内界面(按 K)开关某个包时,Katton 会写一个本地状态文件:

json
{ "enabled": false }

这个文件优先级高于 manifest.json 里的 enabled 字段。删掉状态文件就能恢复 manifest 里的默认设置。

入口函数

脚本是不分端的。同一个 .kt 文件可以同时包含服务端和客户端逻辑。注解会选择明确的执行阶段:

kotlin
import top.katton.api.*

入口必须是顶层函数,可以无参数,也可以接收一个与所选阶段兼容的上下文参数:

kotlin
@ServerScriptEntrypoint(ServerPhase.READY)
fun initMyCommands(context: ServerReadyContext) {
    println("服务器就绪,触发原因:${context.cause}")
}
kotlin
@ClientScriptEntrypoint(ClientPhase.JOINED)
fun initMyHUD(context: ClientJoinedContext) {
    println("为 ${context.player.name.string} 初始化 HUD")
}

阶段与目录作用域的对应关系如下:

作用域服务端阶段客户端阶段
GLOBALBOOTSTRAPREADYREADY
WORLDREADYREGISTRY_SETUPJOINED
SERVER_CACHEREGISTRY_SETUPJOINED

@ServerScriptEntrypoint 默认使用 BOOTSTRAP@ClientScriptEntrypoint 默认使用 READY,所以默认值都面向全局包。世界包应显式写出阶段。

两个注解都有 replay: Boolean = true。全局入口永不重放;世界入口遵循该值;多人同步的 SERVER_CACHE 入口在激活后始终全部重放,即使包没有变化或写了 replay = false

上下文提供 packIdscopereasoncauseplatform,以及当前阶段保证可用的服务器、客户端、玩家或世界对象。一个文件可定义任意多个入口,每个都会被独立发现和调用。实践示例见选择入口执行阶段,完整契约见脚本加载生命周期

使用外部模组或插件 API 前必须在 dependencies 中声明,详见使用其他模组与插件

CAUTION

Katton 不会阻止你在 @ClientScriptEntrypoint 函数里调用服务端专属 API(反之亦然)。强行在客户端入口函数中调用服务端专有 API 可能会导致游戏崩溃!服务端逻辑和客户端逻辑请放在不同的入口函数里。

热重载

执行 /katton reload 即可重载全部脚本,无需重启游戏。这是 Katton 开发循环的核心:

  1. 重新扫描全局和作用域内所有已启用的包
  2. 清除事件处理器、注入和注册所有权
  3. 将所有源码包一起重新编译,然后加载 JAR 包
  4. 根据作用域、阶段和重放策略调用符合条件的入口函数
  5. 在屏幕顶部显示可视化进度条(消息 + 百分比 + 绿色进度条)

你也可以间接触发重载:

  • /reload(原版命令)→ 触发服务端 Katton 重载
  • 包管理界面 → 按 K,点 Reload → 同时触发两端

F3 + T 只重载 Minecraft 资源,不会重载 Katton 脚本。

完整重载流程见 热重载与调试

客户端脚本

WARNING

客户端脚本仅适用于 Fabric 和 NeoForge。Paper 是纯服务端平台,不支持客户端脚本、HUD 或渲染 API。

客户端脚本适合做 HUD 覆盖层、自定义渲染器、UI 互动,以及任何需要访问 Minecraft.getInstance() 或渲染 API 的场景。

kotlin
import top.katton.api.*

@ClientScriptEntrypoint(ClientPhase.JOINED)
fun hudRenderTestMain() {

    registerHudRenderer("katton:test:hud", HudRenderLayer.FOREGROUND, 20) { ctx ->
        fillHudRect(ctx, 6, 6, 258, 64, 0xAA000000.toInt())
        fillHudRect(ctx, 8, 8, 256, 62, 0x66002244)

        drawHudText(ctx, "Katton HUD Render Test", 14, 14, 0xFFE8F1FF.toInt(), true)
        drawHudText(ctx, "FPS: ${clientFps()}", 14, 28, 0xFF9BD5FF.toInt(), false)
        drawHudText(ctx, "Screen: ${clientScreenName() ?: "In-World"}", 14, 40, 0xFFB5FFC5.toInt(), false)

        val p = clientPos()
        if (p != null) {
            drawHudText(ctx, "Pos: %.2f, %.2f, %.2f".format(p.x, p.y, p.z), 14, 52, 0xFFFFD38A.toInt(), false)
        }
    }
    clientTell("[Katton] HUD render test script loaded", overlay = false)
}

客户端脚本可以和服务端脚本放在同一个包里(使用注解 @ClientScriptEntrypoint 加以区分)。在 Fabric/NeoForge 多人服务器上,需要客户端同步的包会自动同步到 <游戏目录>/serverpacks/ 下的修订目录。

服务端脚本

服务端脚本处理游戏逻辑:命令、事件、注册、世界操作、数据包集成等。

kotlin
import top.katton.api.ServerPhase
import top.katton.api.ServerScriptEntrypoint
import top.katton.api.event.PlayerArg
import top.katton.api.event.ServerPlayerEvent
import top.katton.api.dpcaller.tell
import top.katton.api.once

@ServerScriptEntrypoint(ServerPhase.READY)
fun registerPlayerEvents() {
    ServerPlayerEvent.onPlayerJoin += join@ fun(arg: PlayerArg) {
        val player = arg.player
        val firstSeen = once(
            key = "welcome:${player.uuid}",
            namespace = "event_player_lifecycle_demo"
        ) {}

        if (firstSeen) {
            tell(player, "[event-demo] Welcome to the server! Enjoy your stay!")
        } else {
            tell(player, "[event-demo] Welcome back!")
        }
    }
}

在该示例中,我们订阅了 onPlayerJoin 事件来发送欢迎信息。同时使用 once API 判断玩家是否首次加入,以便给出不同提示。

包类型

Katton 支持两种物理格式:

类型结构编译方式适用场景
目录文件夹包含 manifest.json + .kt 文件所有源码包一起编译(跨包引用可用)开发和迭代
JAR (WIP).jar 文件,manifest.json 放在根目录或 META-INF/katton/manifest.json直接加载(已编译)分发和分享

目录包会被合并到一个编译单元里——所以你在一个包里定义的辅助函数可以在另一个包里直接用。JAR 包则单独加载。

进阶:服务端→客户端同步

当客户端连接到多人服务器时,Katton 会自动将服务器脚本包同步到客户端:

  1. 服务端发送哈希列表ScriptPackHashListPacket)——每个包的同步 ID 对 SHA-256
  2. 服务端立刻发送完整包快照ScriptPackBundlePacket)——manifest + 所有同步文件
  3. 客户端写入缓存前验证可选 Ed25519 签名
  4. 客户端把缓存 hash 与服务端声明的 hash 列表比对
  5. 未信任的服务器或签名 key 会打开阻塞式信任界面
  6. 受信任的包会在注册表校验前执行

每次服务端热重载成功后,Katton 还会发布新的游戏阶段修订。客户端只请求变化的包,暂存并预编译完整候选快照,然后重放所有活动服务器缓存包并回执结果。被移除的包会停用,空快照也受支持;候选失败时上一修订继续工作。失败回执或 30 秒超时会断开客户端。

配置阶段与在线重载协议详见脚本包同步与信任

NOTE

Paper 服务端不支持客户端同步,所有脚本均为服务端本地文件。

纯服务端包可以设置 "clientSync": false,从而不再同步过程中被发送到客户端。