200 lines
10 KiB
Markdown
200 lines
10 KiB
Markdown
# SaveSystem 存档快照层
|
||
|
||
> 当前代码包含 P1 快照层、P2 槽位/落盘基础版、P3 可存点判定基础版、P4 读档编排基础版。设计文档见 [`Docs/存档系统设计方案.md`](../../../Docs/存档系统设计方案.md)(**§4.1 / D6** 描述读档编排模型)。
|
||
|
||
## 职责边界
|
||
|
||
| 模块 | 位置 | 做什么 |
|
||
| --- | --- | --- |
|
||
| Yarn 变量 | `Game Loop/YarnVariableStorage.cs` | 运行时 `$` / `$global_` 读写 |
|
||
| **本目录** | `SaveSystem/` | 快照结构、捕获/还原、槽位落盘、流程编排 |
|
||
| 深度维修(旧) | `DeepRepairDataRegistry.cs` | FixSystem 等 IData,P5 前临时保留 |
|
||
| 槽位 / 截图 | `SlotManager` / `SlotThumbnailCapture` | P2 基础版:slot_0 自动档、slot_1..8 手动档、sidecar、缩略图、原子写 |
|
||
|
||
**不要**再往 `YarnVariableStorage` 上堆存档逻辑。
|
||
|
||
## 对外入口
|
||
|
||
```csharp
|
||
// 自动存档(节点进入事件触发)
|
||
yield return SaveRestoreOrchestrator.AutoSaveRoutine(nodeName);
|
||
|
||
// 手动存档:复制最近自动档
|
||
SaveRestoreOrchestrator.CreateManualSlot(slotIndex);
|
||
|
||
// 槽位读档
|
||
yield return SaveRestoreOrchestrator.RestoreFromSlot(slotIndex);
|
||
|
||
// 文件读档(旧档 / 调试)
|
||
yield return SaveRestoreOrchestrator.RestoreFromFile(path);
|
||
|
||
// 仅内存快照
|
||
var snap = SnapshotService.Capture();
|
||
yield return SnapshotService.Restore(snap);
|
||
|
||
// Yarn 变量(与存档无关)
|
||
YarnVariableStorage.Instance.SetValue("$foo", 1f);
|
||
```
|
||
|
||
正式槽位路径:`persistentDataPath/AllOurBrokenParts/saves/slot_x/{snapshot.json, meta.json, thumbnail.png}`
|
||
|
||
P1 测试落盘路径:`persistentDataPath/AllOurBrokenParts/snapshot_test/latest_snapshot.json`(首次访问槽位系统时会尝试迁移到 `slot_0`)
|
||
|
||
## 目录结构
|
||
|
||
```
|
||
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 + 落盘 + 读盘编排
|
||
├── DeepRepairSnapshot.cs 深度维修占位(P5)
|
||
├── DeepRepairDataRegistry.cs 旧 IData 容器(P5 前)
|
||
├── 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
|
||
└── ScreenSnapshotProvider.cs order 70
|
||
```
|
||
|
||
## 存盘 / 读盘流程
|
||
|
||
```
|
||
存盘:
|
||
SaveRestoreOrchestrator
|
||
→ SnapshotService.Capture()
|
||
→ SnapshotCapture(场景 + 锚点 + Yarn 变量 + 各 Provider)
|
||
→ SnapshotPersistence.Save()
|
||
|
||
读盘:
|
||
SaveRestoreOrchestrator
|
||
→ SnapshotPersistence.Load() (或 legacy 分支)
|
||
→ SnapshotService.Restore()
|
||
→ SnapshotRestore(见「读档还原编排」)
|
||
```
|
||
|
||
## 读档还原编排(终态 vs P1 代码)
|
||
|
||
> **勿将当前代码当作终态。** P1 中 `ISnapshotProvider.Restore` 统一返回 `IEnumerator`,`SnapshotRestore` 对每个 Provider 做 `yield return`——这是受旧 `IData.Load()` 影响的**临时 scaffolding**。终态见设计文档 **§4.1 / D6**;P4 实施前应 refactor。
|
||
|
||
### 终态:Phase + Barrier
|
||
|
||
读档分阶段推进,仅在**必须等待**的边界上 `yield`:
|
||
|
||
```
|
||
Phase 0 Yarn 变量(sync)
|
||
↓
|
||
Phase 1 场景加载(Barrier:LoadSceneAsync)← 框架直管,不走 Provider
|
||
↓
|
||
Phase 1.5 设置章节 SO(sync)← 框架直管,从 anchor.sceneSoName 读取
|
||
↓
|
||
Phase 2 env / actor / audio / timeline / fix / screen(Provider 按 RestoreOrder)
|
||
↓
|
||
Phase 2′ 可选 Barrier(如 Timeline Addressable 须显式等待)
|
||
↓
|
||
Phase 3 加载对话工程 + RestoreAnchor(Barrier:StartDialogue)← 框架直管
|
||
↓
|
||
Phase 4 P4:淡入淡出等演出时序
|
||
```
|
||
|
||
- **核心层**(`scene`、`anchor`、`yarnVariables`)由 `SnapshotRestore` 框架直管,不通过 Provider 注册。
|
||
- **表现层**(`sections` 内各子系统)通过 `ISnapshotProvider` 扩展;`RestoreOrder` 表示同 Phase 内的建议顺序或软依赖。
|
||
- **逐项还原(D1)**指各子系统各自写回状态,**不是** Provider 之间逐步 `yield return`。
|
||
|
||
### P1 临时形态 vs P4 目标
|
||
|
||
| | P1(当前) | P4 目标 |
|
||
| --- | --- | --- |
|
||
| 核心层还原 | 硬编码在 `SnapshotRestore` | 保持框架直管 |
|
||
| Provider 还原 | 全部 `IEnumerator Restore` | 默认 `void Restore`;仅需 async 加载的实现显式协程 |
|
||
| 编排 | `foreach` 逐步 `yield return` | Phase 编排,仅 Barrier 步骤 `yield` |
|
||
| Manager | 部分 `IEnumerator RestoreSnapshot` 仅 `yield break` | 默认 `void`;真有 async 才保留协程 |
|
||
|
||
### 已知偏离(tech debt)
|
||
|
||
- `DirectorHandler` 在 Addressable Timeline 路径下内部 `StartCoroutine`,Provider 已返回,编排层无法感知——P4 应改为可等待路径。
|
||
- P2/P3 新增代码**不要**再复制「全 Provider 协程链」模式。
|
||
|
||
## 快照 JSON 结构(schemaVersion = 1)
|
||
|
||
| 字段 | 含义 |
|
||
| --- | --- |
|
||
| `scene` | 当前场景名(Addressable key),读档时最先加载 |
|
||
| `anchor` | 恢复锚点:`sceneSoName` + `yarnProjectId` + `nodeName`;最后阶段加载对话并重进节点 |
|
||
| `yarnVariables` | floats / strings / bools |
|
||
| `sections` | key 为 `SnapshotProviderIds`,值为各 DTO;**表现层状态(env / actor / audio / timeline / fix / screen)** |
|
||
| `deepRepair` | **P5 深度维修**(BlockPuzzle / Cutting / AnalysisMode 等)。P1 阶段已预留 `DeepRepairSnapshotDto`,但详细子系统状态暂不捕获;待存档系统主干稳定后再补充并测试 |
|
||
|
||
> 宏观信息(场景、章节、节点)直接位于 `SaveSnapshot` 根对象,不再冗余到 `sections`;槽位 / 「继续游戏」UI 需要展示时,从 `scene` / `anchor` 直接读取。
|
||
|
||
## 新增一个可存子系统(表现层)
|
||
|
||
表现层子系统通过 Provider 注册到 `sections`:
|
||
|
||
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 场景快照(P1 接口层完成)
|
||
|
||
Fix 场景(维修场景)的快照存储在 `sections["fix"]`,由 `FixSnapshotProvider`(order 65)管理。
|
||
|
||
### 为什么 FixStateMachine 放在 sections 而不是 deepRepair?
|
||
|
||
FixStateMachine 不仅管理深度维修(Eye、Memory、EmoWave 等),还管理 **Clinic(诊所)** 和 **BodyModule(插线维修)** 等非深度维修状态。把它放在 `sections` 中作为表现层子系统之一,可以避免概念错位。
|
||
|
||
### 双入口设计:Enter vs EnterImmediate
|
||
|
||
Fix 场景各 State 的 `Enter()` 通常包含相机过渡、Timeline 播放、Fade 等动画。读档时需要跳过这些动画直接到达终态,因此为 `IFixState` 增加了 `EnterImmediate()` 入口:
|
||
|
||
- 正常游戏流程 → `Enter()`(完整动画)
|
||
- 读档恢复 → `SwitchStateImmediate()` → `EnterImmediate()`(跳过动画)
|
||
|
||
读档时 `SwitchStateImmediate` **不调用前一个状态的 Exit()**,因为读档本质是覆盖当前状态,不需要清理。
|
||
|
||
### P1 已落地的接口
|
||
|
||
- `FixSnapshotDto`:state、args、moduleState、eyeColorState、isSystemOn、currentRepairSystemType
|
||
- `IFixState` 接口 + `EnterImmediate()` 默认实现
|
||
- `FixStateMachine.SwitchStateImmediate()`
|
||
- `FixSystemCenter.CaptureSnapshot()` / `RestoreSnapshot()`
|
||
- `FixSnapshotProvider` 注册到 `SnapshotRegistry.EnsureInitialized`
|
||
|
||
### 尚未实现(P4/P5)
|
||
|
||
- 各 State 的 `EnterImmediate()` 具体逻辑(当前默认 `yield break`)
|
||
- 深度维修子系统详细状态(BlockPuzzle grid、Cutting 进度等)→ P5 `deepRepair`
|
||
- Fix 场景读档的 Fade 时序 → P4 编排层
|
||
|
||
## 相关阶段
|
||
|
||
| 阶段 | 内容 |
|
||
| --- | --- |
|
||
| P2 | 基础版已落地:槽位、原子写、meta sidecar、缩略图、latest_slot、P1 测试档迁移;正式 UI 接入仍属 P6 |
|
||
| P3 | 基础版已落地:`onNodeStart` 判定、tag 白/黑名单、`no_save`、读档/暂停门控;瞬跳 content 排查与无活跃 Yarn 边界仍待补 |
|
||
| P4 | 基础版已落地:Provider sync/async 契约、Phase + Barrier 编排、读档自动存档抑制、Timeline Addressable 可等待恢复、验证窗口读档入口 |
|
||
| P5 | `deepRepair` 段、维修模块清单;还原模型单独定案,不默认套用 P1 Provider 协程链 |
|
||
| P6 | 正式存 / 读档 UI、继续游戏、新游戏覆盖自动档、游戏中读档确认等玩家流程 |
|