队伍与同步流程
event/ 包 4 个类 + cache/TaskNHClientCache.java。TaskNH 是队伍协作型模组:任务属于队伍,编辑由任一队员发起,全队在线成员立刻看到。
登录:PlayerLoginHandler
监听 PlayerEvent.PlayerLoggedInEvent,注册在 FML 总线(源码注释:PlayerLoggedInEvent fires on FML's bus, not MinecraftForge.EVENT_BUS)。顺序严格:
- 先发
WorldIdPacket—— 注释明确「ahead of the team check and the sync: the client needs the world before it reads or cleans pins.」即使该玩家没有队伍也会发。 TeamManager.getTeamByPlayer(...),为null直接 return —— 无队伍玩家收不到任何任务同步- 发
SyncAllTasksPacket(本队任务) - 发
SyncTeamMembersPacket(buildTeamMembersPacket(team)) sendLoginNotifications(...)
成员名解析(buildTeamMembersPacket):先查在线玩家名表;查不到则用 UsernameCache.getLastKnownUsername(playerId);再查不到就用 UUID 前 8 位。所以离线队友在负责人列表里可能显示成一串十六进制。SyncTeamMembersPacket 里名字长度上限 64。
「新任务」提醒:拿存档里的 playerLastSeen(playerId) 与每个任务中 AssignedPlayer.assignedAt() 比较,只列出 assignedAt > lastSeen 的任务。多于一条时先发一条汇总 tasknh.chat.login.new_tasks,然后逐条发 tasknh.chat.login.task——每条都带一个可点击的 [Open] 链接,ClickEvent.Action.RUN_COMMAND,命令是 /tasknh open <完整 UUID>。同一个 buildTaskLink 方法也被提醒功能复用。
登出:PlayerLogoutHandler
监听 PlayerEvent.PlayerLoggedOutEvent(同样在 FML 总线):
TaskNHWorldData.get().setPlayerLastSeen(playerId, System.currentTimeMillis())—— 这就是「上次见过」时间戳的写入点ItemTrackHandler.forget(player)—— 移除物品栏监听器与待检查项
登出时间戳是登录提醒的锚点,因此它必须写进存档(setPlayerLastSeen 会 markDirty())。
队伍合并:TeamMergeListener
监听 GTNHLib 的 TeamEvents.TeamMergeEvent,注册在 Forge 总线。两件事:
data.mergeTasks(consumed.getTeamId(), surviving.getTeamId())—— 被吞并队伍的任务整体搬进幸存队伍- 给幸存队伍发两条包:
SyncAllTasksPacket(新任务列表)+buildTeamMembersPacket(新成员表)
源码注释说明了第二条的必要性:「Accepting an invite as a solo player merges teams, so the assignee lists sent at login are stale now.」——单飞玩家接受入队邀请也会触发队伍合并,所以登录时发的成员表此刻已经过期。
客户端缓存:TaskNHClientCache
cache/TaskNHClientCache.java,全部静态,只在客户端线程访问(ClientProxy 的注释说明这一点,因此断线事件只置一个 volatile boolean 标志,由下一 tick 处理)。
| 字段 | 类型 |
|---|---|
tasks |
Map<UUID, Task>(LinkedHashMap) |
pinConfig |
单例 PinnedTasksConfig |
teamMembers |
List<PlayerEntry> |
pendingEdit |
Task,本地已应用待服务端确认的编辑 |
pendingDelete |
UUID,本地已删待确认 |
乐观更新与 pendingEdit
putLocal(Task) 把改动立刻写进缓存(并记为 pendingEdit),这样 GUI 不必等服务端回包。类注释解释了为什么需要 pendingEdit:「由更早的编辑引发的同步带着早于这次编辑的服务端状态,否则会把它撤销。」
update(Collection<Task> incoming) 的逻辑:
tasks.clear(); for (t : incoming) tasks.put(t.id, t);
if (TaskNHGui.isWithinSelfEditWindow()) {
if (pendingEdit != null) tasks.put(pendingEdit.id, pendingEdit);
if (pendingDelete != null) removeLocal(pendingDelete);
} else {
pendingEdit = null; pendingDelete = null; // Confirmed by now, server state wins again.
}
pinConfig.removeStale(tasks.keySet());
if (isModLoaded("navigator")) TaskLayerManager.INSTANCE.refreshFromCache(tasks.values());
每次同步都用服务端状态整体替换缓存(不是合并),只有落在 1 秒自同步窗口内的那一次才把本地编辑重新贴回去。之后服务端状态无条件获胜。
update 之后固定做两件事:清理失效图钉(批量,一次 save())与刷新 Navigator 地图标记(后者用 Loader.isModLoaded("navigator") 内联判断,因此这个缓存类本身不直接依赖 Navigator)。
断线清空
ClientProxy.onDisconnect(FMLNetworkEvent.ClientDisconnectionFromServerEvent)只置 disconnected = true;下一个 onClientTick 消费该标志并调 TaskNHClientCache.clear()。注释说明原因:「The cache is only safe on the client thread, so the event just leaves a flag for the tick above.」
图钉排序
getPinnedTasks() 决定 HUD 顺序与数量上限的实际口径:
- 过滤掉
DONE任务 —— 类注释:「Done tasks stay pinned but are left out, so a reopened task comes back to the HUD.」即已完成的任务仍留在图钉配置里,只是不显示 - 排序键为
hudRank(status)再order:IN_PROGRESS→ 0,OPEN→ 1,DONE→ 2 - 同一状态内按
order(各标签页独立编号,见 任务存档)
canPin() 是 getPinnedTasks().size() < pinConfig.getMaxPinnedTasks()——因为已过滤掉 DONE,所以已图钉的已完成任务不占名额。这正是 PinnedTasksConfig.pin() 把上限检查推给调用方的原因。
三个事件处理器的总线归属
| 类 | 事件 | 总线 |
|---|---|---|
PlayerLoginHandler |
PlayerEvent.PlayerLoggedInEvent |
FML |
PlayerLogoutHandler |
PlayerEvent.PlayerLoggedOutEvent |
FML |
ItemTrackHandler |
EntityJoinWorldEvent |
Forge |
ItemTrackHandler |
TickEvent.ServerTickEvent |
FML |
TeamMergeListener |
TeamEvents.TeamMergeEvent |
Forge |
HudRenderer |
RenderGameOverlayEvent.Post |
Forge |
ClientProxy |
TickEvent.ClientTickEvent |
FML |
ClientProxy |
FMLNetworkEvent.ClientDisconnectionFromServerEvent |
FML |
ClientProxy |
ModularUI ReloadThemeEvent.Post |
Forge |