节点存档(nodes.json)

本 mod 全部数据的落盘位置:一个按世界隔离的 nodes.json 文件。

路径

<游戏目录>/TCNodeTracker/<worldDir>/nodes.json
  • TCNodeTracker 来自 Constants.MODFOLDER(lib/Constants.java:7),注意它与 modid tcnodetracker 大小写不同。
  • jsonPath 在连上服务器时设置(events/ClientConnectionEvent.java:51)。

worldDir 的取值(ClientConnectionEvent.java:24-33)

场景 worldDir 例子
单人游戏(event.isLocal) Minecraft.getIntegratedServer().getFolderName() New World
单人游戏但取不到 IntegratedServer 字面量 sp_world sp_world
多人服务器 Utils.invalidChars(hostName + "_" + port) example.com_25565

Utils.invalidChars(lib/Utils.java:14-17)把正则 [^a-zA-Z0-9_] 的每个匹配字符替换成 _。

目录迁移

ClientConnectionEvent.java:37-49 存在一次旧目录改名迁移:

Path storagePath = storagePathRoot.resolve(worldDir);              // 新路径:原始 worldDir
Path storagePath_old = storagePathRoot.resolve(Utils.invalidChars(worldDir));  // 旧路径:净化后

if (Files.notExists(storagePath)) {
    if (Files.exists(storagePath_old)) {
        Files.move(storagePath_old, storagePath);
    } else {
        Files.createDirectories(storagePath);
    }
}

即:旧版本会把 worldDir 净化后再建目录,新版本改用原始 worldDir,首次连接时把旧目录 move 过来。 只在新路径不存在时才动作,所以是幂等的。

JSON 结构

由 Gson 序列化整个 TCNodeTracker.nodelist(List<NodeList>),setPrettyPrinting()。 每条记录对应 lib/NodeList.java 的一个字段:

[
  {
    "aspect": { "aer": 64, "aqua": 20, "ignis": 32 },
    "type": "NORMAL",
    "mod": "BRIGHT",
    "dim": 0,
    "x": 123,
    "y": 64,
    "z": -456,
    "date": "2026-08-02T15:03:56Z"
  }
]
字段 类型 含义
aspect HashMap<String, Integer> 侧重点 tag → 数值。任意侧重点都可能进来,不限于 6 个主侧重点
type String 节点类型,来自 INode.getNodeType().toString()
mod String 节点修饰符,来自 INode.getNodeModifier().toString();无修饰符时为字面量 BLANK
dim int 维度 ID,worldObj.provider.dimensionId
x / y / z int 方块坐标
date String ISO-8601 Instant,记录(最后更新)时刻

写入用 FileWriter 直接覆盖(lib/JsonUtils.java:65-69),失败只 LOGGER.error 不抛出。 TCNodeTracker.jsonPath 为 null 时会 NullPointerException —— 但路径在连服时必设。

date 字段的向后兼容

Instant 有自定义序列化器/反序列化器(lib/JsonUtils.java:30-57):

  1. 写:DateTimeFormatter.ISO_INSTANT.format(src)
  2. 读:
    • 先试 ISO_INSTANT.parse
    • 失败则置 needsSaving = true,再试 ofLocalizedDateTime(FormatStyle.MEDIUM).parse(旧格式)
    • 仍失败则 LOGGER.warn("Could not parse saved datetime: " + json) 并放弃解析,返回 Instant.now()(不抛异常)

readJson 末尾(JsonUtils.java:78-81)若 needsSaving 为真则立即 writeJson() 重写整个文件, 把旧格式统一成 ISO-8601。这个重写只做一次(重写后字段已是 ISO 格式,下次读不会再触发)。

首次连接的初始化

ClientConnectionEvent.java:52-58:

TCNodeTracker.nodelist.clear();
// Create empty JSON file if none exists yet to prevent log spam
if (Files.notExists(TCNodeTracker.jsonPath)) {
    JsonUtils.writeJson();
}
JsonUtils.readJson();

即先 clear() 掉上一个世界的内存数据,再按需建空文件,最后读入。建空文件是为了避免 readJson 每次连接都抛 NoSuchIOException 刷日志。

存档删除联动

events/SaveDeletionEvent.java:20-32 监听 GTNHLib 的 WorldDeletionEvent (com.gtnewhorizon.gtnhlib.client.event.WorldDeletionEvent),在玩家从单人游戏世界选择界面删除世界时, 用 org.apache.commons.io.FileUtils.deleteDirectory 删掉整个 <游戏目录>/TCNodeTracker/<worldName>。

⚠️ 这里用的是 event.worldName 原始字符串,没有经过 Utils.invalidChars, 与建目录时单人游戏分支用的 getFolderName() 也不是同一个取值来源。 若世界名含非 [a-zA-Z0-9_] 字符,建目录存的是原始名(单人分支未净化)所以能对上; 但这依赖于两者恰好一致,不是显式契约。

相关条目