11 KiB
存档、剧情进度与选曲缓存:当前状态说明
适用项目:
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(...)。该方法负责:
- 确认
songName、saveDifficultyId和本局判定数据有效; - 取得或创建对应
SongStatusSave和BeatmapSave; - 根据当前
Chart Revision判断是否需要先重置该谱面的可比成绩; - 更新最高 Accuracy、最高 Max Combo,以及“曾达成”的 FC/AP;
- 将修改后的完整游戏记录和 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 使用
<<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. 当前已知边界与待办
- 存档损坏处理。尚未实现损坏文件恢复、备份或用户提示;已按当前决定延后。
- 联网/云同步。尚未实现;未来加入时应作为独立模块,不能直接把网络状态混入本地成绩、剧情文件或解锁 Key 文件。
- 解锁提示本地化。当前
ShowUnlockMessage仍直接生成英文文本并显示 Key。后续本地化阶段应改为根据内容定义取得已本地化的标题与名称。
10. 维护与验证清单
修改相关逻辑后,至少手动验证:
- 新档首次启动:歌曲记录、剧情记录和解锁 Key 文件能创建。
- 完成一次谱面后退出并重启:Accuracy、Max Combo、FC/AP、歌曲完成状态仍正确。
- 用较差成绩重复游玩:最高记录和 Sticky FC/AP 不回退。
- 提高某谱面的
Chart Revision后重启:仅该难度的可比成绩清空。 - 调整难度显示顺序后:成绩仍对应原
saveDifficultyId。 - 在同一运行中离开并重新进入章节:歌曲/难度恢复;目标歌曲没有同难度时按“最近、同距取低”规则回退。
- 对无可用难度:标准 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。若未来共享序列化类型、编辑器预览或导入流程明确引用这些类型,再重新评估跨项目同步。