任务数据模型

TaskNH 全部任务数据由 6 个 Java 类构成,全部位于 com.eldrinn.tasknh.data。这些类同时是 NBT 存档单元和网络封包单元——toNBT() / fromNBT() 与 writeToBuf() / readFromBuf() 成对存在,字段一一对应,因此下面的 NBT 键名与网络字段是同一套。

存储位置见 任务存档,完成判定见 物品完成追踪。

Task — 任务主体

data/Task.java。持有 id(final UUID,永不变),其余字段可变。

字段 类型 NBT 键 写入条件 含义
id UUID idMost / idLeast(各 long) 总是 任务唯一标识
title String title 总是 标题,网络侧上限 256 字符
description String description 总是 描述,网络侧上限 4096 字符
status TaskStatus status(存 name() 字符串) 总是 见下方 TaskStatus
assignees List<AssignedPlayer> assignees(TAG_List of compound) 总是 负责人,网络侧上限 100 人
location TaskLocation(可空) hasLocation(boolean)+ location(compound) hasLocation 总是写;location 仅在非空时写 地图标记坐标
iconItem ItemStack(可空) iconStack 仅在非空时写 列表图标,存为完整 ItemStack NBT,因此仅 NBT 不同的两个物品各有各的图标
trackItem ItemStack(可空) trackStack 仅在非空时写 自动完成的目标物品
trackItemCount int trackItemCount 仅当 > 1 时写 需要携带的数量
completeOnChecklist boolean completeOnChecklist 总是 清单全勾后自动完成,默认 false
showOnMap boolean showOnMap 总是 是否在 Navigator 地图上显示,默认 false
parentId UUID(可空) parentMost / parentLeast 仅在非空时写 父任务;只允许一层嵌套
order int order 总是 同级手动排序位置,小的在前
checklist List<ChecklistItem> checklist 总是 清单项,网络侧上限 200 条
comments List<Comment> comments 总是 评论,网络侧上限 50 条

trackItemCount 单独存的原因:ItemStack 的 Count 只有一个字节(上限 64),装不下 1~2304 的范围,因此数量与物品栈分开保存,并由 clampTrackItemCount() 统一夹取。

MAX_TRACK_ITEM_COUNT = 2304(36 * 64)。源码注释说明:一个主物品栏有 36 格、每格最多 64 个,再多的数量永远无法达成;堆叠上限低于 64 的物品自然到不了这个数。

order 的编号域:位置在「同一个分组内」独立编号,分组 = 一个标签页的根任务,或一个父任务的子任务。Task.endOrder() 算出同组内现有最大位置 + 1,服务端与客户端都用它放置新任务,保证客户端显示的位置就是服务端存档的位置。

TaskStatus — 状态枚举

data/TaskStatus.java,三个常量,顺序即网络封包用的 ordinal:

  • OPEN — lang 键 tasknh.status.open → “Open”(待办标签页)
  • IN_PROGRESS — tasknh.status.in_progress → “In Progress”(进行中标签页)
  • DONE — tasknh.status.done → “Done”(完成标签页)

fromNBT(String) 用 valueOf 解析,失败则回退到 OPEN 并打 warn 日志。源码注释:模组更新重命名枚举值后,旧存档里会出现未知状态。

AssignedPlayer — 负责人

data/AssignedPlayer.java,Java record(@Desugar,由 jabel 在 Java 8 目标下编译)。

字段 NBT 键 说明
playerId (UUID) most / least 玩家 UUID(注意是 most/least,不是 Task 的 idMost/idLeast)
assignedAt (long) assignedAt 分配时间戳(System.currentTimeMillis());hasKey 为假时回退 0L

assignedAt 与存档里的 playerLastSeen 比较,用来在玩家下次登录时列出「新分配」的任务。

ChecklistItem — 清单项

data/ChecklistItem.java。

字段 NBT 键 写入条件 含义
id (UUID) idMost / idLeast 总是 项唯一标识
title title 总是 标题,网络侧上限 256 字符
checked checked 总是 是否已勾选
trackItem (ItemStack 可空) trackStack 仅非空时写 携带该物品即自动勾选
trackItemCount (int) trackItemCount 仅 > 1 时写 需携带数量,同样走 clampTrackItemCount()
trackOre (String) trackOre 仅非空时写 矿辞典名;空串表示对 trackItem 精确匹配

trackOre 的来源:只有 BetterQuesting 导入会设置它——从 NEI 拖进来的栈只指向一个具体物品而非一个标签。设定后该矿辞典名下的任何物品都算满足,而 trackItem 仍保留作为槽位绘制的显示物品。读取时长度上限 256(与网络侧一致)。

加载兼容:在追踪功能存在之前保存的清单项没有 trackStack / trackItemCount / trackOre 这三个键,加载后即为「不追踪」状态。

Comment — 评论

data/Comment.java,Java record:author(String,网络上限 64)、timestamp(long)、text(String,网络上限 2048)。NBT 键同名 author / timestamp / text。无独立 id 字段。

添加时软上限 50 条(Task.comments 字段注释);网络侧 readFromBuf 对 commentCount > 50 直接抛 IOException。

TaskLocation — 坐标

data/TaskLocation.java:NBT 键 dim(int)、x / y / z(int)、label(String,网络上限 256)。

⚠️ 导出格式与 NBT 格式的字段顺序不一致:TaskLocation.toNBT() 写 dim 在前;/tasknh export 的 JSON 写 x / y / z / dimension / label,而 import 处构造 TaskLocation 的实参顺序是 (x, y, z, dimension, label)——与构造器签名 (dimension, x, y, z, label) 相反。源码中 import 分支调用的是全限定名 new com.eldrinn.tasknh.data.TaskLocation(loc.get("x"), loc.get("y"), loc.get("z"), loc.get("dimension"), ...),因此导入的任务坐标与维度是错位的。详见 导出与导入。

网络侧长度与数量上限

Task.readFromBuf() 中所有上限(超限抛 IOException,而非静默截断):

  • title 256、description 4096
  • assigneeCount 0–100
  • trackItemCount 1–2304
  • checklistCount 0–200
  • commentCount 0–50
  • status 按 ordinal 索引,越界抛 IOException

读取物品栈走 Task.readStack():调 readItemStackFromBuffer() 后再检查 stack.getItem() != null,为假则按「无物品」处理。源码注释解释:注册表里没有的 id 会构造出一个背后没有物品的栈,第一次绘制或取名就会抛异常。

向后兼容:两代旧格式

Task 保留了三条旧键兼容路径,全部在 fromNBT() 内实现,读到旧键后下次保存即改写为新格式:

  1. 物品:旧键 iconItem / trackItem 是 "modid:item:meta" 字符串,由 parseLegacyItem() 解析;新键 iconStack / trackStack 存在时优先。parseLegacyItem 在分段少于 3 段时返回 null。
  2. 清单改名:旧键 subtasks,新键 checklist。String checklistTag = tag.hasKey("checklist") ? "checklist" : "subtasks";
  3. 缺省回退:trackItemCount 无键时取 1;completeOnChecklist / showOnMap 无键时 getBoolean 返回 false;order 无键时取 0(全部为 0,因而保持原有顺序)。

trackItemCount 在加载时总是过一遍 clampTrackItemCount()。源码注释:手工编辑过的存档可能存着任意数值,而越界的数量在客户端解码时会失败,所以载入时夹取。

自动完成判定

shouldCompleteOnChecklist() 返回 completeOnChecklist && 清单非空 && 每一项都已勾选。源码注释特别说明空清单永不完成:空清单在平凡意义上「每一项都勾了」,否则一勾上该标志任务就会立刻完成。

completeOnChecklist 的判定放在服务端执行(UpdateTaskPacket.executeServer),而不是客户端。源码注释:这样物品追踪勾上最后一个框时,走的是同一条路径。