数据文件
本 mod 有 3 类数据文件,全部位于 config/JustEnoughCalculation/ 下,
用 NBTTagCompound 序列化成 JSON(Utilities.Json.read/write)。
基本信息
| 属性 | 值 |
|---|---|
| 根目录 | <config>/JustEnoughCalculation/(JecaConfig.configDir) |
| 访问类 | me.towdium.jecalculation.JecaConfig(路径常量)+ Controller(读写逻辑) |
| 序列化格式 | NBTTagCompound → JSON(Utilities.Json) |
| 玩家级 | 无——不写 playerdata/<UUID>.dat,全部落在 config 目录 |
| 读写时机 | ClientProxy.postInit() → Controller.loadFromLocal();界面改动后 Controller.writeToLocal() |
三类文件
| 文件 | 常量 | 内容 | 谁写 |
|---|---|---|---|
main.cfg |
JecaConfig.configFile |
4 个配置项 | JecaConfig.preInit |
record.json |
JecaConfig.recordFile |
会话状态(4 段) | Controller.writeToLocal() |
default.json |
JecaConfig.defaultFile |
默认配方库 | 只读(Recipes() 无参构造) |
data/<group>.json |
JecaConfig.getDataFile(group) |
玩家自建的配方分组 | Controller.export(group) / export() |
JecaConfig.dataDir = <config>/JustEnoughCalculation/data/,
loadFromLocal() 里会 JecaConfig.dataDir.mkdirs() 确保存在。
⚠️
main.cfg的 4 个配置项全部无效,详见 配置。
record.json 的 4 个顶层键
Controller 定义了 4 个键常量:
| 键 | 类型 | 内容 |
|---|---|---|
KEY_GROUPCRAFT = "groupCraft" |
RecordGroupCraft |
配方分组列表(含每组名与索引) |
KEY_CRAFT = "craft" |
RecordCraft |
当前合成会话状态 |
KEY_PLAYER = "player" |
RecordPlayer |
玩家配方库 + 上次编辑的组名 |
KEY_MATH = "math" |
RecordMath |
数学计算器状态 |
writeToLocal() 的写入顺序:groupCraft → craft → player → math。
player 段(RecordPlayer)
| 键 | 类型 | 内容 |
|---|---|---|
KEY_RECIPES = "recipes" |
Recipes |
玩家全部配方(按组名分组) |
KEY_LAST = "last" |
String |
上次编辑的组名;为 null 时不写入 |
if (last != null) ret.setString(KEY_LAST, last);
ret.setTag(KEY_RECIPES, recipes.serialize());
RecordPlayer 无参构造会 recipes = new Recipes()——这会去读 default.json。
无参构造仅在 nbt 为 null(文件不存在)时被 loadFromLocal() 调用。
craft 段(RecordCraft)
| 键 | 类型 | 内容 |
|---|---|---|
KEY_RECENTS = "recents" |
NBTTagList |
最近使用过的标签 |
KEY_AMOUNT = "amount" |
String |
数量输入框的原始文本 |
KEY_INVENTORY = "inventory" |
boolean |
背包读取开关(对应界面上的 BTN_INV_E / BTN_INV_D 按钮) |
KEY_MODE = "mode" |
String |
显示模式名(Mode 枚举的 name()) |
反序列化 KEY_MODE 时会做一次名称解析,失败则回落到 Mode.INPUT。
KEY_AMOUNT 存的是字符串而非数字,因为它是输入框的原始文本。
math 段(RecordMath)
| 键 | 类型 | 内容 |
|---|---|---|
KEY_CURRENT = "current" |
String |
当前输入缓冲(未解析) |
KEY_LAST = "last" |
— | 上一次结果 |
KEY_OPERATOR = "operator" |
— | 待执行的运算 |
KEY_STATE = "state" |
— | 显示状态 |
见 数学显示模式。
配方库格式(Recipes / Recipe)
Recipes 持有一个 HashMap<String, List<Recipe>> records,键按字母序排序后反序列化:
Set<String> keySet = (Set<String>) nbt.func_150296_c();
keySet.stream().sorted().forEach(i -> { ... });
每条 Recipe 有 3 个 NBTTagList(NBT 类型 10 = compound):
| 键 | 语义 |
|---|---|
KEY_INPUT = "input" |
原料 |
KEY_OUTPUT = "output" |
产物 |
KEY_CATALYST = "catalyst" |
催化剂 |
序列化时会裁掉尾部连续的 EMPTY 标签:
for (int i = ls.size() - 1; i >= 0; i--) {
if (start || ls.get(i) != ILabel.EMPTY) {
labels.add(ls.get(i));
start = true;
}
}
之后用一个 Utilities.ReversedIterator 反向写回——即去掉尾部空槽后再翻转顺序,
使第一个产物成为数组第一个元素(因为 Recipe.getRep() 取 output.get(0) 作主产物)。
每条非法配方会被捕获并只记警告不中断加载:
try { add(i, new Recipe((NBTTagCompound) r)); }
catch (IllegalArgumentException e) {
JustEnoughCalculation.logger.warn("Invalid recipe record :" + r);
}
default.json 的加载
Recipes 无参构造:
File file = JecaConfig.defaultFile;
NBTTagCompound nbt = Utilities.Json.read(file);
if (nbt == null) JustEnoughCalculation.logger.info("Failed to load default records at " + file + ".");
else { JustEnoughCalculation.logger.info("Loading default records at " + file + "."); deserialize(nbt); }
失败只 info 级别记一条日志,不影响启动。
本仓库的
src/main/resources下没有default.json模板文件, 它需要玩家或整合包自行提供。
导入 / 导出
Controller.discover() 列出 data/ 下所有文件并读成 List<Pair<String, Recipes>>,
读失败的文件会被 filter(i -> i.two != null) 掉并打 warn。
| 方法 | 行为 |
|---|---|
export(group) |
导出单组到 data/<group>.json |
export() |
导出全部到 data/groups.json(组名写死为 "groups") |
inport(recipes, group) |
导入,注意方法名源码里就是拼错的 inport |
导出成功后聊天栏发 jecalculation.chat.export(带文件绝对路径作为 %s 参数)。
两个 export 重载都先取 Utilities.getPlayer(),为 null 直接静默返回。
inport 会先过滤掉重复项再逐条加入:
recipes.getGroup(group).stream().filter(i -> !hasDuplicate(i)).forEach(buffer::add);
for (Recipe r : buffer) addRecipe(group, r);
loadFromLocal 的占位符特殊处理
boolean s = LPlaceholder.state;
LPlaceholder.state = true;
if (nbt != null) { /* 反序列化 4 段 */ } else { /* 全部 new 空记录 */ }
LPlaceholder.state = s;
反序列化前把 LPlaceholder.state 置 true,结束后还原为原值。
这是为了让占位符标签能在读档时正确解析出百分比信息(见
标签类型与合并)。
当 record.json 不存在或解析为 null 时,四段全部用空构造:
| 记录 | 兜底 |
|---|---|
rGroupCraftClient |
new RecordGroupCraft(new NBTTagCompound()) |
rCraftClient |
new RecordCraft(new NBTTagCompound()) |
rMathClient |
new RecordMath(new NBTTagCompound()) |
rPlayerClient |
new RecordPlayer() ← 无参,会去读 default.json |
访问转换器的初始化
ClientProxy.postInit() 的顺序有讲究:
ILabel.initServer(); // 1
ClientCommandHandler.instance.registerCommand(new JecaCommand());
ILabel.initClient(); // 2 注册 MERGER / CONVERTER / EDITOR
Controller.loadFromLocal(); // 3 读档(需要 2 已完成)
NEIPlugin.init(); // 4
loadFromLocal() 必须排在 ILabel.initClient() 之后,
因为反序列化要靠 ILabel.SERIALIZER 按 "type" 键分派到正确的标签类。