导出与导入
/tasknh export 与 /tasknh import,实现在 command/TaskNHCommand.java 的 case "export" / case "import" 分支。这是 TaskNH 唯一的数据文件外流途径——除存档 NBT 外,唯一会写到磁盘的自定义格式。
文件位置与命名
| 项 | 值 |
|---|---|
| 目录 | <世界存档目录>/tasknh(MinecraftServer.getEntityWorld().getSaveHandler().getWorldDirectory() + "tasknh") |
| 文件名 | <name>.json,name 由玩家在命令里给 |
export 默认名 |
export(args.length >= 2 ? args[1] : "export"),即 export.json |
| 编码 | StandardCharsets.UTF_8(读写两侧都显式指定) |
| 格式 | Gson setPrettyPrinting(),顶层是一个 JSON 数组 |
export 会在目录不存在时 mkdirs();两侧都做规范化路径校验以防路径穿越,见 命令。
导出 JSON 结构
顶层数组,每项一个任务:
| 键 | 类型 | 写入条件 |
|---|---|---|
id |
string | 总是(原样保留 UUID) |
title |
string | 总是 |
description |
string | 总是 |
status |
string | 总是(TaskStatus.name()) |
iconStack |
string | 仅在有图标时 |
trackStack |
string | 仅在有追踪物品时 |
trackItemCount |
number | 仅当 > 1 |
completeOnChecklist |
boolean | 仅当为真(addProperty("completeOnChecklist", true)) |
showOnMap |
boolean | 总是 |
location |
object | 仅在有坐标时,含 x / y / z / dimension / label |
checklist |
array | 总是 |
不导出的字段:assignees(负责人)、parentId(父任务)、order(排序位置)、comments(评论)。所以导出件是无主的任务模板,不是存档备份。
checklist 每项:
| 键 | 写入条件 |
|---|---|
title |
总是 |
checked |
总是 |
trackStack |
仅在有追踪物品时 |
trackItemCount |
仅当 > 1 |
trackOre |
仅当非空 |
清单项的 id 不导出——导入时一律 UUID.randomUUID()。
物品用 NBT 文本承载
stackToJson(ItemStack) 的做法:
NBTTagCompound tag = stack.writeToNBT(new NBTTagCompound());
tag.setString("id", String.valueOf(Item.itemRegistry.getNameForObject(stack.getItem())));
return tag.toString();
即物品以完整 NBT 文本(一个 JSON 字符串值)写出,源码注释给了两条理由:
- 「以 NBT 写,因此仅 tag 不同的物品能完整往返」——
iconItem的字段注释也是同样意思 - 「物品 id 写成注册名:栈里带的数字是逐世界发放的,所以一份导出移到另一个实例上否则会指向另一个物品」
stackFromJson(String) 是逆操作,用 JsonToNBT.func_150315_a 解析;id 键若是字符串类型,就把它反查成世界自己的数字 id(兼容「注册名加入之前写出的导出」),然后 ItemStack.loadItemStackFromNBT。捕获 NBTException 与 RuntimeException 两种——注释解释 JsonToNBT 是手写遍历文本,遇到畸形输入会抛普通运行时异常,否则会让整个导入中途夭折。
导入行为
import 逐条 new Task(UUID.randomUUID(), ...)——一律新建 UUID,不复用导出文件里的 id。因此重复导入同一份文件会产生重复任务。
- 标题缺失会抛异常(
obj.get("title").getAsString()无保护),被外层catch (Exception e)捕获 → 回tasknh.cmd.import_failed并带异常消息 - 状态用
TaskStatus.valueOf(obj.get("status").getAsString()),未知状态名会让整份导入失败(不像存档读取那样回退OPEN) - 全程包在一个大
try { ... } catch (Exception e)里 - 结束后
data.addTask(team.getTeamId(), t)逐条加入,最后统一推一次SyncAllTasksPacket,回tasknh.cmd.imported带条数
导入的兼容分支
导入侧接受三代格式,与存档侧一致:
- 物品:优先
iconStack/trackStack,无则回落旧的"modid:item:meta"字符串键iconItem/trackItem(走Task.parseLegacyItem) - 清单:优先
checklist键,无则用旧的subtasks键 trackItemCount存在时过Task.clampTrackItemCount();label缺失时用空串trackOre长度硬截断:超过 256 字符就置空串。源码注释说明原因「The packet reads at most 256 characters, so a longer name would break the sync.」
⚠️ 源码缺陷:导入的坐标参数顺序反了
TaskLocation 的构造器签名是 (int dimension, int x, int y, int z, String label)(data/TaskLocation.java:14)。
但 import 分支的调用(command/TaskNHCommand.java:426 附近)是:
t.location = new com.eldrinn.tasknh.data.TaskLocation(
loc.get("x").getAsInt(),
loc.get("y").getAsInt(),
loc.get("z").getAsInt(),
loc.get("dimension").getAsInt(),
loc.has("label") ? loc.get("label").getAsString() : "");
实参顺序是 x, y, z, dimension,与形参顺序 dimension, x, y, z 相反。后果:导入出来的任务,其 dimension 字段拿到的是原 X 坐标,其 x 拿到的是原 Y 坐标,y 拿到原 Z 坐标,z 拿到原维度 id。
由于 JSON 侧键名是对的(x / y / z / dimension),这个错位不会在文件里体现,只在内存中的 Task 上体现;而地图图层读的正是内存字段(见 Navigator 地图图层),所以带 location 的导出件导入后,地图标记会落到错误维度与错误坐标。
存档 NBT 侧没有这个问题(TaskLocation.toNBT / fromNBT 键名与赋值一一对应)。