网络协议

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 写封包的服务端实现都遵循同一套模板:

  1. TeamManager.getTeamByPlayer(handler.playerEntity.getUniqueID()),为 null 则 return null(无队伍不处理)
  2. 改 TaskNHWorldData
  3. TaskNHNetwork.sendToTeamMembers(team.getMembers(), new SyncAllTasksPacket(data.getTeamTasks(team.getTeamId())))

各封包的额外校验:

  • CreateTaskPacket —— if (data.getTask(team.getTeamId(), task.id) != null) return null; 拒绝重复 ID
  • UpdateTaskPacket —— if (oldTask == null) return null; 拒绝未知任务;随后在服务端判定 shouldCompleteOnChecklist() 并置 DONE;状态变化时先 moveToEnd;最后给新分配到的在线玩家单发一条 tasknh.chat.assigned
  • DeleteTaskPacket —— 不存在也照常调 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 全部单向。