# 存档数据结构 > 定义游戏存档中需要持久化的数据。仅覆盖局外状态与已结束单局的结算记录;进行中的单局使用临时状态,不写入存档。 > 相关设计:`Docs/GameDesign/06_PrototypeScope.md`(存档相关验收项)、`Docs/GameDesign/05_LootAndProgression.md`(物品归属)。 > 代码位置:`Assets/GameMain/Scripts/Base/Definition/DataStruct/`(`SaveData`、`ItemStack`、`CharacterSave`、`LoadoutSave`、`RunRecord`),枚举复用 Luban 生成的 `RunResultType`、`DifficultyTier`。 ## 设计原则 - 存档只保存局外状态:主仓库内容、角色装备、当前战备配置、已结束单局的结算记录。 - 背包、保险箱、单局进度均为进入单局时创建的临时状态,不落盘;异常关闭直接丢弃。 - 数值上限(堆叠上限、背包格数等)来自 Luban 配表(`ItemConfig.StackLimit`、`GlobalConfig.BackpackSlotCount` 等),不写死在存档中。 - 存档使用 JSON 序列化,便于调试与手动修改;`SaveData.ToJson()` 序列化,`SaveData.FromJson(string)` 反序列化并补齐缺失字段(空列表/空数组)。 ## 序列化格式 枚举在 JSON 中以整数值存储:`RunResultType`(`Extracted=1`、`Defeated=2`、`TimedOut=3`、`Quit=4`)、`DifficultyTier`(`Tier1=1`、`Tier2=2`、`Tier3=3`)。 ```json { "version": 1, "updatedAt": 1756540000000, "mainWarehouse": [ { "itemId": 1001, "count": 3 }, { "itemId": 2005, "count": 1 } ], "characters": [ { "characterId": 1, "weaponItemId": 1001, "armorItemId": 0 }, { "characterId": 2, "weaponItemId": 0, "armorItemId": 0 } ], "loadout": { "partyCharacterIds": [1, 2], "carriedItems": [ { "itemId": 3002, "count": 5 } ], "difficultyId": 1 }, "runHistory": [ { "outcome": 1, "difficultyId": 1, "seed": 2026083001, "startedAt": 1756540000000, "endedAt": 1756541200000 } ] } ``` ## 结构定义 ### SaveData(存档根) | 字段 | 类型 | 说明 | | --- | --- | --- | | `version` | int | 存档结构版本,当前为 1;结构变更时递增并处理迁移 | | `updatedAt` | long | 最后写入时间,Unix 毫秒 | | `mainWarehouse` | `ItemStack[]` | 主仓库内容,格子数上限由 `GlobalConfig.WarehouseSlotCount` 配置,可空 | | `characters` | `CharacterSave[]` | 拥有的角色,数组顺序即角色入队顺序(速度并列时按此顺序行动) | | `loadout` | `LoadoutSave` | 当前战备配置 | | `runHistory` | `RunRecord[]` | 已结束单局的结算记录,可空 | ### ItemStack(物品堆叠) | 字段 | 类型 | 说明 | | --- | --- | --- | | `itemId` | int | 物品 ID,对应 `ItemConfig.Id` | | `count` | int | 数量,不超过配表 `ItemConfig.StackLimit` | 所有物品容器统一使用堆叠列表存储,不记录格子在背包中的位置。 ### CharacterSave(角色) | 字段 | 类型 | 说明 | | --- | --- | --- | | `characterId` | int | 角色 ID,对应 `CharacterConfig.Id` | | `weaponItemId` | int | 武器栏物品 ID,0 表示空栏 | | `armorItemId` | int | 防具栏物品 ID,0 表示空栏 | 装备穿戴在角色装备栏,不占用共享背包格子;成功撤离与任何非全员阵亡的战斗后均保持穿戴,死亡结算时随角色丢失,结算逻辑负责处理,存档本身不记录"阵亡"等局内状态。 ### LoadoutSave(战备配置) | 字段 | 类型 | 说明 | | --- | --- | --- | | `partyCharacterIds` | int[] | 出战角色 ID 及顺序,1~4 人,顺序决定同速时的行动次序 | | `carriedItems` | `ItemStack[]` | 携带进入地图的物品,开局时填充共享背包;首版无局内效果 | | `difficultyId` | `DifficultyTier` | 本局难度,对应 `DifficultyConfig` 主键 | 随机数(seed)不属于战备配置:每次进入单局时输入,只写入该局对应的 `RunRecord`,不持久化到 `loadout`。 ### RunRecord(结算记录) | 字段 | 类型 | 说明 | | --- | --- | --- | | `outcome` | `RunResultType` | 结算结果 | | `difficultyId` | `DifficultyTier` | 本局难度 | | `seed` | long | 本局使用的随机数 | | `startedAt` | long | 进入单局时间,Unix 毫秒 | | `endedAt` | long | 结算时间,Unix 毫秒 | 结算结果枚举复用 Luban 生成的 `RunResultType`: | 值 | 说明 | | --- | --- | | `Extracted` | 成功撤离 | | `Defeated` | 全员阵亡,单局失败 | | `TimedOut` | 25 分钟上限未撤离,撤离失败 | | `Quit` | 玩家主动退出单局,按撤离失败结算 | `Defeated`、`TimedOut`、`Quit` 均只保留保险箱内容,仅用于结算记录的区分。 ## 生命周期 - **新存档**:`characters` 由 `GlobalConfig.NewGameCharacterIds` 生成,初始装备取 `CharacterConfig.WeaponItemId` / `ArmorItemId`;`mainWarehouse`、`loadout.partyCharacterIds`、`loadout.carriedItems`、`runHistory` 为空。 - **进入单局**:根据存档中的局外状态创建仅供本局使用的临时状态(背包、保险箱、单局进度),单局进行中只修改临时状态,不写入存档。 - **正常结算**(撤离成功 / 死亡 / 超时 / 主动退出):按结算规则把物品写入主仓库或丢弃,追加一条 `RunRecord`,一次性写盘。 - **异常关闭**:临时状态丢弃,存档保持进入该局前的状态,不产生结算记录。 ## 读写组件 > 代码位置:`Assets/GameMain/Scripts/Runtime/CustomComponent/Save/SaveComponent.cs`,通过 `GameEntry.Save` 访问,挂在 Launcher 场景 `Customs/Save` 物体上。 | API | 说明 | | --- | --- | | `Data` | 当前内存中的存档数据,未加载或未创建时为 null | | `HasSave` | 磁盘上是否已存在存档文件 | | `IsReady` | 存档数据是否可用 | | `Load()` | 从磁盘读取存档;文件不存在返回 true 且 `HasSave` 为 false,解析失败返回 false | | `CreateNewGame()` | 依据配表创建新存档数据(不写盘) | | `Save()` | 将当前存档写入磁盘,自动更新 `updatedAt`;先写临时文件再替换,避免写入中断损坏存档 | - 存档文件路径为 `Application.persistentDataPath` 下的 `save.json`(`SaveComponent` 的 `_fileName` 可配置)。 - 加载时若 `version` 与 `SaveData.CurrentVersion` 不一致,记录警告并继续加载,迁移逻辑后续需要时补充。 ## 验收对应 对应 `Docs/GameDesign/06_PrototypeScope.md` 存档相关验收项: - [ ] 退出并重新打开游戏后,局外主仓库、角色装备和战备配置能够从本地存档恢复。 - [ ] 已结束单局的结算数据能够写入本地存档并在重新打开游戏后读取。 - [ ] 重新打开游戏时不会恢复尚未结束的单局。 - [ ] 单局中主动退出时能够按撤离失败结算,保存保险箱内容和本局失败记录。 - [ ] 异常关闭的未结算单局不会修改局外存档,也不会生成结算记录。 ## 还没决定的问题 暂无。存档的加密、校验与多存档位支持不在原型范围内,后续需要时再补充。