# 存档、剧情进度与选曲缓存:当前状态说明 > **适用项目**:`ichni Official` > **最后更新**:2026-07-18 > **状态**:已完成 Core-002 的存档与选曲缓存收尾,以及 Core-003 的统一 Offline 内容解锁模块。 本文记录当前已落地的运行时存档机制、数据边界、字段语义和维护规则。它不是面向玩家的说明;以后修改存档、谱面、剧情或选曲逻辑时,应先阅读本文。 ## 1. 发布范围与基本原则 - 首发平台为 Windows、Android、iOS;当前目标允许为纯 Offline 版本。 - Chapter 0 是首发最低内容线;Chapter 1 以实际完成度为准。 - 首发本地化目标为英文、中文、日文、韩文、越南文。 - 游戏记录和剧情记录刻意分离:成绩变化不应破坏剧情进度,剧情变量变化也不应改写成绩。 - 当前仍处于正式发布前。因此两套存档的 Schema 均为 `v1`,出现不兼容旧测试存档时允许删除并重建;正式发布后必须改为显式迁移,不能沿用此策略。 ## 2. 存档总览 | 数据域 | 主要职责 | ES3 文件 | 当前 Schema | 启动加载顺序 | |---|---|---|---|---| | 游戏记录 | 歌曲完成状态、每个谱面的成绩、Chart Revision | `GameSaves/SongSaves.json` | Song Save v1 | 先加载 | | 内容解锁 | 已授予的章节/歌曲/教程等内容访问 Key | `GameSaves/UnlockKeys.json` | Unlock Save v1 | 游戏记录后、菜单显示前加载 | | 剧情记录 | 各章节剧情树、选项状态、全局 Yarn 变量 | `StorySaves/*.json`(由 `StorySaveModule` 管理) | Story Save v1 | 游戏记录后加载 | | 选曲缓存 | 当前运行期间各章节最后选择的歌曲与难度 | 仅内存 | 不适用 | 按需创建 | `GameSaveManager.Start()` 先初始化 `SongSaveModule` 并加载游戏记录、剧情解锁 Key,再初始化 `StorySaveModule`。菜单和章节 UI 因而可在首次展示前读取成绩和解锁状态。 ## 3. 游戏记录:歌曲与谱面成绩 主要实现位于 `Assets/Scripts/Saving/GameSaveManager.cs`。 ### 3.1 保存时机 一次正常游玩结束后,运行时的 `PlayingRecorder` 会把该局结果交给 `SongSaveModule.RecordPlayResult(...)`。该方法负责: 1. 确认 `songName`、`saveDifficultyId` 和本局判定数据有效; 2. 取得或创建对应 `SongStatusSave` 和 `BeatmapSave`; 3. 根据当前 `Chart Revision` 判断是否需要先重置该谱面的可比成绩; 4. 更新最高 Accuracy、最高 Max Combo,以及“曾达成”的 FC/AP; 5. 将修改后的完整游戏记录和 Song Save Schema 一起写回 ES3。 若本局没有任何有效判定(`totalCount <= 0`),不会生成成绩记录,以免异常退出污染数据。 ### 3.2 字段与含义 | 字段 | 所属层级 | 含义与更新规则 | |---|---|---| | `SongStatusSave.isCompleted` | 歌曲 | 任意一个有效游玩结束后置为 `true`;不会因为较差成绩回退。 | | `SongStatusSave.additionalInfo` | 歌曲 | 预留信息;当前保持非空字符串,未承载游戏规则。 | | `BeatmapSave.accuracy` | 谱面 | 当前 `Chart Revision` 下的最高 Accuracy。 | | `BeatmapSave.maxCombo` | 谱面 | 当前 `Chart Revision` 下的最高 Max Combo。 | | `BeatmapSave.isFullCombo` | 谱面 | Sticky 标记:本 Revision 内曾 FC 即保持 `true`。 | | `BeatmapSave.isAllPerfect` | 谱面 | Sticky 标记:本 Revision 内曾 AP 即保持 `true`。 | | `BeatmapSave.chartRevision` | 谱面 | 成绩所对应的谱面版本;与配置不一致时仅重置该谱面的可比成绩。 | ### 3.3 稳定 ID:`saveDifficultyId` 谱面成绩的字典 Key 是 `DifficultyData.saveDifficultyId`,而不是难度在 UI 列表中的位置。这样调整难度显示顺序、临时隐藏某难度或为某曲新增难度,都不会把旧成绩错误地归属到另一张谱面。 `difficultyListIndex` 只用于选曲 UI 的当前列表位置,不能用作存档 ID。每个需要保存成绩的难度应拥有非负、稳定且在同一歌曲内唯一的 `saveDifficultyId`。 ### 3.4 `Chart Revision` `Chart Revision` 是谱面内容版本号,不是整个存档的版本号。 - 仅改名称、封面、UI 排序等不影响判定内容的修改:不应递增。 - 改动音符、判定时机、总物量或足以使成绩不可比的规则:应递增。 - Revision 不一致时:只清空该难度的 Accuracy、Max Combo、FC、AP;歌曲完成状态、其它难度成绩、剧情存档和解锁 Key 均保留。 ## 4. Schema Version `Schema Version` 描述的是**存档数据结构**,不是游戏版本,也不是 `Chart Revision`。 当前 Song Save 和 Story Save 都为 `v1`。加载到不存在 Version 或 Version 不等于当前版本的数据时,预发布策略会删除该域的旧文件并按 v1 重建。这一策略方便当前开发阶段删档验证,但正式发布后必须替换为“读取旧 Version → 逐步迁移 → 写入新 Version”的流程。 改变以下内容时通常需要升级对应 Schema: - 已持久化字段改名、删除或改变类型; - 字典 Key 的含义改变; - 文件或 ES3 Key 的布局改变; - 新结构无法安全读取旧结构。 仅新增可安全默认初始化的字段时,是否升级 Schema 需按实际反序列化兼容性判断;不要为了“看起来更新了”而机械递增。 ## 5. 剧情存档 剧情存档由 `Assets/Scripts/NewStorySystem/Save/StorySaveModule.cs` 负责。它独立保存章节剧情树、选项/节点状态和全局 Yarn 变量,并有自己的 Story Save Schema v1。 剧情存档不得承担歌曲 Accuracy、Combo、FC/AP 等游玩数据;反过来,游戏记录也不得复制 Yarn 变量或剧情节点状态。剧情通过 Yarn 命令授予内容解锁 Key,由独立 `UnlockSaveModule` 决定章节和歌曲是否开放。 ## 6. Offline 内容解锁(Core-003) 主要实现位于 `Assets/Scripts/Saving/UnlockSaveModule.cs`。它是第三个独立存档域:既不是歌曲成绩,也不是剧情树。文件仍使用既有 `GameSaves/UnlockKeys.json`,会把旧的无 Schema 预发布 Key 集合无损补写为 Unlock Save v1。 ### 6.1 Key 命名与授予 - Key 只能使用小写英文字母、数字和下划线,必须以字母开头,长度为 1–96。 - 推荐按“来源_章节_事件_状态”命名,例如 `story_ch0_prologue_completed`。 - 不得使用点号、空格、连字符、玩家可见标题、翻译文本或会频繁修改的 Yarn 标题。 - Yarn 使用 `<>` 授予通用 Key;既有 `<>` 保持兼容,并在首次授予时继续排队歌曲提示。 ### 6.2 内容规则 章节与歌曲均持有 `UnlockRequirement`。根节点为空表示无条件开放;配置了规则时可使用: - `Key`:玩家必须持有指定 Key。 - `All Of`:所有子条件满足才开放。 - `Any Of`:任一子条件满足即可开放。 空的 `Key`、空的组合列表或空子节点属于配置错误,会保持锁定并仅在 Console 输出一次警告,避免错误配置意外放行。 真正进入歌曲时,当前章节和歌曲自身的规则必须同时满足。标准 Play、快速点击当前歌曲、以及未来的其它直达歌曲入口都必须调用 `UnlockSaveModule.CanEnterSong(...)`。 首发纯 Offline 不保留 Payment Unlock 运行时代码。未来若增加商店或其它 entitlement,只能向 `UnlockSaveModule` 授予 Key,不能在 UI 中新增独立支付判断。 ## 7. 选曲与难度缓存 选曲缓存由 `Assets/Scripts/Menu/MenuInformationRecorder.cs` 管理。 - 它只存在于内存,重启游戏后不会恢复;这是当前已确认的设计。 - 缓存单位是章节:记录该章节最后一次选择的歌曲及 `difficultyListIndex`。 - 玩家重新进入同一次运行中的同一章节时,UI 会优先恢复该选择。 - 若缓存歌曲已不在章节内、难度索引失效、歌曲没有可用难度,系统会安全回退,不会把空难度交给进入游戏流程。 ### 6.1 跨歌曲的难度回退规则 当玩家从一首歌拖动到另一首歌,而目标歌曲没有当前难度时,系统会选择目标歌曲中“更接近”的可用难度;距离相同则优先较低的列表序号。例如当前选择难度索引为 `2`,目标歌曲只有 `0`、`1`、`3` 可用时选择 `1`。 这是一项 UI 选择策略,不会改变 `saveDifficultyId` 或既有成绩归属。 ## 8. 已加入的运行时防御 - 选曲初始化会处理缺失/过期缓存、空歌曲列表和无可用难度的歌曲。 - 标准 Play 按钮会在歌曲、难度、选中 Tab 缺失或歌曲锁定时拒绝进入。 - 快速进入路径已防止把空歌曲或空难度传入 `InformationTransistor`,并与标准 Play 共用 `CanEnterSong(...)` 的最终解锁检查。 - 成绩存档加载后会与当前章节/歌曲/难度定义对账:为新增歌曲和难度创建空记录,并按 Revision 检查已有记录。 - 已删除的难度记录暂不自动清除;它们不参与当前 UI 和完成度统计,保留可避免内容恢复时丢失历史成绩。 ## 9. 当前已知边界与待办 1. **存档损坏处理**。尚未实现损坏文件恢复、备份或用户提示;已按当前决定延后。 2. **联网/云同步**。尚未实现;未来加入时应作为独立模块,不能直接把网络状态混入本地成绩、剧情文件或解锁 Key 文件。 3. **解锁提示本地化**。当前 `ShowUnlockMessage` 仍直接生成英文文本并显示 Key。后续本地化阶段应改为根据内容定义取得已本地化的标题与名称。 ## 10. 维护与验证清单 修改相关逻辑后,至少手动验证: 1. 新档首次启动:歌曲记录、剧情记录和解锁 Key 文件能创建。 2. 完成一次谱面后退出并重启:Accuracy、Max Combo、FC/AP、歌曲完成状态仍正确。 3. 用较差成绩重复游玩:最高记录和 Sticky FC/AP 不回退。 4. 提高某谱面的 `Chart Revision` 后重启:仅该难度的可比成绩清空。 5. 调整难度显示顺序后:成绩仍对应原 `saveDifficultyId`。 6. 在同一运行中离开并重新进入章节:歌曲/难度恢复;目标歌曲没有同难度时按“最近、同距取低”规则回退。 7. 对无可用难度:标准 Play 与快速进入都不能开始游戏;对锁定章节或歌曲:剧情入口、选曲入口、标准 Play 和快速进入均无法绕过。 最近一次静态 C# 验证:`dotnet build Assembly-CSharp.csproj --no-restore -v:minimal /m:1 /clp:ErrorsOnly`,结果为 **0 errors**。工程现有警告未在本轮逐项审计。 ## 11. 修改责任边界 本系统当前仅涉及 `ichni Official`。`ichniCreatorStudio` 未引用 `SongStatusSave`、`BeatmapSave`、`SongSaveModule` 或 `DifficultyData` 的这套运行时存档链路,因此本轮没有同步修改 Creator。若未来共享序列化类型、编辑器预览或导入流程明确引用这些类型,再重新评估跨项目同步。