所有 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 服务端 → 客户端 通知客户端清场
模块通过 PacketBridgeAPI 注册 UI:
PacketBridgeAPI.UiRegistrationResult result =
packetBridge. registerOrReloadUi ( "market_shop" , uiFile);
registerOrReloadUi 是幂等安全的:先尝试 reload(已注册则更新),再尝试 register(未注册则注册)。
Map< String , Object > payload = Map. of (
"title" , "新手村" ,
"owned_count" , 12 ,
"list" , listOfTitles
);
packetBridge. sendPacket (player, uiId, "init" , payload);
update 有两种模式:
模式 触发方式 示例模块 周期型 refresh-interval-ticks 定时推EntityTracker、Tab 事件型 状态变化时推 Mail、Title
packetBridge. sendPacket (player, uiId, "update" , updatePayload);
packetBridge. sendPacket (player, uiId, "close" , Map. of ());
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 注册初始化处理器,用于在客户端就绪后推送初始数据。
形态 例子 客户端读取 字符串 "killer={name};victim={name}"整段文本 列表 ["{name}", "{weapon}"]packet[0] / packet[1]字典 {killer: ..., weapon: ...}packet['killer']
客户端通过 Packet.send(packetId, action, ...args) 发送回包:
参数 说明 packetIdUI ID(如 AXS_TITLE) action动作名(如 equip、refresh) args额外参数(可选)
服务端收到的 data 列表中,data.get(0) 为 action,后续为额外参数。
来源 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);
}