Files
aibis-dream/Assets/Scripts/SaveSystem/README.md
T
2026-07-27 16:00:30 +08:00

230 lines
13 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.
# 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 显式存档(<<save>>,保留来源节点但恢复时不启动 Yarn)
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 脚本:
```yarn
// 推荐:有内容的交互入口节点
tags: interaction
// 或为其他一级类型覆盖保存时机
tags: content save_on_exit
// 仅兼容旧内容
<<save>>
```
- `interaction` / `save_on_exit` 进入时不存档;对应节点完成后若直接结束本轮 Dialogue,则自动保存 state-only 档。
- 若执行路径继续 `jump` / `detour` 到其他 Yarn 节点,退出档会取消并记录告警。
- run / flow / current-node 稳定性只约束 `DialogueExit`:任何后续 Yarn 节点(包括 `detour``no_save`)都会取消节点末尾存档。
- 普通 `NodeEnter` 和兼容命令 `<<save>>` 不受后续节点流转影响;它们始终使用请求触发时冻结的 node / YarnProject / TalkScene 生成 anchor,并继续进入串行写盘队列。
- 放在目标节点**末尾**:所有状态命令(`switch_fix_system_to``hide_dialog``play_timeline` 等)执行完毕之后,`<<jump>>` / `<<detour>>` 之前。
- `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` 等无节点进入上下文的路径)。显式 `<<save>>` / 手动档复制不经过 visit 判定。
逻辑集中在 `SavePointEvaluator.cs`**不新增**独立 Tracker 文件。
正式槽位路径:`persistentDataPath/AllOurBrokenParts/demo_saves/slot_x/{snapshot.json, meta.json, thumbnail.png}`
## 目录结构
```
SaveSystem/
├── README.md ← 本文件
├── ISnapshotProvider.cs 契约:Capture / RestoreP1 临时形态,见下)
├── SaveSnapshot.cs 快照根对象 + 各 section DTO + SaveSnapshotSummary
├── SnapshotRegistry.cs provider 稳定 id、注册表、启动注册
├── SnapshotCapture.cs 组装 SaveSnapshotscene / 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 场景加载(BarrierLoadSceneAsync)← 框架直管,不走 Provider
Phase 2 env / actor / audio / timeline / fix / showcase / day2SleepPresentation / playTool / screenProvider 按 RestoreOrder
└─ timeline`Stopped` = untouched(从未 Evaluate),读档仅还原 `isActive`,不 Reset/Evaluate
Phase 2 可选 Barrier(如 Timeline Addressable 须显式等待)
Phase 2.5 SceneReadinessBarrier
Phase 3 加载对话工程 + RestoreAnchorBarrierStartDialogue)← 框架直管
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 → fixcue)→ 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 缩放/offsetP2);ScreenPanel 日志文本
## 相关阶段
| 阶段 | 内容 |
| --- | --- |
| P2 | 基础版已落地:槽位、原子写、meta sidecar、缩略图、latest_slot、P1 测试档迁移;正式 UI 接入仍属 P6 |
| P3 | 基础版已落地:`onNodeStart` 判定、tag 白/黑名单、`no_save`、读档/暂停门控、**detour 返回 InProgress 去重**`<<save>>` 显式 StateOnly 存档已落地 |
| P4 | 基础版已落地:Provider sync/async 契约、Phase + Barrier 编排、读档自动存档抑制、Timeline Addressable 可等待恢复、验证窗口读档入口 |
| P5 | 维修子模块 section 扩展(BlockPuzzle / Memory / Cutting 等待新增 Provider |
| P6 | 正式存 / 读档 UI、继续游戏、新游戏覆盖自动档、游戏中读档确认等玩家流程 |