Files
ichni_Official/docs/save-and-selection-system-status.md

162 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 存档、剧情进度与选曲缓存:当前状态说明
> **适用项目**`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 只能使用小写英文字母、数字和下划线,必须以字母开头,长度为 196。
- 推荐按“来源_章节_事件_状态”命名例如 `story_ch0_prologue_completed`
- 不得使用点号、空格、连字符、玩家可见标题、翻译文本或会频繁修改的 Yarn 标题。
- Yarn 使用 `<<grant_unlock unlock_key>>` 授予通用 Key既有 `<<unlock_song unlock_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。若未来共享序列化类型、编辑器预览或导入流程明确引用这些类型再重新评估跨项目同步。