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

14 KiB
Raw Blame History

存档系统设计方案

本文档为实现层设计文档,承接 存档系统需求

记录拆分方案、各部分实现思路,以及已确定的关键决策。需求未定项见文末 TODO。

. 文档状态

  • 需求来源:Docs/存档系统需求.md

  • 当前阶段:P1 快照层已实现;原 StorageSystem 已重命名为 YarnVariableStorage 并完成职责拆分。

  • 最近更新:2026-06-02(明确读档还原编排终态与 P1 Provider 临时形态)。


一. 前提:旧实现的三档归类

需求§一明确:新方案不沿用旧实现的恢复锚点、存档文件组织等不可靠部分;旧系统中仍可靠的底层能力在后续实现阶段再评估复用。据此分三档:

| 档别 | 含义 | 内容 |

| --- | --- | --- |

| 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 契约 |

决策展开

  • D1 逐项还原:表现类(角色动画、环境、Timeline、UI 遮罩等)在读档时由快照中记录的状态逐项恢复,而非「重进节点重演」。

  • D1.1 时间序列粒度:Timeline 等只需复原到终态或初态,不记录播放进度。

  • D6 Phase + Barrier:读档不是「每个 Provider 串行 yield return 的协程链」。编排层按阶段推进,仅在必须等待的边界上挂起(场景加载、锚点重进、P4 淡入淡出、P5 深度维修状态机等);同阶段内的 sync 还原应连续调用,不必逐步 yield。详见 §4.1。

  • D2~D5:见上表;P2/P6 实现时使用。


三. 两个被点名不可靠点的全新设计(A 档)

1. 恢复锚点(全新)

  • 锚点定义YarnProject 标识 + 节点名 + 进入该节点那一刻的宏观阶段。

  • 落点时机P3 可存点判定通过后(当前 P1SnapshotCapture 在进入节点写档时捕获 GetCurrentNodeContext())。

  • 重入方式:读档后 SnapshotRestore.RestoreAnchorStartDialogue(节点名)

2. 存档文件组织(全新)

  • 槽位制、sidecar、原子写、Steam 目录——P2 实现P1 仅用 SnapshotPersistence 写固定测试路径。

四. 代码架构:职责拆分(已实现)

StorageSystem(现 YarnVariableStorage)曾同时承担 Yarn 变量、深度维修容器、快照 I/O、读档编排,现已拆成以下类。禁止再往 YarnVariableStorage 上堆存档逻辑。


                    ┌─────────────────────┐

                    │   YarnVariableStorage     │  Yarn 运行时变量(VariableStorageBehaviour

                    └──────────┬──────────┘

                               │ GetAllVariables / SetAllVariables

                    ┌──────────▼──────────┐

                    │   SnapshotService   │  Capture() / Restore() — 纯内存快照

                    └──────────┬──────────┘

           ┌───────────────────┼───────────────────┐

           │                   │                   │

┌──────────▼─────────┐ ┌───────▼────────┐ ┌───────▼──────────────────┐

│ SnapshotPersistence │ │ ISnapshotProvider × N │ DeepRepairDataRegistry │

│ 文件读写(P2 接管)  │ │ scene/macro/env/…     │ 深度维修 IData(P5)   │

└──────────┬─────────┘ └──────────────────────┘ └────────────────────────┘

           │

┌──────────▼─────────────────┐

│ SaveRestoreOrchestrator    │  存读档流程:UI 反馈、落盘、还原、旧档兼容

└────────────────────────────┘

| 类 | 路径 | 职责 |

| --- | --- | --- |

| YarnVariableStorage | Game Loop/YarnVariableStorage.cs | 仅 Yarn $ / $global_ 变量读写 |

| SnapshotService | SaveSystem/SnapshotService.cs | Capture() / Restore(),不涉及文件 |

| SnapshotPersistence | SaveSystem/SnapshotPersistence.cs | JSON 读写;P1 测试路径;旧档格式检测 |

| SaveRestoreOrchestrator | SaveSystem/SaveRestoreOrchestrator.cs | SaveToTestPath() / RestoreFromFile();保存 UIlegacy 分支 |

| DeepRepairDataRegistry | SaveSystem/DeepRepairDataRegistry.cs | RegisterData / LoadByJsonFixSystem 等 P5 前临时) |

| SnapshotCapture / SnapshotRestore | SaveSystem/ | 快照组装与逐项 provider 还原 |

| SnapshotSerializer | SaveSystem/ | schemaVersion、稳定 SaveId key |

| SnapshotRegistry + Providers/* | SaveSystem/ | 7 个表现类 / 宏观 provider |

调用约定

| 场景 | 入口 |

| --- | --- |

| <<auto_save>> | SaveRestoreOrchestrator.SaveToTestPath() |

| 读档 | SaveRestoreOrchestrator.RestoreFromFile(path) |

| 仅捕获内存快照(P2 手动档固化) | SnapshotService.Capture() |

| 深度维修注册 | DeepRepairDataRegistry.RegisterData(...) |

| Yarn 变量 | YarnVariableStorage.Instance.SetValue / TryGetValue |

P1 测试落盘路径

persistentDataPath/AllOurBrokenParts/snapshot_test/latest_snapshot.jsonConstRef.SnapshotTestPath

4.1 读档还原编排与 Provider 契约(重要)

勿将 P1 代码形态当作终态架构。 当前 ISnapshotProvider.Restore 统一返回 IEnumerator,且 SnapshotRestore 对每个 Provider 做 yield return,这是受旧 IData.Load() 影响的临时 scaffolding,容易误导后续 P4/P5 规划。本节描述终态模型;P4 实施前应据此 refactor 编排层与 Provider 契约。

终态模型:Phase + Barrier,而非协程链

读档还原由 SnapshotRestore / SaveRestoreOrchestratorP4 扩展)按阶段编排,阶段之间用 Barrier(必须等完再继续) 分隔:


Phase 0  同步预备

         └─ Yarn 变量 SetAllVariables

              │

Phase 1  Barrier(异步)

         └─ 场景加载(Addressables LoadSceneAsync

              │

Phase 2  同步批量还原(同帧连续调用,不逐步 yield)

         └─ macro / env / actor / audio / timeline / 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 / screen | 否(Phase 2 sync | 当前实现均为同步写回 |

| timeline(本地终态) | 否(Phase 2 sync | RestoreAtEndLocal 等 |

| timelineAddressable | 可选 Barrier | 若要求进节点前 Timeline 终态就绪,须显式等待;禁止 fire-and-forget(见下) |

| RestoreAnchor | | StartDialogue 为 Yarn 异步 API |

| P4 淡入淡出 | | 演出时序 |

| P5 深度维修 | 部分 Barrier | 如 FixSystemData.Load()SwitchState 须等待;不宜硬套「与 scene 同款的单一 Provider 协程」 |

RestoreOrder 的语义(终态)

  • 表示同 Phase 内的建议顺序或软依赖(如 scene 必须先于 env/actor),不是「每步之间必须 yield return」。

  • 硬依赖应通过 Phase 划分 + Barrier 表达,而不是无限细化 RestoreOrder 数字。

  • scene10)必须在 Phase 2 之前完成;macro 必须在 RestoreAnchor 之前完成;env / actor / audio / timeline / screen 之间目前无硬依赖,Phase 2 内一批执行即可。

Provider 契约:终态 vs P1 临时形态

| | P1 临时形态(当前代码) | 终态(P4 前 refactor 目标) |

| --- | --- | --- |

| 同步还原 | IEnumerator Restore + yield break | void Restore(object dto) |

| 异步还原 | 同上(仅 scene 真正 yield | IAsyncSnapshotRestore.RestoreAsync(object dto) 或等价显式接口 |

| Manager 层 | 部分 IEnumerator RestoreSnapshotyield break | 默认 void RestoreSnapshot;仅真有 async 处保留 IEnumerator |

| 编排层 | foreach 逐步 yield return provider.Restore | Phase 编排 + 仅对 Barrier 步骤 yield |

新增可存子系统时:默认实现 sync Restore;仅当存在必须等待的加载/状态切换时,才实现 async 接口并向编排层上报可等待句柄,不得在 Provider 内私自 StartCoroutine 而不纳入 Barrier。

已知偏离(tech debt,非推荐 pattern

  • DirectorHandler.RestoreSnapshotEntry 在 Addressable 路径下内部 StartCoroutine(RestoreAtEndFromAddressable),Provider 已返回,编排层无法感知完成——与 D6 冲突。P4 编排时应改为可等待的还原路径,或纳入 Phase 2 Barrier。

  • 各 Manager 的 RestoreSnapshot 声明为 IEnumerator 但内部仅 yield break——属 IData.Load() 惯性,终态应收回到 void

对后续阶段的影响

  • P2 / P3:可不改动 SnapshotRestore;但新增代码不应再复制「全 Provider 协程链」模式。

  • P4:在 SaveRestoreOrchestrator / SnapshotRestore 上按本节 Phase 模型重写编排;顺带 refactor Provider 契约。P4 是「加 fade UI」+「修正编排模型」,而非在现有 foreach 链首尾叠 UI。

  • P5:深度维修还原更接近旧 DataContainer + LoadIndex + 部分 IData.Load() async不应假设「再注册一个 Provider、继续逐步 yield」即可;是否独立 Phase、哪些模块 Barrier,在 P5 单独定案。


五. 拆分方案(分层 × 分阶段)


P0 契约定义 ── 已完成

        │

P1 快照层 ── 已实现(含原 StorageSystem 职责拆分与重命名)

        │

   ┌────┴───────────────┐

P2 槽位/落盘层           P3 可存点判定层

   └────┬───────────────┘

        │

P4 复原编排层(按 §4.1 Phase+Barrier 重写 SnapshotRestore,并扩展淡入淡出)

        │

   ┌────┴────┐

P5 深度维修阶段存档     P6 存/读档 UI & 流程

        │

P7 横切

P1 快照层(已实现)

  • SaveSnapshot 纯数据结构 + ISnapshotProvider 逐项还原(D1/D1.1)。

  • Providerscene / macro / env / actor / audio / timeline / screen

  • Yarn 变量经 YarnVariableStorage 纳入快照;$data.* 死路径已移除。

  • DeepRepair 段可空占位(DeepRepairSavePolicy),P5 再定。

  • 深度维修旧 IData 仍经 DeepRepairDataRegistry 运行,尚未纳入新快照。

  • 注意P1 中 Restore 统一为协程、逐步 yield return 属临时实现,终态见 §4.1 / D6;后续阶段勿在此基础上堆逻辑。

P2 槽位/落盘层(待做)

  • 接管 SnapshotPersistence:槽位抽象、原子写、sidecar、截图(D2/D3)。

  • 手动档 = 拷贝最近自动快照。

P3~P7

  • P4:按 §4.1 将编排从「Provider 协程链」改为 Phase + Barrierrefactor ISnapshotProvider 为 sync 默认 + 显式 async;在 SaveRestoreOrchestrator 接入淡入淡出。不再回到 YarnVariableStorage

  • P5:深度维修还原单独定案,不默认套用 P1 Provider 协程模式(见 §4.1)。

  • P6 / P7:见需求文档与上文决策表。


六. 推荐落地顺序

P0 → P1(完成)→ P2 → P3 → P4 →(P5/P6)→ P7


七. 仍待确定(TODO

  • 手动档数量上限、覆盖 / 删除 / 重命名规则。

  • 游戏进行中读档是否允许、是否二次确认。

  • 自动存档是否需要可见反馈(当前 Orchestrator 保留旧 loading UI)。

  • 各深度维修是否支持阶段存档的逐个清单(P5)。

  • 全局 / 跨周目数据与单局存档的边界划分。

  • 截图的具体规格(P2)。