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

11 KiB
Raw Blame History

存档、剧情进度与选曲缓存:当前状态说明

适用项目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. 确认 songNamesaveDifficultyId 和本局判定数据有效;
  2. 取得或创建对应 SongStatusSaveBeatmapSave
  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 稳定 IDsaveDifficultyId

谱面成绩的字典 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,目标歌曲只有 013 可用时选择 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 OfficialichniCreatorStudio 未引用 SongStatusSaveBeatmapSaveSongSaveModuleDifficultyData 的这套运行时存档链路,因此本轮没有同步修改 Creator。若未来共享序列化类型、编辑器预览或导入流程明确引用这些类型再重新评估跨项目同步。