# SaveSystem 存档快照层 > 当前代码包含 P1 快照层、P2 槽位/落盘基础版、P3 可存点判定基础版、P4 读档编排基础版。设计文档见 [`Docs/存档系统设计方案.md`](../../../Docs/存档系统设计方案.md)(**§4.1 / D6** 描述读档编排模型)。 ## 职责边界 | 模块 | 位置 | 做什么 | | --- | --- | --- | | Yarn 变量 | `Game Loop/YarnVariableStorage.cs` | 运行时 `$` / `$global_` 读写 | | **本目录** | `SaveSystem/` | 快照结构、捕获/还原、槽位落盘、流程编排 | | 槽位 / 截图 | `SlotManager` / `SlotThumbnailCapture` | P2 基础版:slot_0 自动档、slot_1..8 手动档、sidecar、缩略图、原子写 | **不要**再往 `YarnVariableStorage` 上堆存档逻辑。 ## 对外入口 ```csharp // 自动存档(节点进入事件触发) yield return SaveRestoreOrchestrator.AutoSaveRoutine(nodeName); // Yarn 显式存档(<>,保留来源节点但恢复时不启动 Yarn) yield return SaveRestoreOrchestrator.ExplicitSaveRoutine(); // 手动存档:复制最近落盘档(含 OnNodeStart 与 <>) SaveRestoreOrchestrator.CreateManualSlot(slotIndex); // 槽位读档:会话命令统一从 GameManager 进入 GameManager.Instance.TryRestoreSlot(slotIndex, result => { /* 处理结果 */ }); // 仅捕获内存快照;恢复仍须通过 GameManager,以保证会话互斥和失败清理 var snap = SnapshotService.Capture(); // Yarn 变量(与存档无关) YarnVariableStorage.Instance.SetValue("$foo", 1f); ``` Yarn 脚本: ```yarn // 推荐:有内容的交互入口节点 tags: interaction // 或为其他一级类型覆盖保存时机 tags: content save_on_exit // 仅兼容旧内容 <> ``` - `interaction` / `save_on_exit` 进入时不存档;对应节点完成后若直接结束本轮 Dialogue,则自动保存 state-only 档。 - 若执行路径继续 `jump` / `detour` 到其他 Yarn 节点,退出档会取消并记录告警。 - run / flow / current-node 稳定性只约束 `DialogueExit`:任何后续 Yarn 节点(包括 `detour`、`no_save`)都会取消节点末尾存档。 - 普通 `NodeEnter` 和兼容命令 `<>` 不受后续节点流转影响;它们始终使用请求触发时冻结的 node / YarnProject / TalkScene 生成 anchor,并继续进入串行写盘队列。 - 放在目标节点**末尾**:所有状态命令(`switch_fix_system_to`、`hide_dialog`、`play_timeline` 等)执行完毕之后,`<>` / `<>` 之前。 - `anchor.nodeName` 始终保留来源节点;`anchor.startDialogueOnRestore=false`,读档时只加载 YarnProject,不启动节点。 - 退出档在 `DialogEnd` 同步初始化后等待一帧捕获;这一帧由 scoped interaction lock 保持输入锁定,捕获进入写盘队列后再交出控制权。 - 绕过 tag / `no_save` 的自动判定(`CanExplicitSave`);仍受读档中、暂停、SuppressAutoSave 门控。 - 与 `OnNodeStart` 分工:常规 `hub` / `linear` / `content` 仍靠节点进入自动存;`interaction` / `save_on_exit` 用于即将进入无对话 / Fix 交互等边界。 ### P3 自动档边界(含 detour 去重) `DialogController.OnNodeStart` → `SavePointEvaluator.OnNodeStartForAutoSave` → 通过后 `AutoSaveRoutine`。 判定顺序: 1. 全局门控:`IsRestoring` / `IsAutoSaveSuppressed` / `GameManager.pause` 2. **Fresh vs detour 续跑**:同一 `YarnProject` 下,若节点名已在 `InProgressNodes`(尚未 `onNodeComplete`)→ `DetourResume`,**不自动存**(避免 detour 返回父节点时重复写盘) 3. 将节点加入 `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` 等无节点进入上下文的路径)。显式 `<>` / 手动档复制不经过 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 <> 显式存档 ├── 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`。 - Postflight 只核对 Scene、TalkSceneSO、YarnProject 与必要 Provider readiness;不要求 RestartNode 仍停在 anchor,也不校验 StateOnly 结束后的 DialogueRunner 状态。 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 内的还原顺序: 1. 在 `SaveSnapshot.cs` 增加 DTO 类。 2. 在 `SnapshotRegistry.cs` 的 `SnapshotProviderIds` 区域增加稳定 id,并加入 `RequiredForCapture`(若 P1 必须存)。 3. 在对应 Manager 实现 `CaptureSnapshot` / `RestoreSnapshot`(**默认 sync `void`**;仅确有 async 加载/状态切换时用 `IEnumerator` 并向编排层上报)。 4. 新建 `Providers/XxxSnapshotProvider.cs` 实现 `ISnapshotProvider`(P4 后:sync 默认 + 显式 async 接口)。 5. 在 `SnapshotRegistry.EnsureInitialized` 中 `Register`。 6. 在 `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 去重**;`<>` 显式 StateOnly 存档已落地 | | P4 | 基础版已落地:Provider sync/async 契约、Phase + Barrier 编排、读档自动存档抑制、Timeline Addressable 可等待恢复、验证窗口读档入口 | | P5 | 维修子模块 section 扩展(BlockPuzzle / Memory / Cutting 等待新增 Provider) | | P6 | 正式存 / 读档 UI、继续游戏、新游戏覆盖自动档、游戏中读档确认等玩家流程 |