Files
aibis-dream/Docs/存档系统设计方案.md
T
2026-07-27 16:00:30 +08:00

20 KiB
Raw Blame History

存档系统设计方案

本文档为实现层设计文档,承接 存档系统需求。 记录拆分方案、各部分实现思路,以及已确定的关键决策。需求未定项见文末 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 统一 sectionsBlockPuzzle / 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。另:若判定通过后在 settle 帧内 Yarn 已连跳,锚点 nodeName 可能漂移——属实现层已知问题,与瞬跳规范一并处理。

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

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 在会话命令外层调用 PrepareRestoreExecutePreparedRestore

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

读档还原由 SnapshotRestore / SaveRestoreOrchestratorP4 扩展)按阶段编排,阶段之间用 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 中解析并放入 SnapshotRestoreContextenv / 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)。
  • Providerenv / actor / audio / timeline / subway / fix / screen;维修子模块 punchTape(63) / bodyModule(66) / eye(67) 等,后续 BlockPuzzle / Memory 等同理扩展。
  • Yarn 变量经 YarnVariableStorage 纳入快照;$data.* 死路径已移除。
  • 原五类 IData 已迁入 sections ProviderIData/DataContainer/LoadIndex 已移除。
  • 注意P1 中 Restore 统一为协程、逐步 yield return 属临时实现,终态见 §4.1 / D6;后续阶段勿在此基础上堆逻辑。

P2 槽位/落盘层(基础版已落地)

  • SlotIndexslot_0 为自动档,slot_1..slot_8 为手动档。
  • SlotDirectory / SlotAtomicWriter:槽位目录、snapshot.json / meta.json / thumbnail.pnglatest_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 后解析 NodeSavePolicy:普通合法节点执行进入档; interaction / save_on_exit 登记退出 checkpoint,节点完成并直接结束本轮 Dialogue 后写 state-only 档。
  • anchor.nodeName 对所有档位都保存触发节点;anchor.startDialogueOnRestore 独立决定读档时是否启动 Yarn。
  • 退出 checkpoint 在触发时冻结 node / YarnProject / TalkScene / Dialogue runDialogEnd 初始化期间使用 scoped interaction lock,下一帧捕获完成后才开放玩法输入。
  • Dialogue run / flow revision / current node / runner 停止状态的稳定性检查只用于 DialogueExit。退出节点后若开始任何新 Yarn 节点(含 detour、no_save、jump 目标或新 Dialogue),取消该节点末尾存档。
  • NodeEnterExplicitCommand 不因下一帧发生节点流转而取消;它们使用触发时冻结的 node / YarnProject / TalkScene 生成 anchor。连续产生的有效请求全部进入串行队列,后产生的 checkpoint 最终覆盖自动档。
  • Fresh 进入 vs detour 续跑SavePointEvaluatoryarnProjectId 维护 InProgressNodes;节点尚未 onNodeComplete 时再次 onNodeStartYarn detour 返回父节点)→ DetourResume,跳过自动存。OnNodeComplete 移出集合;切换 YarnProject / StopDialog 重置;读档 anchor 重进仅 OnRestoreEnterNode 标记,不写盘。
  • tag 规则hub / linear / content 默认进入时保存;interaction 默认 Dialogue 退出时保存; save_on_exit 可覆盖其他一级类型;no_save 禁止保存,并与上述退出语义冲突。
  • 自动档边界 = Fresh OnNodeStart 且 tag 判定通过且非 DetourResume(不区分 hub / content / linear 的额外终身去重)。
  • Yarn <<save>>SaveYarnCommand)保留为旧内容兼容入口;新内容使用 interactionsave_on_exit
  • 自动档 Capture 后进入串行写盘队列;后续有效 checkpoint 不再因前一档仍在写盘而被丢弃。
  • SaveRestoreOrchestrator.IsRestoringGameManager.Session.IsPaused 会阻止自动保存与显式 save,避免读档中覆盖自动档。
  • CreateManualSlot 复用 CanManualSaveslot_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 等待新增 ProviderisSystemOn/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 全项目补 no_savePeipei UF检查状态 等优先)。
  • 全局 / 跨周目数据与单局存档的边界划分。
  • 缩略图最终规格与 UI 展示方式(当前 P2 基础版为 640×360 PNG)。