Files
aibis-dream/Docs/存档系统设计方案.md
T

269 lines
19 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.
# 存档系统设计方案
> 本文档为**实现层**设计文档,承接 [存档系统需求](存档系统需求.md)。
> 记录拆分方案、各部分实现思路,以及已确定的关键决策。需求未定项见文末 TODO。
## . 文档状态
- 需求来源:`Docs/存档系统需求.md`
- 当前阶段:**P1 快照层、P2 槽位/落盘基础版、P3 可存点判定基础版、P4 读档编排基础版已落地**。原 `StorageSystem` 已重命名为 `YarnVariableStorage` 并完成职责拆分;FixStateMachine 接口与 DTO 已接入;自动档 / 手动档槽位、sidecar、缩略图、原子写、latest_slot 与 P1 测试档迁移已接入;Yarn `onNodeStart` 已触发自动存档判定与写盘;读档已切换为 Provider sync/async 契约 + Phase/Barrier 编排。
- 最近更新:2026-07-17(读档入口收敛到 `GameManager`;恢复拆分为预检与执行;Legacy 旧档停止运行时恢复)。
---
## 一. 前提:旧实现的三档归类
需求§一明确:新方案**不沿用**旧实现的**恢复锚点、存档文件组织**等不可靠部分;旧系统中**仍可靠的底层能力**在后续实现阶段**再评估复用**。据此分三档:
| 档别 | 含义 | 内容 |
| --- | --- | --- |
| **A. 废弃 / 重做** | 被点名的不可靠部分,不沿用旧做法 | 恢复锚点;存档文件组织;依附其上的「无条件写盘触发」 |
| **B. 候选,待评估复用** | 旧系统中仍可靠的底层能力,到对应阶段再决定是否复用 | Yarn 变量存取能力(`IData`/`DataContainer` 已废弃) |
| **C. 全新构建** | 旧系统无对应实现 | 快照模型、槽位/落盘、可存点判定、复原编排、存读档 UI、横切项 |
> B 类只是「候选」,不作为既定地基;是否复用、复用多少,在 P1/P5 设计时单独决策。
---
## 二. 关键决策记录
| 编号 | 决策 | 取值 | 影响 |
| --- | --- | --- | --- |
| D1 | 表现类复原方式 | **逐项还原** | P1 快照内容、P4 复原方式 |
| D1.1 | Timeline 等时间序列项的复原粒度 | 复原到**结尾(或开头)状态**即可,**不**按时间轴逐帧复原 | P1/P4 |
| D2 | 自动档策略 | **单覆盖**(始终保留一份,新档覆盖旧档) | P2 |
| D3 | 槽位界面是否截图 | **需要截图** | P0/P2 |
| D4 | 「继续游戏」指向 | **最近保存的存档**(自动 / 手动中时间最新的一份) | P6 |
| D5 | 新游戏与既有档关系 | **覆盖自动存档,不影响其他(手动)存档** | P6 |
| D6 | 读档还原编排 | **Phase + Barrier**:少数异步边界 `yield`,其余同步批量还原 | P4 编排、`ISnapshotProvider` 契约 |
| D7 | 维修子模块存取 | **统一 `sections` + Provider**,不单独设 `deepRepair` 根字段 | P1 快照结构、新增维修模块方式 |
### 决策展开
- **D1 逐项还原**:表现类(角色动画、环境、Timeline、UI 遮罩等)在读档时由快照中记录的状态逐项恢复,而非「重进节点重演」。
- **D1.1 时间序列粒度**:Timeline 等只需复原到终态或初态,不记录播放进度。
- **D6 Phase + Barrier**:读档不是「每个 Provider 串行 `yield return` 的协程链」。编排层按阶段推进,仅在**必须等待**的边界上挂起(场景加载、锚点重进、P4 淡入淡出、部分 async Provider 还原);同阶段内的 sync 还原应连续调用,不必逐步 yield。详见 §4.1。
- **D7 统一 sections**BlockPuzzle / Memory / Cutting 等维修子模块与 env / actor / fix 一样,各建 DTO + Provider 写入 `sections`,用 `RestoreOrder` 表达还原顺序。不再维护独立的 `SaveSnapshot.deepRepair` 段或 `DeepRepairSavePolicy`
- **D2~D5**:见上表;P2/P6 实现时使用。
---
## 三. 两个被点名不可靠点的全新设计(A 档)
### 1. 恢复锚点(全新)
- **锚点定义**YarnProject 标识 + 节点名 + 进入该节点那一刻的宏观阶段。
- **落点时机**:P3 可存点判定通过后(当前代码由 `DialogController.OnNodeStart` 传入触发节点名,`SnapshotCapture` 以该节点作为 anchor)。
- **瞬跳节点(规范层,待脚本排查 + 实现补强)**:凡进入后不向玩家停留、仅作路由/阶段切换的节点,均不得作为存档边界。维修流程中 **`center` 明确属于瞬跳**`start` / `init` / `event` / `end` 等 tag 已在 `SavePointEvaluator` 黑名单。部分 **`content` 瞬跳节点**(如 `UF检查状态`)须 Yarn 侧标 `no_save`,详见 [Yarn 维修节点类型规范 §8](Yarn维修节点类型规范.md#8-瞬跳节点与存档边界)。另:若判定通过后在 settle 帧内 Yarn 已连跳,锚点 nodeName 可能漂移——属实现层已知问题,与瞬跳规范一并处理。
- **重入方式**:读档后 `SnapshotRestore.RestoreAnchor``StartDialogue(节点名)`
### 2. 存档文件组织(全新)
- 槽位制、sidecar、原子写、截图缩略图——**P2 基础实现已落地**,统一由 `SlotManager` / `SlotDirectory` 写入 `persistentDataPath/AllOurBrokenParts/demo_saves/slot_x/`。Steam 云存档目前仅保留 `CloudSaveManager` / `ICloudSaveBackend` 扩展点,尚未接真实平台后端。
---
## 四. 代码架构:职责拆分(已实现)
`StorageSystem`(现 `YarnVariableStorage`)曾同时承担 Yarn 变量、深度维修容器、快照 I/O、读档编排,现已拆成以下类。**禁止再往 `YarnVariableStorage` 上堆存档逻辑。**
```
┌─────────────────────┐
│ YarnVariableStorage │ Yarn 运行时变量(VariableStorageBehaviour
└──────────┬──────────┘
│ GetAllVariables / SetAllVariables
┌──────────▼──────────┐
│ SnapshotService │ Capture() / Restore() — 纯内存快照
└──────────┬──────────┘
┌───────────────────┼───────────────────┐
│ │ │
┌──────────▼─────────┐ ┌───────▼────────┐
│ SnapshotPersistence │ │ ISnapshotProvider × N │
│ 文件读写(P2 接管) │ │ env/actor/fix/… │
└──────────┬─────────┘ └──────────────────────┘
┌──────────▼─────────────────┐
│ SaveRestoreOrchestrator │ 存读档流程:UI 反馈、落盘、还原、旧档兼容
└────────────────────────────┘
```
| 类 | 路径 | 职责 |
| --- | --- | --- |
| `YarnVariableStorage` | `Game Loop/YarnVariableStorage.cs` | 仅 Yarn `$` / `$global_` 变量读写 |
| `SnapshotService` | `SaveSystem/SnapshotService.cs` | `Capture()`,不涉及文件 |
| `SnapshotPersistence` | `SaveSystem/SnapshotPersistence.cs` | JSON 序列化 / 反序列化;旧档格式检测 |
| `SlotManager` / `SlotDirectory` | `SaveSystem/SlotManager.cs` / `SaveSystem/SlotFileSystem.cs` | P2 槽位业务、sidecar、缩略图、latest_slot、原子写 |
| `SaveRestoreOrchestrator` | `SaveSystem/SaveRestoreOrchestrator.cs` | 保存入口;读档预检与 Snapshot 内部执行阶段;不拥有会话状态 |
| `SnapshotCapture` / `SnapshotRestore` | `SaveSystem/` | 快照组装与逐项 provider 还原 |
| `SnapshotSerializer` | `SaveSystem/` | schemaVersion、稳定 SaveId key |
| `SnapshotRegistry` + `Providers/*` | `SaveSystem/` | 表现层 + 维修子模块 Providerenv / actor / fix / bodyModule …) |
### 调用约定
| 场景 | 入口 |
| --- | --- |
| Yarn `onNodeStart` 自动存档 | `SavePointEvaluator.CanAutoSave()``SaveRestoreOrchestrator.AutoSaveRoutine(nodeName)` |
| 手动存档 | `SaveRestoreOrchestrator.CreateManualSlot(slotIndex)`(复制最近自动档) |
| 槽位读档 | `GameManager.Instance.TryRestoreSlot(slotIndex, completed)` |
| 仅捕获内存快照 | `SnapshotService.Capture()` |
| Yarn 变量 | `YarnVariableStorage.Instance.SetValue / TryGetValue` |
### 4.1 读档还原编排与 Provider 契约(重要)
当前实现已经采用同步 / 异步 Provider 显式契约,并由 `GameManager` 在会话命令外层调用 `PrepareRestore``ExecutePreparedRestore`
#### 终态模型:Phase + Barrier,而非协程链
读档还原由 `SnapshotRestore` / `SaveRestoreOrchestrator`(P4 扩展)按**阶段**编排,阶段之间用 **Barrier(必须等完再继续)** 分隔:
```
Phase 0 同步预备
└─ Yarn 变量 SetAllVariables
Phase 1 Barrier(异步)
└─ 场景加载(Addressables LoadSceneAsync
Phase 2 同步批量还原(同帧连续调用,不逐步 yield)
└─ macro / env / actor / audio / timeline / fix / screen …
Phase 2 可选 Barrier(异步,按需)
└─ Timeline Addressable 加载、其他须上报的 async 还原
Phase 3 Barrier(异步)
└─ RestoreAnchorStop + StartDialogue
Phase 4 BarrierP4:淡入淡出等演出时序)
└─ 黑屏 / Loading / FadeIn …
```
- **Barrier**:该步完成前不进入下一阶段(如场景未加载完不还原 actorYarn 未 LoadDialog 不 StartDialogue)。
- **同步批量**:Phase 2 内各子系统在主线程上连续调用即可,**不需要** Provider 之间逐步 `yield return`;这与 D1「逐项还原」不矛盾——「逐项」指各子系统各自写回状态,不是指每步之间必须挂起协程。
- **并行**:Unity 主线程上不做多线程并行;此处「不必串行 yield」指**不必为 sync 还原逐步挂协程**,而非 CPU 多线程并行。
#### 哪些步骤属于 Barrier(异步边界)
| 步骤 | 是否 Barrier | 说明 |
| --- | --- | --- |
| Yarn 变量 | 否(Phase 0 sync | `SetAllVariables` |
| 场景加载 | **是** | 唯一 P1 中 Provider 层真正需要等待的 async |
| macro / env / actor / audio / fix / screen | 否(Phase 2 sync | 当前实现均为同步写回;fix 的 `EnterImmediate` 默认 `yield break` |
| timeline(本地终态) | 否(Phase 2 sync | `RestoreAtEndLocal` 等 |
| timelineAddressable | **可选 Barrier** | 若要求进节点前 Timeline 终态就绪,须显式等待;**禁止** fire-and-forget(见下) |
| RestoreAnchor | **是** | `StartDialogue` 为 Yarn 异步 API |
| P4 淡入淡出 | **是** | 演出时序 |
| P5 深度维修 | **部分 Barrier** | 如 Fix cue `EnterImmediate`、async Provider 还原须等待;与其他 section 同走 Provider 编排 |
#### `RestoreOrder` 的语义(终态)
- 表示**同 Phase 内的建议顺序或软依赖**(如 scene 必须先于 env/actor),**不是**「每步之间必须 `yield return`」。
- 硬依赖应通过 **Phase 划分 + Barrier** 表达,而不是无限细化 RestoreOrder 数字。
- scene 加载(Phase 1 Barrier)必须在 Phase 2 之前完成;目标章节已在 PrepareRestore 中解析并放入 `SnapshotRestoreContext`env / actor / audio / timeline / fix / screen 之间目前无硬依赖,Phase 2 内一批执行即可。
#### Provider 契约(当前实现)
| 类型 | 契约 | 使用规则 |
| --- | --- | --- |
| 同步还原 | `ISyncSnapshotProvider.Restore(object dto, context)` | 同一 Phase 内按 RestoreOrder 连续调用 |
| 异步还原 | `IAsyncSnapshotProvider.RestoreAsync(object dto, context)` | 必须把完整等待过程交给编排层 |
| 编排层 | `SnapshotRestore` + `SaveRestoreOrchestrator` | Phase 编排,仅对异步 Provider、Scene、Readiness、Anchor 等 Barrier `yield` |
新增可存子系统时:**默认实现 sync `Restore`**;仅当存在必须等待的加载/状态切换时,才实现 async 接口并向编排层**上报**可等待句柄,不得在 Provider 内私自 `StartCoroutine` 而不纳入 Barrier。
#### 对后续阶段的影响
- **P2 / P3**:新增代码不得复制「全 Provider 协程链」模式。
- **P4**:当前已完成 Prepare / Execute 边界、Provider 契约、SceneReadiness 与 GameManager 会话编排;后续主要补 Play Mode 存档样本验证。
- **P5**:深度维修子模块与其他子系统一样经 sections Provider 扩展,**不应**再假设独立 Phase 或 `deepRepair` 根字段;是否独立 Phase 仅由具体 Provider 的 async 需求决定。
---
## 五. 拆分方案(分层 × 分阶段)
```
P0 契约定义 ── 已完成
P1 快照层 ── 已实现(含原 StorageSystem 职责拆分与重命名)
┌────┴───────────────┐
P2 槽位/落盘层基础版 P3 可存点判定基础版
已落地:slot_0/slot_1..8、sidecar、缩略图、原子写、latest_slot、onNodeStart 门控
└────┬───────────────┘
P4 复原编排层基础版(Provider 契约 + Phase/Barrier + 抑制自动存档)── 已落地
┌────┴────┐
P5 维修子模块 section 扩展 P6 存/读档 UI & 流程
P7 横切
```
### P1 快照层(已实现)
- `SaveSnapshot` 纯数据结构 + `ISnapshotProvider` 逐项还原(D1/D1.1)。
- Provider`env` / `actor` / `audio` / `timeline` / `subway` / `fix` / `screen`;维修子模块 `punchTape`(63) / `bodyModule`(66) / `eye`(67) 等,后续 BlockPuzzle / Memory 等同理扩展。
- Yarn 变量经 `YarnVariableStorage` 纳入快照;`$data.*` 死路径已移除。
- 原五类 IData 已迁入 `sections` Provider`IData`/`DataContainer`/`LoadIndex` 已移除。
- **注意**P1 中 `Restore` 统一为协程、逐步 `yield return` 属临时实现,终态见 **§4.1 / D6**;后续阶段勿在此基础上堆逻辑。
### P2 槽位/落盘层(基础版已落地)
- `SlotIndex``slot_0` 为自动档,`slot_1..slot_8` 为手动档。
- `SlotDirectory` / `SlotAtomicWriter`:槽位目录、`snapshot.json` / `meta.json` / `thumbnail.png``latest_slot.json` 与原子写。
- `SlotManager`:自动档覆盖写入、手动档复制、删除槽位、读取快照 / meta / 缩略图、UI view model、P1 测试档迁移。
- `SlotThumbnailCapture`:当前使用 `Camera.main` 渲染 640×360 PNG;失败时返回 null,不阻断存档。
- `CloudSaveManager` / `ICloudSaveBackend`:仅保留云存档扩展点,未接 Steam 后端与冲突处理。
- 手动档 = 拷贝最近自动快照,符合需求约定。
当前接入:
- `SavesPanel` / `SaveFileUIController` 使用 `SlotManager.GetSlotViewModels()`,并通过 `GameManager.TryRestoreSlot()` 发起恢复。
- 「继续游戏」使用 `SlotManager.GetLatestSlotIndex()`,由 `ChapterController` 转交 `GameManager.TryRestoreSlot()`
- 缩略图规格暂按 640×360 实装,是否满足正式 UI 仍需 P6 验证。
### P3 可存点判定层(基础版已落地)
- `DialogController.OnNodeStart` 更新当前 node/tags 后调用 `SavePointEvaluator.OnNodeStartForAutoSave()`,通过后执行 `SaveRestoreOrchestrator.AutoSaveRoutine(nodeName)`
- **Fresh 进入 vs detour 续跑**`SavePointEvaluator``yarnProjectId` 维护 `InProgressNodes`;节点尚未 `onNodeComplete` 时再次 `onNodeStart`Yarn detour 返回父节点)→ `DetourResume`,跳过自动存。`OnNodeComplete` 移出集合;切换 YarnProject / `StopDialog` 重置;读档 anchor 重进仅 `OnRestoreEnterNode` 标记,不写盘。
- **tag 规则**`hub` / `linear` / `content` 默认允许;`start` / `init` / `function` / `detour` / `center` / `performance` / `event` / `end` 默认禁止;`no_save` 覆盖一切并禁止 **OnNodeStart 自动存**
- 自动档边界 = **Fresh OnNodeStart** 且 tag 判定通过且非 `DetourResume`(不区分 hub / content / linear 的额外终身去重)。
- Yarn `<<save>>``SaveYarnCommand`)走 `CanExplicitSave`:仅全局门控,默认 omit anchor;用于节点末尾进入无对话 / Fix 交互等边界。
- `SaveRestoreOrchestrator.IsRestoring``GameManager.Session.IsPaused` 会阻止自动保存与显式 save,避免读档中覆盖自动档。
- `CreateManualSlot` 复用 `CanManualSave``slot_0` 存在且通过全局门控即可复制(含显式 `<<save>>` 写盘后的手动档)。
仍待补齐:
- 维修 Yarn 的瞬跳 `content` 节点需要继续排查并补 `no_save`,优先处理验证工具中暴露的误存节点。
- 深度维修过程中是否允许 `<<save>>` 的需求边界待确认(与可存点判定一并定案)。
- 目前验证工具主要覆盖存盘行为;复杂节点 tags 边界和读档后落点尚未形成自动化验证。
### P4~P7
- P4:已落地。`ISnapshotProvider` 已拆为捕获契约 + `ISyncSnapshotProvider` / `IAsyncSnapshotProvider`;读档由 `GameManager` 管理会话、遮罩和失败决策,`SaveRestoreOrchestrator` 负责预检与 Snapshot 内部 Phase + BarrierProvider 后增加 SceneReadiness;所有文件统一按当前 schema 预检,不保留旧格式专用分支。
- P5(部分已落地):`punchTape`/`fix`/`bodyModule`/`eye` 已迁入 sectionsBlockPuzzle / Memory / Cutting 等待新增 Provider`isSystemOn`/`currentRepairSystemType` Restore 仍延后。
- P6:接入正式存 / 读档 UI、继续游戏、新游戏覆盖自动档、游戏中读档确认等玩家流程。
- P7:Steam 云存档真实后端、版本兼容策略、全局 / 跨周目数据边界等横切项。
---
## 六. 推荐落地顺序
**P0 → P1(完成)→ P2/P3 基础版(已落地,待补边界与 UI 接入)→ P4 基础版(完成,待 Play Mode 验证)→(P5/P6)→ P7**
---
## 七. 仍待确定(TODO
- 手动档数量上限、覆盖 / 删除 / 重命名规则。
- 游戏进行中读档是否允许、是否二次确认。
- 自动存档是否需要正式可见反馈(当前 `SaveRestoreOrchestrator` 使用 `InfoPanel.ShowSaveLoading()` / `HideSaveLoading()`,是否作为最终 UX 待定)。
- 各维修子模块是否支持阶段存档的逐个清单(新增 Provider 前定案)。
- **维修 Yarn 瞬跳 `content` 节点**:按 [Yarn 维修节点类型规范 §8.4](Yarn维修节点类型规范.md#84-维修脚本中的常见瞬跳-content待排查) 全项目补 `no_save`Peipei `UF检查状态` 等优先)。
- 全局 / 跨周目数据与单局存档的边界划分。
- 缩略图最终规格与 UI 展示方式(当前 P2 基础版为 640×360 PNG)。