任务存档
storage/TaskNHWorldData.java,继承 WorldSavedData。任务不放在方块实体或物品里,而是以整个世界的 WorldSavedData 形式落在主世界(维度 0)的 mapStorage 中。字段与 NBT 键见 任务数据模型。
存档名与位置
| 项 | 值 | 来源 |
|---|---|---|
| 当前存档名 | TaskNHTasks |
private static final String DATA_NAME = "TaskNHTasks" |
| 旧存档名(一次性迁移源) | ForemanTasks |
LEGACY_DATA_NAME;源码注释:模组原名 “Foreman” |
| 存储位置 | 主世界(server.worldServerForDimension(0))的 MapStorage |
TaskNHWorldData.get() |
get() 每次调用都走 MapStorage.loadData(),客户端调用会抛 IllegalStateException(源码:TaskNHWorldData.get() called on client side)。
首次加载(loadData 返回 null)时会创建新实例,并检查旧键 ForemanTasks:若旧数据非空(任务或 playerLastSeen 任一非空),则整体搬入、置 markDirty(),并打出 info 日志「Migrated tasks from legacy storage ‘ForemanTasks’ to ‘TaskNHTasks’ (N team(s))」。只做一次,因为搬入后 TaskNHTasks 已存在,下次走正常加载分支。
顶层 NBT 结构
| 键 | 类型 | 说明 |
|---|---|---|
worldIdMost / worldIdLeast |
long | 世界 UUID,首次调用 getWorldId() 时随机生成并置脏。用于客户端按世界分别保存图钉与折叠状态 |
perTeamTasks |
TAG_List of compound | 每项见下 |
playerLastSeen |
TAG_List of compound | 每项为 {most, least, ts},记录玩家退出时的 System.currentTimeMillis() |
perTeamTasks 的每个元素:
teamIdMost/teamIdLeast(long)——队伍 UUID,来自 GTNHLib 的Teamtasks(TAG_List of compound)——该队伍的全部Task.toNBT()
内层索引是 Map<UUID, LinkedHashMap<UUID, Task>>:外层 key = 队伍 ID,内层 key = 任务 ID,用 LinkedHashMap 保住插入顺序。
按队伍隔离
任务从属于队伍,不从属于玩家。每个子命令和每条封包都先用 TeamManager.getTeamByPlayer(playerUUID) 查队伍,查不到直接 return null(无队伍的玩家不参与任何任务操作)。对外的 getTeamTasks(UUID teamId) 返回 Collections.unmodifiableCollection(...),没有「跨队伍查询」的方法。
服务端改动的统一后续动作是 TaskNHNetwork.sendToTeamMembers(team.getMembers(), new SyncAllTasksPacket(...)),只推给在线队员(sendToTeamMembers 遍历 playerEntityList,离线的队员被静默跳过)。
排序与移动
addTask(teamId, task)—— 总是先调moveToEnd,即新建任务一律落到同组末尾,无论是谁创建的moveToEnd(teamId, task)—— 置task.order = Task.endOrder(getTeamTasks(teamId), task)updateTask(teamId, task)—— 覆盖写入并置脏deleteTask(teamId, taskId)—— 删除任务,并连带删除其子任务(removeIf(t -> taskId.equals(t.parentId)))。源码注释:嵌套只有一层,所以一层removeIf就够mergeTasks(consumedTeamId, survivingTeamId)—— 队伍合并时把被吞并队伍的全部任务搬进幸存队伍
endOrder() 跳过两类任务:other.id.equals(task.id)(自己)以及 other.status != task.status(不同状态的任务各自独立编号),再要求 Objects.equals(other.parentId, task.parentId)(同组)。因此切换状态会改变所在分组,UpdateTaskPacket 与 /tasknh done 都会在状态变化时先 moveToEnd 再 updateTask。源码注释:这一步必须在重排之前,因为重排读的是任务最终落定的状态。
旧扁平格式迁移
readFromNBT 分两层兼容:
- 有
perTeamTasks键 —— 当前格式,正常读取。 - 否则有
tasks键 —— 旧的扁平格式(所有任务平铺,不分队伍)。这些任务被塞进一个哨兵队伍 UUID:00000000-0000-0000-0000-000000000000,并打出 info 日志「Migrated N legacy tasks to sentinel team …」。
两处读取循环都用 try { ... } catch (Exception e) 包裹单个任务,失败只打 warn(「Skipping corrupt task at team …, index …」)并跳过该条,一条坏数据不会让整份存档加载失败。
markDirty() 与 client 调用
addTask / updateTask / deleteTask / mergeTasks / setPlayerLastSeen / getWorldId()(首次生成时)都调 markDirty()。getWorldId() 在 worldId == null 时生成随机 UUID 并置脏——源码注释说明这是为了让客户端区分不同存档,从而按存档分别保存图钉与折叠状态。
客户端侧只通过 WorldIdPacket 拿到这个 UUID,从不直接读 WorldSavedData。