网络协议
network/ 包,通道为 GTNHLib 的 NetworkChannel("tasknh"),不是 SimpleNetworkWrapper。封包实现 com.gtnewhorizon.gtnhlib.network.base.IPacket,靠 executeServer / executeClient 区分两端。
封包 ID 按 TaskNHNetwork.init() 里的注册顺序分配,没有显式数字 ID。
9 个封包
| # | 封包 | 方向 | 载荷 | 何时发送 |
|---|---|---|---|---|
| 0 | SyncAllTasksPacket |
S→C | int 数量 + N 个 Task.writeToBuf |
队伍任务全量同步。登录、/tasknh reload、任何写操作后、物品追踪有改动后 |
| 1 | OpenGuiPacket |
S→C | boolean 有无 taskId + 可选 UUID |
玩家按热键(客户端本地触发,不经网络);或服务端用 /tasknh gui、/tasknh open <id> 叫客户端开界面 |
| 2 | SyncTeamMembersPacket |
S→C | int 数量 + 每项 UUID + 名字字符串 |
登录、队伍合并后 |
| 3 | CreateTaskPacket |
C→S | 一个 Task |
GUI 新建任务;BetterQuesting 与 BlockRenderer6343 集成也走这条 |
| 4 | UpdateTaskPacket |
C→S | 一个 Task |
GUI 任何编辑 |
| 5 | DeleteTaskPacket |
C→S | UUID |
GUI 删除任务 |
| 6 | ReorderTasksPacket |
C→S | int 数量 + N 个 UUID(只传 ID 与顺序,不传任务内容) |
拖拽排序松手时,一次手势一个封包 |
| 7 | RemindTaskPacket |
C→S | UUID taskId + UUID targetPlayerId |
GUI 里提醒某个队员 |
| 8 | WorldIdPacket |
S→C | UUID |
登录时最先发,在队伍判定与任务同步之前 |
⚠️ WorldIdPacket 故意注册在最后。源码注释:「Registered last so the ids of the packets above stay the same.」——插入新封包不会改变既有封包的 ID。
关键载荷细节
SyncAllTasksPacket.decode的count没有上限检查(直接new ArrayList<>(count)),而SyncTeamMembersPacket限制 500、ReorderTasksPacket限制 2000、Task内部各项限制 100/200/50。三者不一致是源码现状。ReorderTasksPacket.MAX_TASKS = 2000,注释说明这是「合理性上限,防止畸形封包让服务端分配任意大小的 list」。OpenGuiPacket用一个boolean区分「开空界面」与「开并选中某个任务」,因此一个封包类型服务两种场景。DeleteTaskPacket载荷就是裸taskId,服务端调data.deleteTask(teamId, taskId),连带删除子任务(见 任务存档)。
统一的写入模式
所有 C→S 写封包的服务端实现都遵循同一套模板:
TeamManager.getTeamByPlayer(handler.playerEntity.getUniqueID()),为null则return null(无队伍不处理)- 改
TaskNHWorldData TaskNHNetwork.sendToTeamMembers(team.getMembers(), new SyncAllTasksPacket(data.getTeamTasks(team.getTeamId())))
各封包的额外校验:
CreateTaskPacket——if (data.getTask(team.getTeamId(), task.id) != null) return null;拒绝重复 IDUpdateTaskPacket——if (oldTask == null) return null;拒绝未知任务;随后在服务端判定shouldCompleteOnChecklist()并置DONE;状态变化时先moveToEnd;最后给新分配到的在线玩家单发一条tasknh.chat.assignedDeleteTaskPacket—— 不存在也照常调deleteTask(内部map.remove返回null时直接返回)ReorderTasksPacket—— 逐个比对task.order == i,相同则跳过;任务被队友删掉时会在编号上留一个空洞(源码注释);changed为假则不发回同步RemindTaskPacket—— 冷却检查 + 目标在线检查,见 服务端配置
客户端发包辅助方法
TaskNHNetwork 提供三个 @SideOnly(CLIENT) 静态方法,都先调 TaskNHGui.expectSelfSync()(标记 1 秒自同步窗口,避免自己编辑的回声触发 GUI 重建):
sendEditToServer(Task, IPacket)—— 先TaskNHClientCache.putLocal(task)把改动写进本地缓存,再发包sendReorderToServer(List<Task>)—— 同时把order写进传入对象和缓存中的副本(两个地方都写)。源码注释解释:widget 持有的是面板构建时缓存里的那个 Task,而每次同步都会用全新对象替换缓存;若只写进 widget 持有的副本,重建时读到的仍是旧顺序,列表会「慢一次打开」才移动sendDeleteToServer(UUID, IPacket)—— 先TaskNHClientCache.removeLocal(taskId)。源码注释:否则待处理的编辑会把已删任务放回缓存
sendToTeamMembers(Set<UUID> memberUuids, IPacket packet) 是服务端广播辅助:遍历 playerEntityList,只发给 UUID 在集合里的在线玩家,离线的静默跳过。
客户端接收端
SyncAllTasksPacket.executeClient——TaskNHClientCache.update(tasks)后TaskNHGui.notifySyncReceived()SyncTeamMembersPacket.executeClient——TaskNHClientCache.updateTeamMembers(members)WorldIdPacket.executeClient——TaskNHClientCache.setWorld(worldId)OpenGuiPacket.executeClient—— 有 taskId 则新建TaskNHGuiData并selectTask(taskId)后TaskNHGui.open(data),否则TaskNHGui.open()
没有任何一个封包回发服务器,C→S 全部单向。