269 lines
19 KiB
Markdown
269 lines
19 KiB
Markdown
# 存档系统设计方案
|
||
|
||
> 本文档为**实现层**设计文档,承接 [存档系统需求](存档系统需求.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/` | 表现层 + 维修子模块 Provider(env / 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(异步)
|
||
└─ RestoreAnchor(Stop + StartDialogue)
|
||
│
|
||
Phase 4 Barrier(P4:淡入淡出等演出时序)
|
||
└─ 黑屏 / Loading / FadeIn …
|
||
```
|
||
|
||
- **Barrier**:该步完成前不进入下一阶段(如场景未加载完不还原 actor;Yarn 未 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` 等 |
|
||
| timeline(Addressable) | **可选 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 + Barrier;Provider 后增加 SceneReadiness;所有文件统一按当前 schema 预检,不保留旧格式专用分支。
|
||
- P5(部分已落地):`punchTape`/`fix`/`bodyModule`/`eye` 已迁入 sections;BlockPuzzle / 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)。
|