导出与导入

/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 字符串值)写出,源码注释给了两条理由:

  1. 「以 NBT 写,因此仅 tag 不同的物品能完整往返」——iconItem 的字段注释也是同样意思
  2. 「物品 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 键名与赋值一一对应)。