模型数据集与模型项结构
Ark-Pets 通过一个中央化的模型数据集(ModelsDataset)来描述可用的桌面宠物模型:它从 models_data.json 反序列化而来,内部以 ModelItemGroup/ModelItem 的层级组织每一个模型条目,并为 UI 展示、文件校验与启动提供了统一的数据源。本页讲解该数据集的结构、反序列化与预处理机制,以及 ModelItem 的字段语义。
目的与范围
本页覆盖以下内容(以 core/src/cn/harryh/arkpets/assets/ModelsDataset.java 为核心证据):
ModelsDataset的顶层字段结构与语义(storageDirectory、sortTags、gameDataVersionDescription、gameDataServerRegion、data、arkPetsCompatibility)。- 基于 fastjson2 的两段式反序列化设计(
JSONObject→ModelsDatasetBean→ModelsDataset)。 - 构造期对
ModelItem的预处理:key回填、assetDir目录拼接、旧版数据集的assetList兼容回填。 ModelItem与ModelItemGroup的结构(以ModelsDataset.java中被引用的字段/方法为准)。- 与数据集相关的文件常量(
models_data.json、ArkModels压缩包、临时解压目录)。 - desktop 侧消费数据集的入口(
ArkHomeFX、ModelsModule、VerifyModelsTask、UnzipModelsTask)。
以下相关主题有意留给兄弟页面,本页仅在边界处提及:
- 模型压缩包的下载/解压 GUI 任务链与进度提示细节 → 见「GUI 后台任务」相关页面。
ModelsModule列表 UI 的完整交互(搜索、选中、启动模型的渲染流程)→ 见模型列表 UI 相关页面。ArkConfig应用配置与启动参数 → 见配置管理相关页面。
概述
Ark-Pets 的模型资产以 ArkModels 仓库形式分发(常量 mirrorChyanModelRepoRID = "ArkModelsRepo",见 Const.java)。压缩包解压后,根目录下的 models_data.json(常量 fileModelsDataPath)是整个模型库的"清单文件",它描述了:
- 存储目录映射(
storageDirectory):模型类型(如avatar/monster等游戏内分类)到磁盘目录的映射; - 排序标签(
sortTags):供 UI 展示用的标签文案与顺序; - 游戏数据版本信息:
gameDataVersionDescription、gameDataServerRegion; - 模型条目集合(
data):key → 模型条目 JSON 的映射; - 兼容性版本号(
arkPetsCompatibility):声明该数据集要求的最低 Ark-Pets 版本。
ModelsDataset 类将这份 JSON 清单转换为强类型的、经过预处理并排序的运行时对象,挂载在 ArkHomeFX.modelsDataset 字段上(见 ArkHomeFX.java),供桌面端模型列表、搜索与校验流程共享。
设计意图:把"清单解析 + 目录拼接 + 版本兼容回填"集中到 core 模块的单个构造函数中完成,使得 desktop 侧的所有消费者拿到的 ModelItem 都是"开箱即用"的——每个条目都已经带上了绝对的 assetDir 目录和规范化的 assetList 文件映射,无需各自再做路径推导或兼容处理。
架构
分层说明:
- 模型仓库层:Ark-Pets 自身不生成模型,只消费 ArkModels 仓库产物。
UnzipModelsTask(继承UnzipTask)负责把下载到的 zip 解压到临时目录,见 UnzipModelsTask.java。 - core 数据层:
ModelsDataset是纯 Java 数据结构(无 JavaFX 依赖),可以同时被 core 与 desktop 使用。它通过内部静态 Bean 类完成 JSON 桥接,并在构造函数中完成全部校验与预处理。 - desktop 消费层:
ArkHomeFX作为应用根上下文持有数据集实例;ModelsModule(FXML 控制器)负责初始化数据集并在列表中展示/搜索;VerifyModelsTask以数据集为输入校验本地模型文件完整性,见 VerifyModelsTask.java。
类型结构
注意:上图中
ModelItem的字段列表来自ModelsDataset构造函数中实际读写的成员(key、type、assetDir、assetList、assetId、checksum与静态extensions)。ModelItem与ModelItemGroup的完整定义位于同包的 ModelItem.java 与 ModelItemGroup.java,本页只保证列出在ModelsDataset.java中被验证过的部分。
核心实现:反序列化与预处理流程
ModelsDataset 的构造流程是本页最核心的控制流。它分为两段:外部 JSON 入口与内部 Bean 转换。
两段式构造设计
1public ModelsDataset(JSONObject jsonObject) {
2 this(jsonObject.toJavaObject(ModelsDatasetBean.class));
3}
4
5protected ModelsDataset(ModelsDatasetBean bean) {
6 Objects.requireNonNull(bean);
7 // ... 校验与预处理
8}Source: ModelsDataset.java
第一段构造器接受一个 fastjson2 的 JSONObject,立即转换为内部 ModelsDatasetBean;第二段(protected)构造器做真正的校验、类型转换与预处理。设计意图:ModelsDatasetBean 只承担"JSON 字段名 ↔ Java 字段"的映射职责(通过 @JSONField 标注的 setter),而业务校验与派生字段计算完全隔离在 ModelsDataset 构造器里。这样公共 API 只有 JSONObject 一个入口,Bean 作为受保护的实现细节不对外暴露。
逐阶段控制流
storageDirectory:类型 → 磁盘目录
1storageDirectory = new HashMap<>();
2if (bean.storageDirectory == null || bean.storageDirectory.isEmpty())
3 throw new DatasetKeyException("storageDirectory");
4for (String key : bean.storageDirectory.keySet())
5 storageDirectory.put(key, Path.of(bean.storageDirectory.get(key)).toFile());Source: ModelsDataset.java
JSON 中该字段是 HashMap<String, String>(相对路径字符串),构造时逐项转换为 java.io.File。快速失败:清单缺少该键直接抛出 DatasetKeyException——没有存储目录映射,后续所有 assetDir 都无法推导,因此宁可立即失败也不允许半初始化的数据集流入系统。
data:模型条目集合与预处理
1if (bean.data == null || bean.data.isEmpty())
2 throw new DatasetKeyException("data");
3data = new ModelItemGroup();
4for (String key : bean.data.keySet()) {
5 // Pre deserialization
6 ModelItem modelItem = bean.data.get(key).toJavaObject(ModelItem.class);
7 // Make up for `assetDir` field
8 if (modelItem == null || !storageDirectory.containsKey(modelItem.type))
9 throw new DatasetKeyException("type");
10 modelItem.key = key;
11 modelItem.assetDir = Path.of(storageDirectory.get(modelItem.type).toString(), key).toFile();
12 // Compatible to lower version dataset
13 if (modelItem.assetList == null && modelItem.assetId != null && modelItem.checksum != null) {
14 HashMap<String, Object> defaultFileMap = new HashMap<>();
15 for (String fileType : ModelItem.extensions)
16 defaultFileMap.put(fileType, modelItem.assetId + fileType);
17 modelItem.assetList = new JSONObject(defaultFileMap);
18 }
19 data.add(modelItem);
20}
21data.sort();Source: ModelsDataset.java
这段循环承担了四项职责,每一项都对应一个真实的数据完整性需求:
- 反序列化(Pre deserialization):每个条目在 JSON 中仍是
JSONObject,先转为ModelItem,使后续可以用类型安全的方式访问字段。 - 回填
key:JSON 对象的键名(如模型唯一标识)在反序列化成ModelItem后会丢失(它不是条目内部的字段),因此显式写回modelItem.key = key。这保证了运行时每个模型都能自报家门,供 UI 列表、持久化选中项使用。 - 回填
assetDir:模型资产目录 =storageDirectory[type] / key。这是一次刻意的冗余派生——JSON 清单中不存绝对目录,而是"类型目录 + 模型键"的规则推导,避免仓库在移动/换机器后路径失效;同时校验type必须存在于storageDirectory,防止清单内部自相矛盾。 - 旧版兼容回填
assetList:旧格式数据集没有assetList(多文件映射),只有单一的assetId与checksum。此时按ModelItem.extensions(模型文件扩展名数组,如.skel/.atlas/.png等约定后缀)拼出"文件类型 → 文件名"的默认映射(文件名 =assetId + 后缀)。设计意图:让新代码只面向统一的assetList抽象,旧数据在加载边界处被一次性升级,而不是在每个消费点写if (assetList == null)分支。
循环结束后调用 data.sort()——ModelItemGroup 聚合并排序所有条目,保证模型列表在 UI 中呈现稳定、确定的顺序(排序规则定义于 ModelItemGroup/ModelItem 内部)。
兼容性版本号
arkPetsCompatibility = new Version(bean.arkPetsCompatibility);Source: ModelsDataset.java
JSON 中为 int[] 数组,运行时转为 core 工具类 cn.harryh.arkpets.utils.Version,用于与当前应用版本比较,判断数据集是否被当前 Ark-Pets 版本支持。
JSON 桥接 Bean
1protected static class ModelsDatasetBean implements Serializable {
2 private HashMap<String, String> storageDirectory;
3 private HashMap<String, String> sortTags;
4 private String gameDataVersionDescription;
5 private String gameDataServerRegion;
6 private HashMap<String, JSONObject> data;
7 private int[] arkPetsCompatibility;
8
9 @JSONField
10 public void setData(HashMap<String, JSONObject> data) {
11 this.data = data;
12 }
13 // ... 其余 @JSONField setter 同构
14}Source: ModelsDataset.java
Bean 实现了 Serializable,字段全部私有且只暴露 fastjson2 setter 注入。data 的类型刻意是 HashMap<String, JSONObject> 而非 HashMap<String, ModelItem>:因为需要在外层拿到 key 后才能完整初始化 ModelItem(回填 key/assetDir),所以把条目级反序列化延迟到 ModelsDataset 构造器的循环里完成。
数据模型(models_data.json 顶层结构)
结合 ModelsDatasetBean 的字段定义,清单文件的顶层结构与运行时语义如下:
| JSON 键 | Java 类型(Bean) | 运行时类型(Dataset) | 必填 | 说明 |
|---|---|---|---|---|
storageDirectory | HashMap<String, String> | HashMap<String, File> | ✅(非空) | 模型类型 → 资产子目录(相对数据集根目录)映射 |
sortTags | HashMap<String, String> | HashMap<String, String> | ❌ | 标签排序/文案映射,供 UI 分组展示 |
gameDataVersionDescription | String | String | ❌ | 提取该批模型的游戏数据版本描述 |
gameDataServerRegion | String | String | ❌ | 游戏数据来源服务器区域 |
data | HashMap<String, JSONObject> | ModelItemGroup(已排序) | ✅(非空) | 模型键 → 模型条目;运行时逐条转为 ModelItem 并回填 key/assetDir/assetList |
arkPetsCompatibility | int[] | Version | ❌ | 最低兼容的 Ark-Pets 版本(如 [3,0,0]) |
ModelItem(经 ModelsDataset.java 验证的字段子集):
| 字段 | 类型 | 来源 | 说明 |
|---|---|---|---|
key | String | 构造期回填 | data 映射中的键名,即模型唯一标识 |
type | String | JSON | 模型类型,必须存在于 storageDirectory,否则抛 DatasetKeyException("type") |
assetDir | File | 构造期回填 | storageDirectory[type] / key 派生的资产目录 |
assetList | JSONObject | JSON 或兼容回填 | 文件类型 → 文件名映射;旧版数据集由 assetId + extensions 生成 |
assetId | String | JSON | 旧格式使用的资产基础文件名(不含扩展名) |
checksum | String | JSON | 旧格式使用的校验和,供 VerifyModelsTask 校验完整性 |
extensions | String[](静态) | 代码常量 | ModelItem 支持的模型文件扩展名数组 |
相关文件常量
数据集在磁盘上的落点由 Const.FilePath(core 常量类)集中定义:
| 常量 | 值 | 用途 |
|---|---|---|
tempDirPath | temp/ | 下载/解压工作目录根 |
fileModelsZipName | ArkModels | 模型仓库压缩包文件名(不含扩展名) |
fileModelsDataPath | models_data.json | 数据集清单文件在解压后目录内的相对路径 |
tempModelsUnzipDirPath | temp/models_unzipped/ | 压缩包解压目标目录 |
mirrorChyanModelRepoRID | ArkModelsRepo | MirrorChyan 下载源中模型仓库的资源 ID |
mirrorChyanRID | ArkPetsApp | MirrorChyan 中 Ark-Pets 应用本体资源 ID |
Source: Const.java
这些常量把"文件名/路径"从业务代码中抽离,解压流程(UnzipModelsTask)与清单解析(ModelsDataset)因此解耦于具体路径约定。
使用示例
desktop 侧:持有与初始化数据集
ArkHomeFX(JavaFX 应用根控制器)把数据集作为应用级共享状态持有,ModelsModule 在初始化时填充它:
public ArkConfig config;
public ModelsDataset modelsDataset;Source: ArkHomeFX.java
1public boolean initModelsDataset(boolean doPopNotice) {
2 try {
3 // 读取并解析 models_data.json → ModelsDataset
4 // (完整实现见 ModelsModule;此处为方法签名与职责入口)
5 ...
6 } catch (Exception e) {
7 ...
8 }
9}Source: ModelsModule.java
initModelsDataset(boolean doPopNotice) 是数据集进入 UI 生命周期的正式入口:返回布尔值表示初始化成败,doPopNotice 控制失败时是否弹出提示。失败路径(文件缺失、JSON 损坏、键校验失败)都会被捕获并转化为用户可见的反馈,而不是让应用崩溃。
desktop 侧:基于数据集的搜索过滤
1public void modelSearch(String keyWords) {
2 modelListView.getItems().clear();
3 ...
4}Source: ModelsModule.java
modelSearch 清空列表后按关键词重新填充条目——它读取的正是 ArkHomeFX.modelsDataset.data 中已排序的 ModelItem 集合。这解释了为什么 ModelsDataset 构造期就执行 data.sort():排序一次,所有后续列表渲染/搜索结果都天然有序。
desktop 侧:以数据集为输入的完整性校验任务
1public class VerifyModelsTask extends GuiTask {
2 protected final ModelsDataset modelsDataset;
3
4 public VerifyModelsTask(StackPane parent, GuiTaskStyle style, ModelsDataset modelsDataset) {
5 super(parent, style);
6 ...
7 }
8}Source: VerifyModelsTask.java
VerifyModelsTask 在后台线程遍历数据集中每个 ModelItem 的 assetDir/assetList 指向的文件,核对存在性与(旧格式的)checksum。这是构造期回填 assetDir 的直接收益:校验任务无需理解 JSON 路径规则,直接读派生好的 File 即可。
desktop 侧:解压任务产出数据集载体
1public class UnzipModelsTask extends UnzipTask {
2 public UnzipModelsTask(StackPane parent, GuiTaskStyle style, String zipPath) {
3 ...
4 }
5}Source: UnzipModelsTask.java
UnzipModelsTask 把下载的 ArkModels zip 解压到 temp/models_unzipped/,解压完成后 models_data.json 才存在,initModelsDataset 随后才能读取它。下载/解压的 GUI 任务框架细节属于兄弟页面范围。
API 参考
public ModelsDataset(JSONObject jsonObject)
说明:公共构造器,从 fastjson2 JSONObject(即 models_data.json 的解析结果)构建数据集,内部委托给 Bean 构造器。
参数:
jsonObject(JSONObject):完整清单的 JSON 对象。
抛出:
NullPointerException:jsonObject转换结果 Bean 为null(Objects.requireNonNull(bean))。DatasetKeyException:storageDirectory缺失/为空、data缺失/为空,或某条目type不在storageDirectory中。
protected ModelsDataset(ModelsDatasetBean bean)
说明:受保护构造器,执行全部校验、类型转换(String 路径→File)与条目预处理(key/assetDir 回填、assetList 兼容回填、排序)。
public static class DatasetKeyException extends IllegalArgumentException
说明:数据集必需键缺失或非法时抛出的异常,消息形如 The key "xxx" not found or invalid.。继承 IllegalArgumentException,便于调用方用单一类型捕获所有清单校验错误。
Source: ModelsDataset.java
失败模式、边界与并发
| 失败场景 | 触发条件 | 处理方式 | 设计意图 |
|---|---|---|---|
清单缺 storageDirectory | 字段为 null 或空 Map | 构造即抛 DatasetKeyException("storageDirectory") | 无目录映射则一切 assetDir 不可推导,快速失败优于带病运行 |
清单缺 data | 字段为 null 或空 Map | 构造即抛 DatasetKeyException("data") | 空数据集对应用无意义 |
条目 type 未登记 | modelItem == null 或 storageDirectory 不含该 type | 抛 DatasetKeyException("type") | 保证清单内部一致性:每个条目都有合法落盘目录 |
旧版清单(无 assetList) | assetList == null 且 assetId、checksum 均非 null | 按 ModelItem.extensions 生成默认文件映射 | 在加载边界完成格式升级,消费端零分支 |
旧版清单缺 assetId/checksum | assetList == null 且二者任一为 null | 不回填,assetList 保持 null | 保守处理:由后续校验/使用方按各自规则报错 |
| JSON 结构非法 | fastjson2 反序列化失败 | 由调用方(ModelsModule.initModelsDataset)的 try/catch 转为用户提示 | core 不做 UI 假设,错误上抛 |
| 版本不兼容 | arkPetsCompatibility 高于当前应用版本 | 由 Version 比较逻辑判定 | 提示用户升级应用或数据集 |
并发说明:ModelsDataset 的所有 public final 字段(storageDirectory、data 等)在构造完成后即不再由 core 修改;但容器本身是普通 HashMap/ModelItemGroup,非线程安全。桌面端的并发模式是"主线程持有 + GuiTask 后台线程只读"(如 VerifyModelsTask 只读不写),写操作(重建数据集)发生在 initModelsDataset 中的受控时点。若未来引入并发写入,需要外部同步——当前源码中没有加锁证据。
边界条件:
storageDirectory中的路径是相对路径语义(Path.of(...)直接转换,未强制绝对化),实际落盘依赖解压根目录的工作目录约定。assetDir由"类型目录 + key"推导,因此数据集整体迁移目录后无需修改 JSON——只要相对结构不变即可继续工作。
扩展点
- 新增模型类型:只需在
storageDirectory中登记新 type 的子目录,并在data中放入对应条目,无需改 core 代码——目录与条目的映射关系完全数据驱动。 - 新增资产文件类型:扩展
ModelItem.extensions静态数组即可同时影响旧格式兼容回填生成的文件映射(定义于 ModelItem.java)。 - 自定义排序:
data.sort()的排序规则封装在ModelItemGroup中(见 ModelItemGroup.java),调整 UI 列表顺序无需触碰ModelsDataset。 - 替换 JSON 后端:由于桥接职责集中在
ModelsDatasetBean的@JSONFieldsetter 上,替换 fastjson2 只需重写这一个静态类的注解/映射方式。
相关链接
- ModelsDataset.java — 本页核心:数据集结构、校验与预处理
- ModelItem.java — 模型条目完整定义(含
extensions与排序比较) - ModelItemGroup.java — 模型集合容器与排序
- Const.java —
models_data.json/ArkModels等文件常量 - ModelsModule.java —
initModelsDataset与modelSearch消费入口 - VerifyModelsTask.java — 基于数据集的完整性校验任务
- UnzipModelsTask.java — 解压 ArkModels 产出数据集载体
- ArkHomeFX.java — 应用根上下文,持有
modelsDataset