Repository Wiki
isHarryh/Ark-Pets

模型数据集与模型项结构

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)是整个模型库的"清单文件",它描述了:

  1. 存储目录映射(storageDirectory):模型类型(如 avatar/monster 等游戏内分类)到磁盘目录的映射;
  2. 排序标签(sortTags):供 UI 展示用的标签文案与顺序;
  3. 游戏数据版本信息:gameDataVersionDescription、gameDataServerRegion;
  4. 模型条目集合(data):key → 模型条目 JSON 的映射;
  5. 兼容性版本号(arkPetsCompatibility):声明该数据集要求的最低 Ark-Pets 版本。

ModelsDataset 类将这份 JSON 清单转换为强类型的、经过预处理并排序的运行时对象,挂载在 ArkHomeFX.modelsDataset 字段上(见 ArkHomeFX.java),供桌面端模型列表、搜索与校验流程共享。

设计意图:把"清单解析 + 目录拼接 + 版本兼容回填"集中到 core 模块的单个构造函数中完成,使得 desktop 侧的所有消费者拿到的 ModelItem 都是"开箱即用"的——每个条目都已经带上了绝对的 assetDir 目录和规范化的 assetList 文件映射,无需各自再做路径推导或兼容处理。

架构

Loading diagram...

分层说明:

  • 模型仓库层:Ark-Pets 自身不生成模型,只消费 ArkModels 仓库产物。UnzipModelsTask(继承 UnzipTask)负责把下载到的 zip 解压到临时目录,见 UnzipModelsTask.java。
  • core 数据层:ModelsDataset 是纯 Java 数据结构(无 JavaFX 依赖),可以同时被 core 与 desktop 使用。它通过内部静态 Bean 类完成 JSON 桥接,并在构造函数中完成全部校验与预处理。
  • desktop 消费层:ArkHomeFX 作为应用根上下文持有数据集实例;ModelsModule(FXML 控制器)负责初始化数据集并在列表中展示/搜索;VerifyModelsTask 以数据集为输入校验本地模型文件完整性,见 VerifyModelsTask.java。

类型结构

Loading diagram...

注意:上图中 ModelItem 的字段列表来自 ModelsDataset 构造函数中实际读写的成员(key、type、assetDir、assetList、assetId、checksum 与静态 extensions)。ModelItem 与 ModelItemGroup 的完整定义位于同包的 ModelItem.java 与 ModelItemGroup.java,本页只保证列出在 ModelsDataset.java 中被验证过的部分。

核心实现:反序列化与预处理流程

ModelsDataset 的构造流程是本页最核心的控制流。它分为两段:外部 JSON 入口与内部 Bean 转换。

两段式构造设计

java
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 作为受保护的实现细节不对外暴露。

逐阶段控制流

Loading diagram...

storageDirectory:类型 → 磁盘目录

java
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:模型条目集合与预处理

java
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

这段循环承担了四项职责,每一项都对应一个真实的数据完整性需求:

  1. 反序列化(Pre deserialization):每个条目在 JSON 中仍是 JSONObject,先转为 ModelItem,使后续可以用类型安全的方式访问字段。
  2. 回填 key:JSON 对象的键名(如模型唯一标识)在反序列化成 ModelItem 后会丢失(它不是条目内部的字段),因此显式写回 modelItem.key = key。这保证了运行时每个模型都能自报家门,供 UI 列表、持久化选中项使用。
  3. 回填 assetDir:模型资产目录 = storageDirectory[type] / key。这是一次刻意的冗余派生——JSON 清单中不存绝对目录,而是"类型目录 + 模型键"的规则推导,避免仓库在移动/换机器后路径失效;同时校验 type 必须存在于 storageDirectory,防止清单内部自相矛盾。
  4. 旧版兼容回填 assetList:旧格式数据集没有 assetList(多文件映射),只有单一的 assetId 与 checksum。此时按 ModelItem.extensions(模型文件扩展名数组,如 .skel/.atlas/.png 等约定后缀)拼出"文件类型 → 文件名"的默认映射(文件名 = assetId + 后缀)。设计意图:让新代码只面向统一的 assetList 抽象,旧数据在加载边界处被一次性升级,而不是在每个消费点写 if (assetList == null) 分支。

循环结束后调用 data.sort()——ModelItemGroup 聚合并排序所有条目,保证模型列表在 UI 中呈现稳定、确定的顺序(排序规则定义于 ModelItemGroup/ModelItem 内部)。

兼容性版本号

java
arkPetsCompatibility = new Version(bean.arkPetsCompatibility);

Source: ModelsDataset.java

JSON 中为 int[] 数组,运行时转为 core 工具类 cn.harryh.arkpets.utils.Version,用于与当前应用版本比较,判断数据集是否被当前 Ark-Pets 版本支持。

JSON 桥接 Bean

java
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)必填说明
storageDirectoryHashMap<String, String>HashMap<String, File>✅(非空)模型类型 → 资产子目录(相对数据集根目录)映射
sortTagsHashMap<String, String>HashMap<String, String>❌标签排序/文案映射,供 UI 分组展示
gameDataVersionDescriptionStringString❌提取该批模型的游戏数据版本描述
gameDataServerRegionStringString❌游戏数据来源服务器区域
dataHashMap<String, JSONObject>ModelItemGroup(已排序)✅(非空)模型键 → 模型条目;运行时逐条转为 ModelItem 并回填 key/assetDir/assetList
arkPetsCompatibilityint[]Version❌最低兼容的 Ark-Pets 版本(如 [3,0,0])

ModelItem(经 ModelsDataset.java 验证的字段子集):

字段类型来源说明
keyString构造期回填data 映射中的键名,即模型唯一标识
typeStringJSON模型类型,必须存在于 storageDirectory,否则抛 DatasetKeyException("type")
assetDirFile构造期回填storageDirectory[type] / key 派生的资产目录
assetListJSONObjectJSON 或兼容回填文件类型 → 文件名映射;旧版数据集由 assetId + extensions 生成
assetIdStringJSON旧格式使用的资产基础文件名(不含扩展名)
checksumStringJSON旧格式使用的校验和,供 VerifyModelsTask 校验完整性
extensionsString[](静态)代码常量ModelItem 支持的模型文件扩展名数组

相关文件常量

数据集在磁盘上的落点由 Const.FilePath(core 常量类)集中定义:

常量值用途
tempDirPathtemp/下载/解压工作目录根
fileModelsZipNameArkModels模型仓库压缩包文件名(不含扩展名)
fileModelsDataPathmodels_data.json数据集清单文件在解压后目录内的相对路径
tempModelsUnzipDirPathtemp/models_unzipped/压缩包解压目标目录
mirrorChyanModelRepoRIDArkModelsRepoMirrorChyan 下载源中模型仓库的资源 ID
mirrorChyanRIDArkPetsAppMirrorChyan 中 Ark-Pets 应用本体资源 ID

Source: Const.java

这些常量把"文件名/路径"从业务代码中抽离,解压流程(UnzipModelsTask)与清单解析(ModelsDataset)因此解耦于具体路径约定。

使用示例

desktop 侧:持有与初始化数据集

ArkHomeFX(JavaFX 应用根控制器)把数据集作为应用级共享状态持有,ModelsModule 在初始化时填充它:

java
public ArkConfig config; public ModelsDataset modelsDataset;

Source: ArkHomeFX.java

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 侧:基于数据集的搜索过滤

java
1public void modelSearch(String keyWords) { 2 modelListView.getItems().clear(); 3 ... 4}

Source: ModelsModule.java

modelSearch 清空列表后按关键词重新填充条目——它读取的正是 ArkHomeFX.modelsDataset.data 中已排序的 ModelItem 集合。这解释了为什么 ModelsDataset 构造期就执行 data.sort():排序一次,所有后续列表渲染/搜索结果都天然有序。

desktop 侧:以数据集为输入的完整性校验任务

java
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 侧:解压任务产出数据集载体

java
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/checksumassetList == 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 的 @JSONField setter 上,替换 fastjson2 只需重写这一个静态类的注解/映射方式。

相关链接

Sources

(1 files)