12 KiB
SaveSystem 存档快照层
当前代码包含 P1 快照层、P2 槽位/落盘基础版、P3 可存点判定基础版、P4 读档编排基础版。设计文档见
Docs/存档系统设计方案.md(§4.1 / D6 描述读档编排模型)。
职责边界
| 模块 | 位置 | 做什么 |
|---|---|---|
| Yarn 变量 | Game Loop/YarnVariableStorage.cs |
运行时 $ / $global_ 读写 |
| 本目录 | SaveSystem/ |
快照结构、捕获/还原、槽位落盘、流程编排 |
| 槽位 / 截图 | SlotManager / SlotThumbnailCapture |
P2 基础版:slot_0 自动档、slot_1..8 手动档、sidecar、缩略图、原子写 |
不要再往 YarnVariableStorage 上堆存档逻辑。
对外入口
// 自动存档(节点进入事件触发)
yield return SaveRestoreOrchestrator.AutoSaveRoutine(nodeName);
// Yarn 显式存档(<<save>>,默认 omit anchor)
yield return SaveRestoreOrchestrator.ExplicitSaveRoutine();
// 手动存档:复制最近落盘档(含 OnNodeStart 与 <<save>>)
SaveRestoreOrchestrator.CreateManualSlot(slotIndex);
// 槽位读档:会话命令统一从 GameManager 进入
GameManager.Instance.TryRestoreSlot(slotIndex, result => { /* 处理结果 */ });
// 仅捕获内存快照;恢复仍须通过 GameManager,以保证会话互斥和失败清理
var snap = SnapshotService.Capture();
// Yarn 变量(与存档无关)
YarnVariableStorage.Instance.SetValue("$foo", 1f);
Yarn 脚本:
// 推荐:有内容的交互入口节点
tags: interaction
// 或为其他一级类型覆盖保存时机
tags: content save_on_exit
// 仅兼容旧内容
<<save>>
interaction/save_on_exit进入时不存档;对应节点完成后若直接结束本轮 Dialogue,则自动保存 state-only 档。- 若执行路径继续
jump/detour到其他 Yarn 节点,退出档会取消并记录告警。 - 放在目标节点末尾:所有状态命令(
switch_fix_system_to、hide_dialog、play_timeline等)执行完毕之后,<<jump>>/<<detour>>之前。 - 默认 omit anchor(
anchor.nodeName为空);读档时不重进 Yarn,仅还原 scene + sections + 变量。 - 绕过 tag /
no_save的自动判定(CanExplicitSave);仍受读档中、暂停、SuppressAutoSave 门控。 - 与
OnNodeStart分工:常规hub/linear/content仍靠节点进入自动存;interaction/save_on_exit用于即将进入无对话 / Fix 交互等边界。
P3 自动档边界(含 detour 去重)
DialogController.OnNodeStart → SavePointEvaluator.OnNodeStartForAutoSave → 通过后 AutoSaveRoutine。
判定顺序:
- 全局门控:
IsRestoring/IsAutoSaveSuppressed/GameManager.pause - Fresh vs detour 续跑:同一
YarnProject下,若节点名已在InProgressNodes(尚未onNodeComplete)→DetourResume,不自动存(避免 detour 返回父节点时重复写盘) - 将节点加入
InProgressNodes,再解析NodeSavePolicy:hub/linear/content进入时保存;interaction/save_on_exit在 Dialogue 正常结束时保存 state-only 档;detour/function等默认禁止
OnNodeComplete 将节点移出 InProgressNodes。StartDialog / LoadDialog 切换 YarnProject.name 时、StopDialog 时重置集合。读档重进 anchor 走 OnRestoreEnterNode(只标记 InProgress,不写盘)。
CanAutoSave() 仍仅做 tag + 全局门控,不含 detour 判定(供 TryAutoSave 等无节点进入上下文的路径)。显式 <<save>> / 手动档复制不经过 visit 判定。
逻辑集中在 SavePointEvaluator.cs,不新增独立 Tracker 文件。
正式槽位路径:persistentDataPath/AllOurBrokenParts/demo_saves/slot_x/{snapshot.json, meta.json, thumbnail.png}
目录结构
SaveSystem/
├── README.md ← 本文件
├── ISnapshotProvider.cs 契约:Capture / Restore(P1 临时形态,见下)
├── SaveSnapshot.cs 快照根对象 + 各 section DTO + SaveSnapshotSummary
├── SnapshotRegistry.cs provider 稳定 id、注册表、启动注册
├── SnapshotCapture.cs 组装 SaveSnapshot(scene / anchor 由框架直管)
├── SnapshotRestore.cs 写回运行时;含 section JObject → 强类型转换
├── SnapshotService.cs Capture / Restore 对外 API
├── SnapshotPersistence.cs 文件读写、JSON 序列化、旧档检测
├── SaveRestoreOrchestrator.cs UI + 落盘 + 读盘编排
├── SaveYarnCommand.cs Yarn <<save>> 显式存档
├── ScreenSnapshotHelper.cs screen section 实现细节
├── SlotTypes.cs P2:槽位常量、sidecar、UI 视图模型
├── SlotFileSystem.cs P2:槽位路径 + 原子写
├── SlotManager.cs P2:槽位业务核心
├── SlotThumbnailCapture.cs P2:截图 helper
├── CloudSave/ P2:云存档适配器接口与协调器
│ ├── ICloudSaveBackend.cs
│ └── CloudSaveManager.cs
└── Providers/ 表现层子系统薄适配层
├── EnvironmentSnapshotProvider.cs order 20
├── ActorSnapshotProvider.cs order 40
├── AudioSnapshotProvider.cs order 50
├── TimelineSnapshotProvider.cs order 60
├── FixSnapshotProvider.cs order 65
├── FixPanelSnapshotProvider.cs order 66
├── ShowcaseSnapshotProvider.cs order 69
├── Day2SleepPresentationSnapshotProvider.cs order 70
├── PlayToolSnapshotProvider.cs order 71
└── ScreenSnapshotProvider.cs order 80
存盘 / 读盘流程
存盘:
SaveRestoreOrchestrator
→ SnapshotService.Capture()
→ SnapshotCapture(场景 + 锚点 + Yarn 变量 + 各 Provider)
→ SnapshotPersistence.Save()
读盘:
GameManager
→ SaveRestoreOrchestrator.PrepareRestore(读盘 + 预检,不修改运行时)
→ SaveRestoreOrchestrator.ExecutePreparedRestore
→ SnapshotRestore(变量 + Scene + Providers + Anchor)
→ GameManager 提交章节和会话状态
读档还原编排
读档已经采用 Prepare / Execute 边界与 Phase + Barrier 模型。所有文件统一反序列化,并由 schema 与必填字段预检判断是否兼容;不存在旧格式专用识别、恢复或转写分支。
Phase + Barrier
读档分阶段推进,仅在必须等待的边界上 yield:
Phase 0 Yarn 变量(sync)
↓
Phase 1 场景加载(Barrier:LoadSceneAsync)← 框架直管,不走 Provider
↓
Phase 2 env / actor / audio / timeline / fix / showcase / day2SleepPresentation / playTool / screen(Provider 按 RestoreOrder)
└─ timeline:`Stopped` = untouched(从未 Evaluate),读档仅还原 `isActive`,不 Reset/Evaluate
↓
Phase 2′ 可选 Barrier(如 Timeline Addressable 须显式等待)
↓
Phase 2.5 SceneReadiness(Barrier)
↓
Phase 3 加载对话工程 + RestoreAnchor(Barrier:StartDialogue)← 框架直管
↓
Postflight 校验
- 核心层(
scene、anchor、yarnVariables)由SnapshotRestore框架直管,不通过 Provider 注册。 - 表现层(
sections内各子系统)通过ISnapshotProvider扩展;RestoreOrder表示同 Phase 内的建议顺序或软依赖。 - **逐项还原(D1)**指各子系统各自写回状态,不是 Provider 之间逐步
yield return。
Provider 使用 ISyncSnapshotProvider 或 IAsyncSnapshotProvider 显式声明同步/异步恢复;异步 Provider 必须把等待过程返回给编排层,禁止内部 fire-and-forget。
快照 JSON 结构(schemaVersion = 2)
| 字段 | 含义 |
|---|---|
scene |
当前场景名(Addressable key),读档时最先加载 |
anchor |
恢复锚点:sceneSoName + yarnProjectId + nodeName;最后阶段加载对话并重进节点 |
yarnVariables |
floats / strings / bools |
sections |
key 为 SnapshotProviderIds,值为各 DTO;表现层与维修子模块(env / actor / fix / bodyModule / blockPuzzle …)统一在此 |
宏观信息(场景、章节、节点)直接位于
SaveSnapshot根对象,不再冗余到sections;槽位 / 「继续游戏」UI 需要展示时,从scene/anchor直接读取。
新增一个可存子系统
所有可存子系统(含维修小游戏)均通过 Provider 注册到 sections,用 RestoreOrder 控制同 Phase 内的还原顺序:
- 在
SaveSnapshot.cs增加 DTO 类。 - 在
SnapshotRegistry.cs的SnapshotProviderIds区域增加稳定 id,并加入RequiredForCapture(若 P1 必须存)。 - 在对应 Manager 实现
CaptureSnapshot/RestoreSnapshot(默认 syncvoid;仅确有 async 加载/状态切换时用IEnumerator并向编排层上报)。 - 新建
Providers/XxxSnapshotProvider.cs实现ISnapshotProvider(P4 后:sync 默认 + 显式 async 接口)。 - 在
SnapshotRegistry.EnsureInitialized中Register。 - 在
SnapshotRestore.CoerceSectionDto增加 JObject 转换分支。
禁止在 Provider 内私自 StartCoroutine 而不纳入编排 Barrier。
例外:
scene(场景加载)与anchor(章节+节点重进)属于核心叙事坐标,由SnapshotCapture/SnapshotRestore框架直管,不走 Provider 注册。新增「核心层」字段需修改SaveSnapshot根对象与对应编排方法。
Fix 场景快照
Fix 场景与维修子模块的快照均存储在 sections,由各自 Provider 按 RestoreOrder 还原。
双入口设计:Enter vs EnterImmediate
Fix 场景各 State 的 Enter() 通常包含相机过渡、Timeline 播放、Fade 等动画。读档时需要跳过这些动画直接到达终态,因此为 IFixState 增加了 EnterImmediate() 入口:
- 正常游戏流程 →
Enter()(完整动画) - 读档恢复 →
SwitchStateImmediate()→EnterImmediate()(跳过动画)
读档时 SwitchStateImmediate 不调用前一个状态的 Exit(),因为读档本质是覆盖当前状态,不需要清理。
已落地的维修 section
| section id | DTO | RestoreOrder | 说明 |
|---|---|---|---|
punchTape |
PunchTapeSnapshotDto |
63 | 打孔带收藏 |
fix |
FixSnapshotDto |
65 | FixSceneDirector 宏观模式(state + args) |
fixPanel |
FixPanelSnapshotDto |
66 | FixPanel 壳层 + 线缆/插头 + TaskPanel(请求展开状态与任务列表) |
bodyModule |
BodyModuleSnapshotDto |
67 | 插线模块物理态 |
eye |
EyeSnapshotDto |
68 | Eye 叙事阶段 |
还原顺序:punchTape → fix(cue)→ fixPanel → bodyModule → eye → showcase → day2SleepPresentation → playTool → screen。fixPanel 须在 fix 之后,以覆盖 Cue EnterImmediate 中的 ResetPlug,并让 Memory Cue 先建立 TaskPanel 临时隐藏状态;D2 动态表现须在通用 Showcase 图片之后恢复,才能叠加虚焦、缩放或场景专属动画。
维修场景门控(FixSceneSnapshotHelper):上述 section 仅在 FixSystemCenter.Instance != null 时 Capture/Restore。
尚未实现
- 部分 FixCue 的
EnterImmediate()具体逻辑(HuoShan / BlockPuzzle / Cutting 等仍用默认空实现) - BlockPuzzle / Memory / Cutting 等子模块的 section DTO + Provider
fixPanel扩展:灯光状态(P1)、RepairSystemManager 缩放/offset(P2);ScreenPanel 日志文本
相关阶段
| 阶段 | 内容 |
|---|---|
| P2 | 基础版已落地:槽位、原子写、meta sidecar、缩略图、latest_slot、P1 测试档迁移;正式 UI 接入仍属 P6 |
| P3 | 基础版已落地:onNodeStart 判定、tag 白/黑名单、no_save、读档/暂停门控、detour 返回 InProgress 去重;<<save>> 显式存档(CanExplicitSave、omit anchor)已落地 |
| P4 | 基础版已落地:Provider sync/async 契约、Phase + Barrier 编排、读档自动存档抑制、Timeline Addressable 可等待恢复、验证窗口读档入口 |
| P5 | 维修子模块 section 扩展(BlockPuzzle / Memory / Cutting 等待新增 Provider) |
| P6 | 正式存 / 读档 UI、继续游戏、新游戏覆盖自动档、游戏中读档确认等玩家流程 |