Suite

数据包流向

ArcartXSuite 客户端到服务端到模块的数据流、UI Packet 五段式生命周期与结构

数据包流向

所有 UI 类模块都遵循统一的 五段式生命周期 数据包协议,从服务端注册 UI 到客户端交互,再到服务端处理回包。

五段式生命周期

sequenceDiagram
    participant S as 服务端
    participant A as ArcartX 客户端 MOD
    participant M as 模块
 
    S->>A: 1. register(注册 UI 模板)
    S->>A: 2. open(通知打开 UI)
    S->>A: 3. init(推送首帧完整状态)
    loop 状态变化时
        S->>A: 4. update(推送增量更新)
    end
    S->>A: 5. close(通知清场)
 
    A->>S: Packet.send(packetId, action, data)
    S->>M: ClientPacketRouter 路由
    M->>S: 处理结果
    S->>A: update(推送处理后的新状态)
阶段方向说明
register服务端 → 客户端把 UI 模板注册到 ArcartX
open服务端 → 客户端通知客户端打开 UI
init服务端 → 客户端推送首帧完整状态
update服务端 → 客户端按刷新周期或事件推增量
close服务端 → 客户端通知客户端清场

服务端 → 客户端

UI 注册

模块通过 PacketBridgeAPI 注册 UI:

PacketBridgeAPI.UiRegistrationResult result =
    packetBridge.registerOrReloadUi("market_shop", uiFile);

registerOrReloadUi 是幂等安全的:先尝试 reload(已注册则更新),再尝试 register(未注册则注册)。

init — 推送首帧

Map<String, Object> payload = Map.of(
    "title", "新手村",
    "owned_count", 12,
    "list", listOfTitles
);
packetBridge.sendPacket(player, uiId, "init", payload);

update — 推送增量

update 有两种模式:

模式触发方式示例模块
周期型refresh-interval-ticks 定时推EntityTracker、Tab
事件型状态变化时推Mail、Title
packetBridge.sendPacket(player, uiId, "update", updatePayload);

close — 通知清场

packetBridge.sendPacket(player, uiId, "close", Map.of());

客户端 → 服务端

UI 模板中的 Packet.send

UI 模板中通过 Shimmer 脚本调用 Packet.send(...)

controls:
  equip_btn:
    type: button
    onClick: |-
      Packet.send("AXS_TITLE", "equip", "myth_hunter")

客户端事件路由

客户端发送的自定义包通过 ArcartX 的 ClientCustomPacketEvent 事件进入服务端:

flowchart TD
    A["客户端 Packet.send()"] --> B["ClientCustomPacketEvent<br/>ArcartX 事件"]
    B --> C["ClientEventLifecycleManager<br/>onClientCustomPacket()"]
    C --> D["ModuleRegistry<br/>routeClientPacket()"]
    D --> E["ClientPacketRouter<br/>routeClientPacket()"]
    E --> F{"ClientPacketGuard<br/>频率校验"}
    F -->|"放行"| G["按优先级遍历处理器"]
    F -->|"拒绝"| H["短路返回 true"]
    G --> I{"handler.handleClientPacket()"}
    I -->|"消费"| J["短路返回 true"]
    I -->|"不消费"| G

服务端包处理器路由

ClientPacketRouter 按优先级遍历已注册的处理器,第一个消费的处理器短路返回:

boolean routeClientPacket(Player player, String packetId, List<String> data) {
    String action = data.isEmpty() ? "refresh" : data.get(0).toLowerCase();
    for (PrioritizedPacketHandler ph : packetHandlers) {
        // 频率校验
        if (packetGuard != null && !packetGuard.allow(player, guardModule, action, false)) {
            return true; // 被守卫拒绝
        }
        // 处理器消费
        if (ph.handler().handleClientPacket(player, packetId, data)) {
            return true; // 短路
        }
    }
    return false;
}

模块处理回包

模块的 ClientPacketHandler 处理客户端回包:

public class TitlePacketHandler implements ClientPacketHandler {
    @Override
    public boolean handleClientPacket(Player player, String packetId, List<String> data) {
        String action = data.get(0);  // 动作名
        switch (action) {
            case "equip"   -> service.equip(player, data.get(1));
            case "unequip" -> service.unequipGroup(player, data.get(1));
            case "refresh" -> service.refresh(player);
        }
        return true; // 消费
    }
}

客户端初始化事件

当 ArcartX 客户端 MOD 完成初始化时,触发 ClientInitializedEvent.End 事件:

flowchart TD
    A["客户端 MOD 初始化完成"] --> B["ClientInitializedEvent.End"]
    B --> C["ClientEventLifecycleManager<br/>onClientInitialized()"]
    C --> D["ModuleRegistry<br/>routeClientInitialized()"]
    D --> E["ClientPacketRouter<br/>routeClientInitialized()"]
    E --> F["遍历所有初始化处理器"]
    F --> G["handler.onClientInitialized(player)"]

模块通过 ModuleContext.registerClientInitializedHandler 注册初始化处理器,用于在客户端就绪后推送初始数据。

UI Packet 结构

三种 payload 形态

形态例子客户端读取
字符串"killer={name};victim={name}"整段文本
列表["{name}", "{weapon}"]packet[0] / packet[1]
字典{killer: ..., weapon: ...}packet['killer']

客户端回包数据格式

客户端通过 Packet.send(packetId, action, ...args) 发送回包:

参数说明
packetIdUI ID(如 AXS_TITLE
action动作名(如 equiprefresh
args额外参数(可选)

服务端收到的 data 列表中,data.get(0) 为 action,后续为额外参数。

UI ID 命名空间

来源UI ID 格式示例
模块注册模块名:ui_idAXS:lottery_case
直接 ui/ 目录文件名lottery_case

关闭 UI 时需要使用注册时的完整 ID。

线程安全

ClientEventLifecycleManager 收到客户端事件后,若不在主线程则通过 AxsScheduler.runTask 调度到主线程执行,确保模块处理器在主线程安全运行:

Runnable route = () -> registry.routeClientPacket(player, packetId, data);
if (Bukkit.isPrimaryThread()) {
    route.run();
} else {
    AxsScheduler.runTask(plugin, route);
}