节点存档(nodes.json)
本 mod 全部数据的落盘位置:一个按世界隔离的 nodes.json 文件。
路径
<游戏目录>/TCNodeTracker/<worldDir>/nodes.json
TCNodeTracker来自Constants.MODFOLDER(lib/Constants.java:7),注意它与 modidtcnodetracker大小写不同。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):
- 写:
DateTimeFormatter.ISO_INSTANT.format(src) - 读:
- 先试
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_] 字符,建目录存的是原始名(单人分支未净化)所以能对上;
但这依赖于两者恰好一致,不是显式契约。
相关条目
- 节点扫描 - 往这个文件里写记录的操作
- 节点列表界面 - 删除记录时会触发重写
- Navigator 地图图层整合 - 删除记录的另一条路径