Files
aibis-dream/Assets/Scripts/SaveSystem
..
2026-07-28 01:12:22 +08:00
2026-07-24 13:16:03 +08:00
2026-07-27 16:00:30 +08:00

SaveSystem 存档快照层

当前代码包含 P1 快照层、P2 槽位/落盘基础版、P3 可存点判定基础版、P4 读档编排基础版。设计文档见 Docs/存档系统设计方案.md§4.1 / D6 描述读档编排模型)。

职责边界

模块 位置 做什么
Yarn 变量 Game Loop/YarnVariableStorage.cs 运行时 $ / $global_ 读写
本目录 SaveSystem/ 快照结构、捕获/还原、槽位落盘、流程编排
槽位 / 截图 SlotManager / SlotThumbnailCapture P2 基础版:slot_0 自动档、slot_1..8 手动档、sidecar、缩略图、原子写

不要再往 YarnVariableStorage 上堆存档逻辑。

对外入口

// 自动存档(节点进入事件触发)
yield return SaveRestoreOrchestrator.AutoSaveRoutine(nodeName);

// Yarn 显式存档(<<save>>,保留来源节点但恢复时不启动 Yarn)
yield return SaveRestoreOrchestrator.ExplicitSaveRoutine();

// 手动存档:复制最近落盘档(含 OnNodeStart 与 <<save>>
SaveRestoreOrchestrator.CreateManualSlot(slotIndex);

// 槽位读档:会话命令统一从 GameManager 进入
GameManager.Instance.TryRestoreSlot(slotIndex, result => { /* 处理结果 */ });

// 仅捕获内存快照;恢复仍须通过 GameManager,以保证会话互斥和失败清理
var snap = SnapshotService.Capture();

// Yarn 变量(与存档无关)
YarnVariableStorage.Instance.SetValue("$foo", 1f);

Yarn 脚本:

// 推荐:有内容的交互入口节点
tags: interaction

// 或为其他一级类型覆盖保存时机
tags: content save_on_exit

// 仅兼容旧内容
<<save>>
  • interaction / save_on_exit 进入时不存档;对应节点完成后若直接结束本轮 Dialogue,则自动保存 state-only 档。
  • 若执行路径继续 jump / detour 到其他 Yarn 节点,退出档会取消并记录告警。
  • run / flow / current-node 稳定性只约束 DialogueExit:任何后续 Yarn 节点(包括 detourno_save)都会取消节点末尾存档。
  • 普通 NodeEnter 和兼容命令 <<save>> 不受后续节点流转影响;它们始终使用请求触发时冻结的 node / YarnProject / TalkScene 生成 anchor,并继续进入串行写盘队列。
  • 放在目标节点末尾:所有状态命令(switch_fix_system_tohide_dialogplay_timeline 等)执行完毕之后,<<jump>> / <<detour>> 之前。
  • anchor.nodeName 始终保留来源节点;anchor.startDialogueOnRestore=false,读档时只加载 YarnProject,不启动节点。
  • 退出档在 DialogEnd 同步初始化后等待一帧捕获;这一帧由 scoped interaction lock 保持输入锁定,捕获进入写盘队列后再交出控制权。
  • 绕过 tag / no_save 的自动判定(CanExplicitSave);仍受读档中、暂停、SuppressAutoSave 门控。
  • OnNodeStart 分工:常规 hub / linear / content 仍靠节点进入自动存;interaction / save_on_exit 用于即将进入无对话 / Fix 交互等边界。

P3 自动档边界(含 detour 去重)

DialogController.OnNodeStartSavePointEvaluator.OnNodeStartForAutoSave → 通过后 AutoSaveRoutine

判定顺序:

  1. 全局门控:IsRestoring / IsAutoSaveSuppressed / GameManager.pause
  2. Fresh vs detour 续跑:同一 YarnProject 下,若节点名已在 InProgressNodes(尚未 onNodeComplete)→ DetourResume不自动存(避免 detour 返回父节点时重复写盘)
  3. 将节点加入 InProgressNodes,再解析 NodeSavePolicyhub / linear / content 进入时保存;interaction / save_on_exit 在 Dialogue 正常结束时保存 state-only 档;detour / function 等默认禁止

OnNodeComplete 将节点移出 InProgressNodesStartDialog / LoadDialog 切换 YarnProject.name 时、StopDialog 时重置集合。读档重进 anchor 走 OnRestoreEnterNode(只标记 InProgress,不写盘)。

CanAutoSave() 仍仅做 tag + 全局门控,不含 detour 判定(供 TryAutoSave 等无节点进入上下文的路径)。显式 <<save>> / 手动档复制不经过 visit 判定。

逻辑集中在 SavePointEvaluator.cs不新增独立 Tracker 文件。

正式槽位路径:persistentDataPath/AllOurBrokenParts/demo_saves/slot_x/{snapshot.json, meta.json, thumbnail.png}

目录结构

SaveSystem/
├── README.md                    ← 本文件
├── ISnapshotProvider.cs         契约:Capture / RestoreP1 临时形态,见下)
├── SaveSnapshot.cs              快照根对象 + 各 section DTO + SaveSnapshotSummary
├── SnapshotRegistry.cs          provider 稳定 id、注册表、启动注册
├── SnapshotCapture.cs           组装 SaveSnapshotscene / anchor 由框架直管)
├── SnapshotRestore.cs           写回运行时;含 section JObject → 强类型转换
├── SnapshotService.cs           Capture / Restore 对外 API
├── SnapshotPersistence.cs       文件读写、JSON 序列化、旧档检测
├── SaveRestoreOrchestrator.cs   UI + 落盘 + 读盘编排
├── SaveYarnCommand.cs           Yarn <<save>> 显式存档
├── 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
    ├── FixPanelSnapshotProvider.cs   order 66
    ├── ShowcaseSnapshotProvider.cs   order 69
    ├── Day2SleepPresentationSnapshotProvider.cs order 70
    ├── PlayToolSnapshotProvider.cs   order 71
    └── ScreenSnapshotProvider.cs     order 80

存盘 / 读盘流程

存盘:
  SaveRestoreOrchestrator
    → SnapshotService.Capture()
    → SnapshotCapture(场景 + 锚点 + Yarn 变量 + 各 Provider
    → SnapshotPersistence.Save()

读盘:
  GameManager
    → SaveRestoreOrchestrator.PrepareRestore(读盘 + 预检,不修改运行时)
    → SaveRestoreOrchestrator.ExecutePreparedRestore
    → SnapshotRestore(变量 + Scene + Providers + Anchor
    → GameManager 提交章节和会话状态

读档还原编排

读档已经采用 Prepare / Execute 边界与 Phase + Barrier 模型。所有文件统一反序列化,并由 schema 与必填字段预检判断是否兼容;不存在旧格式专用识别、恢复或转写分支。

Phase + Barrier

读档分阶段推进,仅在必须等待的边界上 yield

Phase 0  Yarn 变量(sync
    ↓
Phase 1  场景加载(BarrierLoadSceneAsync)← 框架直管,不走 Provider
    ↓
Phase 2  env / actor / audio / timeline / fix / showcase / day2SleepPresentation / playTool / screenProvider 按 RestoreOrder
         └─ timeline`Stopped` = untouched(从未 Evaluate),读档仅还原 `isActive`,不 Reset/Evaluate
    ↓
Phase 2 可选 Barrier(如 Timeline Addressable 须显式等待)
    ↓
Phase 2.5 SceneReadinessBarrier
    ↓
Phase 3  加载对话工程 + RestoreAnchorBarrierStartDialogue)← 框架直管
    ↓
Postflight 校验
  • 核心层sceneanchoryarnVariables)由 SnapshotRestore 框架直管,不通过 Provider 注册。
  • 表现层sections 内各子系统)通过 ISnapshotProvider 扩展;RestoreOrder 表示同 Phase 内的建议顺序或软依赖。
  • **逐项还原(D1)**指各子系统各自写回状态,不是 Provider 之间逐步 yield return
  • Postflight 只核对 Scene、TalkSceneSO、YarnProject 与必要 Provider readiness;不要求 RestartNode 仍停在 anchor,也不校验 StateOnly 结束后的 DialogueRunner 状态。

Provider 使用 ISyncSnapshotProviderIAsyncSnapshotProvider 显式声明同步/异步恢复;异步 Provider 必须把等待过程返回给编排层,禁止内部 fire-and-forget。

快照 JSON 结构(schemaVersion = 2

字段 含义
scene 当前场景名(Addressable key),读档时最先加载
anchor 恢复锚点:sceneSoName + yarnProjectId + nodeName;最后阶段加载对话并重进节点
yarnVariables floats / strings / bools
sections key 为 SnapshotProviderIds,值为各 DTO;表现层与维修子模块(env / actor / fix / bodyModule / blockPuzzle …)统一在此

宏观信息(场景、章节、节点)直接位于 SaveSnapshot 根对象,不再冗余到 sections;槽位 / 「继续游戏」UI 需要展示时,从 scene / anchor 直接读取。

新增一个可存子系统

所有可存子系统(含维修小游戏)均通过 Provider 注册到 sections,用 RestoreOrder 控制同 Phase 内的还原顺序:

  1. SaveSnapshot.cs 增加 DTO 类。
  2. SnapshotRegistry.csSnapshotProviderIds 区域增加稳定 id,并加入 RequiredForCapture(若 P1 必须存)。
  3. 在对应 Manager 实现 CaptureSnapshot / RestoreSnapshot默认 sync void;仅确有 async 加载/状态切换时用 IEnumerator 并向编排层上报)。
  4. 新建 Providers/XxxSnapshotProvider.cs 实现 ISnapshotProviderP4 后:sync 默认 + 显式 async 接口)。
  5. SnapshotRegistry.EnsureInitializedRegister
  6. SnapshotRestore.CoerceSectionDto 增加 JObject 转换分支。

禁止在 Provider 内私自 StartCoroutine 而不纳入编排 Barrier。

例外scene(场景加载)与 anchor(章节+节点重进)属于核心叙事坐标,由 SnapshotCapture / SnapshotRestore 框架直管,不走 Provider 注册。新增「核心层」字段需修改 SaveSnapshot 根对象与对应编排方法。

Fix 场景快照

Fix 场景与维修子模块的快照均存储在 sections,由各自 Provider 按 RestoreOrder 还原。

双入口设计:Enter vs EnterImmediate

Fix 场景各 State 的 Enter() 通常包含相机过渡、Timeline 播放、Fade 等动画。读档时需要跳过这些动画直接到达终态,因此为 IFixState 增加了 EnterImmediate() 入口:

  • 正常游戏流程 → Enter()(完整动画)
  • 读档恢复 → SwitchStateImmediate()EnterImmediate()(跳过动画)

读档时 SwitchStateImmediate 不调用前一个状态的 Exit(),因为读档本质是覆盖当前状态,不需要清理。

已落地的维修 section

section id DTO RestoreOrder 说明
punchTape PunchTapeSnapshotDto 63 打孔带收藏
fix FixSnapshotDto 65 FixSceneDirector 宏观模式(state + args
fixPanel FixPanelSnapshotDto 66 FixPanel 壳层 + 线缆/插头 + TaskPanel(请求展开状态与任务列表)
bodyModule BodyModuleSnapshotDto 67 插线模块物理态
eye EyeSnapshotDto 68 Eye 叙事阶段

还原顺序punchTape → fixcue)→ fixPanel → bodyModule → eye → showcase → day2SleepPresentation → playTool → screen。fixPanel 须在 fix 之后,以覆盖 Cue EnterImmediate 中的 ResetPlug,并让 Memory Cue 先建立 TaskPanel 临时隐藏状态;D2 动态表现须在通用 Showcase 图片之后恢复,才能叠加虚焦、缩放或场景专属动画。

维修场景门控FixSceneSnapshotHelper):上述 section 仅在 FixSystemCenter.Instance != null 时 Capture/Restore。

尚未实现

  • 部分 FixCue 的 EnterImmediate() 具体逻辑(HuoShan / BlockPuzzle / Cutting 等仍用默认空实现)
  • BlockPuzzle / Memory / Cutting 等子模块的 section DTO + Provider
  • fixPanel 扩展:灯光状态(P1)、RepairSystemManager 缩放/offsetP2);ScreenPanel 日志文本

相关阶段

阶段 内容
P2 基础版已落地:槽位、原子写、meta sidecar、缩略图、latest_slot、P1 测试档迁移;正式 UI 接入仍属 P6
P3 基础版已落地:onNodeStart 判定、tag 白/黑名单、no_save、读档/暂停门控、detour 返回 InProgress 去重<<save>> 显式 StateOnly 存档已落地
P4 基础版已落地:Provider sync/async 契约、Phase + Barrier 编排、读档自动存档抑制、Timeline Addressable 可等待恢复、验证窗口读档入口
P5 维修子模块 section 扩展(BlockPuzzle / Memory / Cutting 等待新增 Provider
P6 正式存 / 读档 UI、继续游戏、新游戏覆盖自动档、游戏中读档确认等玩家流程