diff --git a/Assets/Scripts/AssetRefs/ConstRef.cs b/Assets/Scripts/AssetRefs/ConstRef.cs
index 9980b77a9..b4c2dfb6e 100644
--- a/Assets/Scripts/AssetRefs/ConstRef.cs
+++ b/Assets/Scripts/AssetRefs/ConstRef.cs
@@ -23,6 +23,12 @@ namespace AibisDream.Utility
public static readonly string SaveFilePath = Path.Combine(Application.persistentDataPath, "AllOurBrokenParts", "saves");
+ /// P1 快照层自测落盘路径(P2 槽位层接管后迁移)。
+ public static readonly string SnapshotTestPath =
+ Path.Combine(Application.persistentDataPath, "AllOurBrokenParts", "snapshot_test");
+
+ public const string SnapshotTestFileName = "latest_snapshot";
+
public static readonly string ChapterProgressPath = Path.Combine(Application.persistentDataPath, "AllOurBrokenParts", "Config", "chapter.json");
public static readonly string CharacterConfigPath = Path.Combine(Application.streamingAssetsPath, "Config", "character.csv");
diff --git a/Assets/Scripts/SaveSystem/DeepRepairDataRegistry.cs b/Assets/Scripts/SaveSystem/DeepRepairDataRegistry.cs
new file mode 100644
index 000000000..f65b80477
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/DeepRepairDataRegistry.cs
@@ -0,0 +1,39 @@
+using System.Collections;
+using AibisDream.Framework;
+using Newtonsoft.Json.Linq;
+
+namespace AibisDream.SaveSystem
+{
+ ///
+ /// 深度维修子系统的旧 IData 注册表(基于 )。
+ ///
+ /// FixSystem / Eye / Chip 等仍通过此处 RegisterData;尚未纳入新 。
+ /// P5 评估后迁移或废弃;与 、快照 I/O 分离。
+ ///
+ ///
+ public static class DeepRepairDataRegistry
+ {
+ private static readonly DataContainer Container = new();
+
+ public static void RegisterData(T data) where T : IData
+ {
+ Container.Register(data);
+ }
+
+ public static void UnregisterData() where T : class, IData
+ {
+ Container.Unregister();
+ }
+
+ /// 旧格式存档 systemData 段还原;按 LoadIndex 顺序执行各 IData.Load()。
+ public static IEnumerator LoadByJson(JObject dataJson)
+ {
+ if (dataJson == null)
+ {
+ yield break;
+ }
+
+ yield return Container.LoadByJson(dataJson);
+ }
+ }
+}
diff --git a/Assets/Scripts/SaveSystem/DeepRepairDataRegistry.cs.meta b/Assets/Scripts/SaveSystem/DeepRepairDataRegistry.cs.meta
new file mode 100644
index 000000000..f3174e2cb
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/DeepRepairDataRegistry.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ba4cbc590c8177045b16b8a02db31121
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/SaveSystem/DeepRepairSnapshot.cs b/Assets/Scripts/SaveSystem/DeepRepairSnapshot.cs
new file mode 100644
index 000000000..499498276
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/DeepRepairSnapshot.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using Newtonsoft.Json.Linq;
+
+namespace AibisDream.SaveSystem
+{
+ ///
+ /// 深度维修是否纳入新快照、以何种粒度纳入(P5 逐个定案)。
+ ///
+ public enum DeepRepairSavePolicy
+ {
+ /// 完整阶段终点状态写入 deepRepair.sections。
+ Supported,
+
+ /// 只存简化后的关键字段。
+ Simplified,
+
+ /// 不存;读档回到进入该维修前的可存点。
+ Dropped
+ }
+
+ ///
+ /// 占位段。
+ /// P1 捕获时不写入;读档时缺省或 sections 为空则不报错。
+ /// 正式实现后替代/收敛 的旧 IData 路径。
+ ///
+ [Serializable]
+ public class DeepRepairSnapshot
+ {
+ public DeepRepairSavePolicy policy = DeepRepairSavePolicy.Dropped;
+
+ /// 按维修模块 id 分子段,具体 key 在 P5 定义。
+ public Dictionary sections = new();
+ }
+}
diff --git a/Assets/Scripts/SaveSystem/DeepRepairSnapshot.cs.meta b/Assets/Scripts/SaveSystem/DeepRepairSnapshot.cs.meta
new file mode 100644
index 000000000..a64eebf57
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/DeepRepairSnapshot.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 6ee71c8af7b93d64aa87335a011eccf5
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/SaveSystem/ISnapshotProvider.cs b/Assets/Scripts/SaveSystem/ISnapshotProvider.cs
new file mode 100644
index 000000000..459f708f5
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/ISnapshotProvider.cs
@@ -0,0 +1,31 @@
+using System.Collections;
+
+namespace AibisDream.SaveSystem
+{
+ ///
+ /// 快照提供者契约:将某一子系统的运行时状态与纯数据 DTO 互相转换。
+ ///
+ /// 设计约定:DTO 不含事件、协程或 Unity 引用; 只读,
+ /// 负责逐项写回。实现类通常很薄,实际逻辑在各 Manager 中。
+ ///
+ ///
+ public interface ISnapshotProvider
+ {
+ ///
+ /// 稳定显式 id,用作 的 key。
+ /// 见 ,不使用 C# 类型全名。
+ ///
+ string SaveId { get; }
+
+ ///
+ /// 读档还原顺序。数值越小越先执行(如 scene=10 先于 actor=40)。
+ ///
+ int RestoreOrder { get; }
+
+ /// 从当前运行时捕获该子系统的快照 DTO;无可存内容时可返回 null。
+ object Capture();
+
+ /// 将 DTO 逐项还原到运行时;可为协程以等待异步加载。
+ IEnumerator Restore(object dto);
+ }
+}
diff --git a/Assets/Scripts/SaveSystem/ISnapshotProvider.cs.meta b/Assets/Scripts/SaveSystem/ISnapshotProvider.cs.meta
new file mode 100644
index 000000000..87500a0da
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/ISnapshotProvider.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 6a7b39d650ea48340bba1daa7387d119
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/SaveSystem/Providers.meta b/Assets/Scripts/SaveSystem/Providers.meta
new file mode 100644
index 000000000..7c09ab0fa
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/Providers.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: ce5fddeb465208c4ea3dbd9eacaa8d7b
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/SaveSystem/Providers/ActorSnapshotProvider.cs b/Assets/Scripts/SaveSystem/Providers/ActorSnapshotProvider.cs
new file mode 100644
index 000000000..97b7d66c6
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/Providers/ActorSnapshotProvider.cs
@@ -0,0 +1,29 @@
+using System.Collections;
+
+namespace AibisDream.SaveSystem
+{
+ ///
+ /// sections["actor"]:场景中各角色显隐、槽位、动画状态、透明度等(ActorManager)。
+ /// RestoreOrder=40,依赖 scene 与 env 已就绪。
+ ///
+ public class ActorSnapshotProvider : ISnapshotProvider
+ {
+ public string SaveId => SnapshotProviderIds.Actor;
+ public int RestoreOrder => 40;
+
+ public object Capture()
+ {
+ return ActorManager.Instance.CaptureSnapshot();
+ }
+
+ public IEnumerator Restore(object dto)
+ {
+ if (dto is not ActorSnapshotDto actors)
+ {
+ yield break;
+ }
+
+ yield return ActorManager.Instance.RestoreSnapshot(actors);
+ }
+ }
+}
diff --git a/Assets/Scripts/SaveSystem/Providers/ActorSnapshotProvider.cs.meta b/Assets/Scripts/SaveSystem/Providers/ActorSnapshotProvider.cs.meta
new file mode 100644
index 000000000..7f522ba8d
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/Providers/ActorSnapshotProvider.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 0e062fe96fad9b545bc01559345476cb
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/SaveSystem/Providers/AudioSnapshotProvider.cs b/Assets/Scripts/SaveSystem/Providers/AudioSnapshotProvider.cs
new file mode 100644
index 000000000..92a74ac04
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/Providers/AudioSnapshotProvider.cs
@@ -0,0 +1,30 @@
+using System.Collections;
+using AibisDream.Kit;
+
+namespace AibisDream.SaveSystem
+{
+ ///
+ /// sections["audio"]:FMOD 音乐/SFX 实例路径与 AMB 状态终态(AudioManager)。
+ /// 不记录播放进度,读档后按路径重新 CreateInstance 并 start。
+ ///
+ public class AudioSnapshotProvider : ISnapshotProvider
+ {
+ public string SaveId => SnapshotProviderIds.Audio;
+ public int RestoreOrder => 50;
+
+ public object Capture()
+ {
+ return AudioManager.Instance.CaptureSnapshot();
+ }
+
+ public IEnumerator Restore(object dto)
+ {
+ if (dto is not AudioSnapshotDto audio)
+ {
+ yield break;
+ }
+
+ yield return AudioManager.Instance.RestoreSnapshot(audio);
+ }
+ }
+}
diff --git a/Assets/Scripts/SaveSystem/Providers/AudioSnapshotProvider.cs.meta b/Assets/Scripts/SaveSystem/Providers/AudioSnapshotProvider.cs.meta
new file mode 100644
index 000000000..e8fe23045
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/Providers/AudioSnapshotProvider.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 2158be13aca7fb74985d11effbf0304d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/SaveSystem/Providers/EnvironmentSnapshotProvider.cs b/Assets/Scripts/SaveSystem/Providers/EnvironmentSnapshotProvider.cs
new file mode 100644
index 000000000..ca84c6f3f
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/Providers/EnvironmentSnapshotProvider.cs
@@ -0,0 +1,29 @@
+using System.Collections;
+
+namespace AibisDream.SaveSystem
+{
+ ///
+ /// sections["env"]:场景内时间/天气(EnvironmentManager)。
+ /// RestoreOrder=20,在 scene 加载完成后、macro 之前还原环境。
+ ///
+ public class EnvironmentSnapshotProvider : ISnapshotProvider
+ {
+ public string SaveId => SnapshotProviderIds.Environment;
+ public int RestoreOrder => 20;
+
+ public object Capture()
+ {
+ return EnvironmentManager.Instance.CaptureSnapshot();
+ }
+
+ public IEnumerator Restore(object dto)
+ {
+ if (dto is not EnvironmentSnapshotDto env)
+ {
+ yield break;
+ }
+
+ yield return EnvironmentManager.Instance.RestoreSnapshot(env);
+ }
+ }
+}
diff --git a/Assets/Scripts/SaveSystem/Providers/EnvironmentSnapshotProvider.cs.meta b/Assets/Scripts/SaveSystem/Providers/EnvironmentSnapshotProvider.cs.meta
new file mode 100644
index 000000000..8bb529916
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/Providers/EnvironmentSnapshotProvider.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a879f9bc4b096c346be7af81876c792c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/SaveSystem/Providers/MacroSnapshotProvider.cs b/Assets/Scripts/SaveSystem/Providers/MacroSnapshotProvider.cs
new file mode 100644
index 000000000..1a20cb8a2
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/Providers/MacroSnapshotProvider.cs
@@ -0,0 +1,56 @@
+using System.Collections;
+using UnityEngine;
+
+namespace AibisDream.SaveSystem
+{
+ ///
+ /// sections["macro"]:章节 TalkSceneSO 与 YarnProject 绑定。
+ /// RestoreOrder=30,在场景与环境就绪后设置 GameManager 并 LoadDialog(不 Start 节点,锚点由 RestoreAnchor 负责)。
+ ///
+ public class MacroSnapshotProvider : ISnapshotProvider
+ {
+ public string SaveId => SnapshotProviderIds.Macro;
+ public int RestoreOrder => 30;
+
+ public object Capture()
+ {
+ var gameManager = GameManager.Instance;
+ var dialog = DialogController.Instance;
+ var yarnProject = dialog?.DialogueRunner?.YarnProject;
+
+ return new MacroSnapshotDto
+ {
+ sceneSoName = gameManager != null ? gameManager.GetSceneSoName() : null,
+ yarnProjectId = yarnProject != null ? yarnProject.name : null
+ };
+ }
+
+ public IEnumerator Restore(object dto)
+ {
+ if (dto is not MacroSnapshotDto macro)
+ {
+ yield break;
+ }
+
+ if (!string.IsNullOrEmpty(macro.sceneSoName))
+ {
+ GameManager.Instance.SetSceneSoByName(macro.sceneSoName);
+ }
+
+ if (!string.IsNullOrEmpty(macro.yarnProjectId))
+ {
+ var sceneSo = GameManager.Instance.GetCurrentTalkSceneSo();
+ if (sceneSo?.yarnProject != null && sceneSo.yarnProject.name == macro.yarnProjectId)
+ {
+ DialogController.Instance.LoadDialog(sceneSo.yarnProject);
+ }
+ else
+ {
+ Debug.LogWarning($"[MacroSnapshotProvider] 无法匹配 YarnProject: {macro.yarnProjectId}");
+ }
+ }
+
+ yield break;
+ }
+ }
+}
diff --git a/Assets/Scripts/SaveSystem/Providers/MacroSnapshotProvider.cs.meta b/Assets/Scripts/SaveSystem/Providers/MacroSnapshotProvider.cs.meta
new file mode 100644
index 000000000..d2883a8a3
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/Providers/MacroSnapshotProvider.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: af663fafb5b2a9748902e2e34e1f6be3
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/SaveSystem/Providers/SceneSnapshotProvider.cs b/Assets/Scripts/SaveSystem/Providers/SceneSnapshotProvider.cs
new file mode 100644
index 000000000..ef56fd906
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/Providers/SceneSnapshotProvider.cs
@@ -0,0 +1,33 @@
+using System.Collections;
+
+namespace AibisDream.SaveSystem
+{
+ ///
+ /// sections["scene"]:当前 Addressable 场景。
+ /// RestoreOrder=10,读档时最先加载场景,供后续 env/actor 等依赖。
+ ///
+ public class SceneSnapshotProvider : ISnapshotProvider
+ {
+ public string SaveId => SnapshotProviderIds.Scene;
+ public int RestoreOrder => 10;
+
+ public object Capture()
+ {
+ var sceneLoader = SceneLoader.Instance;
+ return new SceneSnapshotDto
+ {
+ sceneName = sceneLoader != null ? sceneLoader.CurrentSceneName : null
+ };
+ }
+
+ public IEnumerator Restore(object dto)
+ {
+ if (dto is not SceneSnapshotDto scene || string.IsNullOrEmpty(scene.sceneName))
+ {
+ yield break;
+ }
+
+ yield return SceneLoader.Instance.LoadSceneAsync(scene.sceneName);
+ }
+ }
+}
diff --git a/Assets/Scripts/SaveSystem/Providers/SceneSnapshotProvider.cs.meta b/Assets/Scripts/SaveSystem/Providers/SceneSnapshotProvider.cs.meta
new file mode 100644
index 000000000..7c5c2fa6d
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/Providers/SceneSnapshotProvider.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: d814b6c56374b4446baa86e09f510098
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/SaveSystem/Providers/ScreenSnapshotProvider.cs b/Assets/Scripts/SaveSystem/Providers/ScreenSnapshotProvider.cs
new file mode 100644
index 000000000..6fba58af8
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/Providers/ScreenSnapshotProvider.cs
@@ -0,0 +1,30 @@
+using System.Collections;
+
+namespace AibisDream.SaveSystem
+{
+ ///
+ /// sections["screen"]:屏幕饱和度与演出淡入淡出遮罩。
+ /// 委托 ;RestoreOrder=70,通常在表现类最后还原。
+ ///
+ public class ScreenSnapshotProvider : ISnapshotProvider
+ {
+ public string SaveId => SnapshotProviderIds.Screen;
+ public int RestoreOrder => 70;
+
+ public object Capture()
+ {
+ return ScreenSnapshotHelper.Capture();
+ }
+
+ public IEnumerator Restore(object dto)
+ {
+ if (dto is not ScreenSnapshotDto screen)
+ {
+ yield break;
+ }
+
+ ScreenSnapshotHelper.Restore(screen);
+ yield break;
+ }
+ }
+}
diff --git a/Assets/Scripts/SaveSystem/Providers/ScreenSnapshotProvider.cs.meta b/Assets/Scripts/SaveSystem/Providers/ScreenSnapshotProvider.cs.meta
new file mode 100644
index 000000000..11722efc6
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/Providers/ScreenSnapshotProvider.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 07b9ceaefa3550f4583465dd32e420f2
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/SaveSystem/Providers/TimelineSnapshotProvider.cs b/Assets/Scripts/SaveSystem/Providers/TimelineSnapshotProvider.cs
new file mode 100644
index 000000000..b0ff05e04
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/Providers/TimelineSnapshotProvider.cs
@@ -0,0 +1,30 @@
+using System.Collections;
+
+namespace AibisDream.SaveSystem
+{
+ ///
+ /// sections["timeline"]:各 PlayableDirector 的终/初态(TimelineCenter)。
+ /// 按 D1.1 只存 AtStart/AtEnd/Stopped,不存时间轴中间帧。
+ ///
+ public class TimelineSnapshotProvider : ISnapshotProvider
+ {
+ public string SaveId => SnapshotProviderIds.Timeline;
+ public int RestoreOrder => 60;
+
+ public object Capture()
+ {
+ return TimelineCenter.Instance.CaptureSnapshot();
+ }
+
+ public IEnumerator Restore(object dto)
+ {
+ if (dto is not TimelineSnapshotDto timeline)
+ {
+ yield break;
+ }
+
+ TimelineCenter.Instance.RestoreSnapshot(timeline);
+ yield break;
+ }
+ }
+}
diff --git a/Assets/Scripts/SaveSystem/Providers/TimelineSnapshotProvider.cs.meta b/Assets/Scripts/SaveSystem/Providers/TimelineSnapshotProvider.cs.meta
new file mode 100644
index 000000000..1da746c01
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/Providers/TimelineSnapshotProvider.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 2630a928f06aef14bb4a8520c743f3b5
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/SaveSystem/README.md b/Assets/Scripts/SaveSystem/README.md
new file mode 100644
index 000000000..4094a8f78
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/README.md
@@ -0,0 +1,148 @@
+# SaveSystem 存档快照层
+
+> P1 实现:纯数据快照 + 逐项还原。设计文档见 [`Docs/存档系统设计方案.md`](../../../Docs/存档系统设计方案.md)(**§4.1 / D6** 描述读档编排终态)。
+
+## 职责边界
+
+| 模块 | 位置 | 做什么 |
+| --- | --- | --- |
+| Yarn 变量 | `Game Loop/YarnVariableStorage.cs` | 运行时 `$` / `$global_` 读写 |
+| **本目录** | `SaveSystem/` | 快照结构、捕获/还原、落盘、流程编排 |
+| 深度维修(旧) | `DeepRepairDataRegistry.cs` | FixSystem 等 IData,P5 前临时保留 |
+| 槽位 / 截图 | P2 待做 | 接管 `SnapshotPersistence` 与目录布局 |
+
+**不要**再往 `YarnVariableStorage` 上堆存档逻辑。
+
+## 对外入口
+
+```csharp
+// 存盘(<>、调试)
+SaveRestoreOrchestrator.SaveToTestPath();
+
+// 读盘
+yield return SaveRestoreOrchestrator.RestoreFromFile(path);
+
+// 仅内存快照(P2 手动档固化等)
+var snap = SnapshotService.Capture();
+yield return SnapshotService.Restore(snap);
+
+// Yarn 变量(与存档无关)
+YarnVariableStorage.Instance.SetValue("$foo", 1f);
+```
+
+P1 测试落盘路径:`persistentDataPath/AllOurBrokenParts/snapshot_test/latest_snapshot.json`
+
+## 目录结构
+
+```
+SaveSystem/
+├── README.md ← 本文件
+├── ISnapshotProvider.cs 契约:Capture / Restore(P1 临时形态,见下)
+├── SaveSnapshot.cs 快照根对象 + 各 section DTO
+├── SnapshotProviderIds.cs section 稳定 id("actor" 等)
+├── SnapshotRegistry.cs provider 注册表
+├── SnapshotBootstrap.cs 启动时注册 7 个 provider
+├── SnapshotCapture.cs 组装 SaveSnapshot
+├── SnapshotRestore.cs 写回运行时 + 锚点重进节点
+├── SnapshotService.cs Capture / Restore 对外 API
+├── SnapshotPersistence.cs 文件读写、旧档检测
+├── SnapshotSerializer.cs JSON + schemaVersion
+├── SnapshotSectionDeserializer.cs JObject → 强类型 DTO
+├── SaveRestoreOrchestrator.cs UI + 落盘 + 读盘编排
+├── DeepRepairSnapshot.cs 深度维修占位(P5)
+├── DeepRepairDataRegistry.cs 旧 IData 容器(P5 前)
+├── ScreenSnapshotHelper.cs screen section 实现细节
+└── Providers/ 各子系统薄适配层
+ ├── SceneSnapshotProvider.cs order 10
+ ├── EnvironmentSnapshotProvider.cs order 20
+ ├── MacroSnapshotProvider.cs order 30
+ ├── ActorSnapshotProvider.cs order 40
+ ├── AudioSnapshotProvider.cs order 50
+ ├── TimelineSnapshotProvider.cs order 60
+ └── ScreenSnapshotProvider.cs order 70
+```
+
+## 存盘 / 读盘流程
+
+```
+存盘:
+ SaveRestoreOrchestrator
+ → SnapshotService.Capture()
+ → SnapshotCapture(锚点 + Yarn 变量 + 各 Provider)
+ → SnapshotPersistence.Save()
+
+读盘:
+ SaveRestoreOrchestrator
+ → SnapshotPersistence.Load() (或 legacy 分支)
+ → SnapshotService.Restore()
+ → SnapshotRestore(见「读档还原编排」)
+```
+
+## 读档还原编排(终态 vs P1 代码)
+
+> **勿将当前代码当作终态。** P1 中 `ISnapshotProvider.Restore` 统一返回 `IEnumerator`,`SnapshotRestore` 对每个 Provider 做 `yield return`——这是受旧 `IData.Load()` 影响的**临时 scaffolding**。终态见设计文档 **§4.1 / D6**;P4 实施前应 refactor。
+
+### 终态:Phase + Barrier
+
+读档不是「每个 Provider 串行协程链」,而是分阶段推进,仅在**必须等待**的边界上 `yield`:
+
+```
+Phase 0 Yarn 变量(sync)
+ ↓
+Phase 1 场景加载(Barrier:LoadSceneAsync)
+ ↓
+Phase 2 macro / env / actor / audio / timeline / screen(sync 批量,同帧连续调用)
+ ↓
+Phase 2′ 可选 Barrier(如 Timeline Addressable 须显式等待)
+ ↓
+Phase 3 RestoreAnchor(Barrier:StartDialogue)
+ ↓
+Phase 4 P4:淡入淡出等演出时序
+```
+
+- **逐项还原(D1)**指各子系统各自写回状态,**不是** Provider 之间逐步 `yield return`。
+- **`RestoreOrder`** 表示同 Phase 内的建议顺序或软依赖(scene 先于 env/actor 等),**不是**「每步必须挂协程」。硬依赖用 Phase / Barrier 表达。
+
+### P1 临时形态 vs P4 目标
+
+| | P1(当前) | P4 目标 |
+| --- | --- | --- |
+| Provider 还原 | 全部 `IEnumerator Restore` | 默认 `void Restore`;仅 scene 等实现显式 async |
+| 编排 | `foreach` 逐步 `yield return` | Phase 编排,仅 Barrier 步骤 `yield` |
+| Manager | 部分 `IEnumerator RestoreSnapshot` 仅 `yield break` | 默认 `void`;真有 async 才保留协程 |
+
+### 已知偏离(tech debt)
+
+- `DirectorHandler` 在 Addressable Timeline 路径下内部 `StartCoroutine`,Provider 已返回,编排层无法感知——P4 应改为可等待路径。
+- P2/P3 新增代码**不要**再复制「全 Provider 协程链」模式。
+
+## 快照 JSON 结构(schemaVersion = 1)
+
+| 字段 | 含义 |
+| --- | --- |
+| `anchor` | 恢复锚点:`nodeName` + `yarnProjectId`(独立阶段重进节点的自洽校验) |
+| `yarnVariables` | floats / strings / bools |
+| `sections` | key 为 `SnapshotProviderIds`,值为各 DTO;**宏观信息(场景/SceneSO/YarnProject)以 `scene`/`macro` 两节为唯一来源** |
+| `deepRepair` | P1 通常为空,P5 再填 |
+
+> 宏观阶段摘要不再冗余到顶层;槽位 / 「继续游戏」UI 需要展示场景名、章节、YarnProject 时,调用 `SaveSnapshotSummary.From(snapshot)` 从 `sections` 现算派生。
+
+## 新增一个可存子系统
+
+1. 在 `SaveSnapshot.cs` 增加 DTO 类。
+2. 在 `SnapshotProviderIds` 增加稳定 id,并加入 `RequiredForCapture`(若 P1 必须存)。
+3. 在对应 Manager 实现 `CaptureSnapshot` / `RestoreSnapshot`(**默认 sync `void`**;仅确有 async 加载/状态切换时用 `IEnumerator` 并向编排层上报)。
+4. 新建 `Providers/XxxSnapshotProvider.cs` 实现 `ISnapshotProvider`(P4 后:sync 默认 + 显式 async 接口)。
+5. 在 `SnapshotBootstrap.EnsureInitialized` 中 `Register`。
+6. 在 `SnapshotSectionDeserializer.Coerce` 增加 JObject 转换分支。
+
+**禁止**在 Provider 内私自 `StartCoroutine` 而不纳入编排 Barrier。
+
+## 相关阶段(未在本目录完整实现)
+
+| 阶段 | 内容 |
+| --- | --- |
+| P2 | 槽位、原子写、截图 sidecar → 扩展 `SnapshotPersistence` |
+| P3 | 可存点判定、写盘门控 |
+| P4 | 按 §4.1 重写 `SnapshotRestore`(Phase + Barrier);refactor Provider 契约;`SaveRestoreOrchestrator` 接入淡入淡出 |
+| P5 | `deepRepair` 段、维修模块清单;还原模型单独定案,不默认套用 P1 Provider 协程链 |
diff --git a/Assets/Scripts/SaveSystem/SaveRestoreOrchestrator.cs b/Assets/Scripts/SaveSystem/SaveRestoreOrchestrator.cs
new file mode 100644
index 000000000..15f485554
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/SaveRestoreOrchestrator.cs
@@ -0,0 +1,77 @@
+using System.Collections;
+using System.Collections.Generic;
+using AibisDream.Framework;
+using AibisDream.Kit;
+using AibisDream.UI;
+using Newtonsoft.Json.Linq;
+using UnityEngine;
+
+namespace AibisDream.SaveSystem
+{
+ ///
+ /// 存读档流程编排层:连接 UI、 与 。
+ ///
+ /// 对外入口:(<>)、(读档)。
+ /// P4 在此扩展淡入淡出与完整时序;P2 将替换测试路径为槽位 API。
+ ///
+ ///
+ public static class SaveRestoreOrchestrator
+ {
+ /// 捕获快照并写入 P1 测试路径;含保存 Loading UI。
+ public static void SaveToTestPath()
+ {
+ var host = YarnVariableStorage.Instance;
+ if (host == null)
+ {
+ Debug.LogError("[SaveRestoreOrchestrator] YarnVariableStorage 未初始化");
+ return;
+ }
+
+ ActionKit.Sequence()
+ .Callback(() => UIManager.Instance.GetPanel().ShowSaveLoading())
+ .Delay(1f)
+ .Callback(() => UIManager.Instance.GetPanel().HideSaveLoading())
+ .Start(host);
+
+ using (new CodeTimer("SaveSnapshot"))
+ {
+ var snapshot = SnapshotService.Capture();
+ SnapshotPersistence.Save(snapshot, SnapshotPersistence.GetTestFilePath());
+ }
+ }
+
+ /// 从文件读档并还原;自动区分新快照格式与 legacy 格式。
+ public static IEnumerator RestoreFromFile(string savePath)
+ {
+ if (SnapshotPersistence.IsLegacyFormat(savePath))
+ {
+ Debug.LogWarning("[SaveRestoreOrchestrator] 检测到旧格式存档,请使用新快照格式重新保存。");
+ yield return RestoreLegacy(savePath);
+ yield break;
+ }
+
+ SaveSnapshot snapshot;
+ using (new CodeTimer("LoadSnapshot"))
+ {
+ snapshot = SnapshotPersistence.Load(savePath);
+ }
+
+ yield return SnapshotService.Restore(snapshot);
+ }
+
+ /// 旧档:Yarn 变量 + DeepRepairDataRegistry systemData。
+ private static IEnumerator RestoreLegacy(string savePath)
+ {
+ var saveData = SnapshotPersistence.ReadLegacySaveRoot(savePath);
+ var floatDict = saveData["floatDict"]?.ToObject>();
+ var stringDict = saveData["stringDict"]?.ToObject>();
+ var boolDict = saveData["boolDict"]?.ToObject>();
+ YarnVariableStorage.Instance.SetAllVariables(floatDict, stringDict, boolDict);
+
+ if (saveData["systemData"] is JObject systemDataJson)
+ {
+ yield return DeepRepairDataRegistry.LoadByJson(systemDataJson);
+ }
+ }
+ }
+}
diff --git a/Assets/Scripts/SaveSystem/SaveRestoreOrchestrator.cs.meta b/Assets/Scripts/SaveSystem/SaveRestoreOrchestrator.cs.meta
new file mode 100644
index 000000000..de093d088
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/SaveRestoreOrchestrator.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 359343f1db2912f49bdcad4fbe443f6a
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/SaveSystem/SaveSnapshot.cs b/Assets/Scripts/SaveSystem/SaveSnapshot.cs
new file mode 100644
index 000000000..181859ec3
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/SaveSnapshot.cs
@@ -0,0 +1,181 @@
+using System;
+using System.Collections.Generic;
+
+namespace AibisDream.SaveSystem
+{
+ ///
+ /// 当前快照 schema 的版本号。结构变更时递增。
+ ///
+ public static class SaveSnapshotSchema
+ {
+ public const int CurrentVersion = 1;
+ }
+
+ ///
+ /// 存档快照根对象:描述「进入可存节点那一刻」的完整游戏状态。
+ ///
+ /// 与槽位/文件路径解耦;序列化后写入 JSON。对应需求§3 四分类:
+ /// anchor(恢复锚点)、sections(宏观 scene/macro + 表现类)、yarnVariables、deepRepair(深度维修,P5)。
+ ///
+ ///
+ /// 宏观阶段(场景 / 章节 SO / YarnProject)不单独冗余存储,统一以 sections["scene"] / sections["macro"]
+ /// 为唯一来源;槽位 UI 等需要摘要时通过 现算派生,避免重复字段漂移。
+ ///
+ ///
+ [Serializable]
+ public class SaveSnapshot
+ {
+ /// 快照结构版本,对应 。
+ public int schemaVersion = SaveSnapshotSchema.CurrentVersion;
+
+ /// 保存时的 Application.version。
+ public string gameVersion;
+
+ /// 保存时间(本地),格式 yyyy-MM-dd HH:mm:ss。
+ public string savedAt;
+
+ /// 恢复锚点:读档后从此 Yarn 节点重新进入(所见即所存)。
+ public AnchorSnapshot anchor = new();
+
+ /// 全部 Yarn 运行时变量(来自 )。
+ public YarnVariablesSnapshot yarnVariables = new();
+
+ ///
+ /// 各子系统表现类快照,key 为 中的稳定 id。
+ ///
+ public Dictionary sections = new();
+
+ ///
+ /// 深度维修段。P1 捕获时不写入(null);P5 按模块策略填充。
+ ///
+ public DeepRepairSnapshot deepRepair;
+ }
+
+ ///
+ /// 恢复锚点。读档时在场景与状态还原完成后,从 重新 StartDialogue。
+ ///
+ [Serializable]
+ public class AnchorSnapshot
+ {
+ public string yarnProjectId;
+ public string nodeName;
+ }
+
+ /// Yarn 变量三分组,与 Yarn Spinner 支持的类型一致。
+ [Serializable]
+ public class YarnVariablesSnapshot
+ {
+ public Dictionary floats = new();
+ public Dictionary strings = new();
+ public Dictionary bools = new();
+ }
+
+ #region Provider DTOs
+
+ /// sections["scene"]:当前加载的场景。
+ [Serializable]
+ public class SceneSnapshotDto
+ {
+ public string sceneName;
+ }
+
+ /// sections["macro"]:章节与 Yarn 工程绑定。
+ [Serializable]
+ public class MacroSnapshotDto
+ {
+ public string sceneSoName;
+ public string yarnProjectId;
+ }
+
+ /// sections["env"]:环境时间/天气。
+ [Serializable]
+ public class EnvironmentSnapshotDto
+ {
+ /// 枚举名。
+ public string curTime;
+
+ /// 枚举名。
+ public string curWeather;
+
+ public bool isInitialized;
+ }
+
+ /// sections["actor"]:场景中所有角色的逐项状态。
+ [Serializable]
+ public class ActorSnapshotDto
+ {
+ public List actors = new();
+ }
+
+ /// 单个角色的快照条目。
+ [Serializable]
+ public class ActorEntrySnapshotDto
+ {
+ public string actorName;
+ public string slotName;
+ public string actorType;
+ public string stateName;
+ public float alpha;
+ public float faceParam;
+ }
+
+ /// sections["audio"]:FMOD 音乐/SFX 终态与 AMB 状态(不记播放进度)。
+ [Serializable]
+ public class AudioSnapshotDto
+ {
+ /// key → FMOD event path。
+ public Dictionary musicPaths = new();
+
+ public Dictionary sfxPaths = new();
+
+ /// 枚举名。
+ public string ambState;
+ }
+
+ ///
+ /// Timeline 终/初态标记(决策 D1.1:不按时间轴逐帧还原)。
+ /// 序列化进 JSON 时使用字符串形式存入 。
+ ///
+ public enum TimelinePlaybackPhase
+ {
+ AtStart,
+ AtEnd,
+ Stopped
+ }
+
+ /// sections["timeline"]:各 PlayableDirector 的终/初态。
+ [Serializable]
+ public class TimelineSnapshotDto
+ {
+ public List entries = new();
+ }
+
+ [Serializable]
+ public class TimelineEntrySnapshotDto
+ {
+ public string directorName;
+ public bool isActive;
+
+ /// 的字符串名。
+ public string phase;
+
+ /// 若由 Addressables 加载 Timeline,记录 key;否则为空。
+ public string loadedAddressableKey;
+ }
+
+ /// sections["screen"]:屏幕饱和度与演出淡入淡出遮罩。
+ [Serializable]
+ public class ScreenSnapshotDto
+ {
+ /// Post-process Volume weight(0=正常,1=完全褪色)。
+ public float saturationWeight;
+
+ ///
+ /// 演出遮罩终态:true = 黑屏盖住(FadeIn 完成);
+ /// false = 画面可见(FadeOut 完成或遮罩未启用)。不记录转场中间 alpha。
+ ///
+ public bool isFadeScreenCovered;
+ }
+
+ #endregion
+}
diff --git a/Assets/Scripts/SaveSystem/SaveSnapshot.cs.meta b/Assets/Scripts/SaveSystem/SaveSnapshot.cs.meta
new file mode 100644
index 000000000..21744ec4f
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/SaveSnapshot.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 067e34fac1df0c1409cabbab3af2084b
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/SaveSystem/SaveSnapshotSummary.cs b/Assets/Scripts/SaveSystem/SaveSnapshotSummary.cs
new file mode 100644
index 000000000..15f1d487e
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/SaveSnapshotSummary.cs
@@ -0,0 +1,60 @@
+namespace AibisDream.SaveSystem
+{
+ ///
+ /// 从 现算的宏观摘要(场景 / 章节 SO / YarnProject)。
+ ///
+ /// 替代已移除的顶层 macroStage 冗余字段:sections["scene"] / sections["macro"] 为唯一来源,
+ /// 槽位 / 「继续游戏」等 UI 在需要展示时调用 派生,避免重复字段与读写漂移。
+ ///
+ ///
+ public readonly struct SaveSnapshotSummary
+ {
+ /// Addressable 场景 key,如 Scene/ClinicOut(来自 sections["scene"])。
+ public readonly string SceneName;
+
+ /// 当前 TalkSceneSO 的 asset 名称(来自 sections["macro"])。
+ public readonly string SceneSoName;
+
+ /// 当前 YarnProject 的 name(来自 sections["macro"],缺省回退到 anchor)。
+ public readonly string YarnProjectId;
+
+ public SaveSnapshotSummary(string sceneName, string sceneSoName, string yarnProjectId)
+ {
+ SceneName = sceneName;
+ SceneSoName = sceneSoName;
+ YarnProjectId = yarnProjectId;
+ }
+
+ /// 从快照派生宏观摘要;兼容 sections 为强类型 DTO 或读盘后的 JObject。
+ public static SaveSnapshotSummary From(SaveSnapshot snapshot)
+ {
+ if (snapshot?.sections == null)
+ {
+ return new SaveSnapshotSummary(null, null, snapshot?.anchor?.yarnProjectId);
+ }
+
+ string sceneName = null;
+ if (snapshot.sections.TryGetValue(SnapshotProviderIds.Scene, out var sceneRaw)
+ && SnapshotSectionDeserializer.Coerce(SnapshotProviderIds.Scene, sceneRaw) is SceneSnapshotDto scene)
+ {
+ sceneName = scene.sceneName;
+ }
+
+ string sceneSoName = null;
+ string yarnProjectId = null;
+ if (snapshot.sections.TryGetValue(SnapshotProviderIds.Macro, out var macroRaw)
+ && SnapshotSectionDeserializer.Coerce(SnapshotProviderIds.Macro, macroRaw) is MacroSnapshotDto macro)
+ {
+ sceneSoName = macro.sceneSoName;
+ yarnProjectId = macro.yarnProjectId;
+ }
+
+ if (string.IsNullOrEmpty(yarnProjectId))
+ {
+ yarnProjectId = snapshot.anchor?.yarnProjectId;
+ }
+
+ return new SaveSnapshotSummary(sceneName, sceneSoName, yarnProjectId);
+ }
+ }
+}
diff --git a/Assets/Scripts/SaveSystem/SaveSnapshotSummary.cs.meta b/Assets/Scripts/SaveSystem/SaveSnapshotSummary.cs.meta
new file mode 100644
index 000000000..1da3b3180
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/SaveSnapshotSummary.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5b1c9a2f7d3e4c8a9b6f0d1e2a3c4b5d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/SaveSystem/ScreenSnapshotHelper.cs b/Assets/Scripts/SaveSystem/ScreenSnapshotHelper.cs
new file mode 100644
index 000000000..75d0c44f7
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/ScreenSnapshotHelper.cs
@@ -0,0 +1,50 @@
+using AibisDream.UI;
+using UnityEngine;
+
+namespace AibisDream.SaveSystem
+{
+ ///
+ /// screen section 的捕获/还原实现:屏幕后处理饱和度 + PlayToolPanel 遮罩终态(黑屏盖住 / 画面可见)。
+ /// screen section:饱和度 + PlayToolPanel 遮罩终态()。
+ /// 供 委托,避免 Provider 直接依赖 UI 细节。
+ ///
+ public static class ScreenSnapshotHelper
+ {
+ public static ScreenSnapshotDto Capture()
+ {
+ var dto = new ScreenSnapshotDto();
+
+ if (ScreenEffectManager.Instance != null && ScreenEffectManager.Instance.saturation != null)
+ {
+ dto.saturationWeight = ScreenEffectManager.Instance.saturation.weight;
+ }
+
+ if (UIManager.Instance != null)
+ {
+ var panel = UIManager.Instance.GetPanel();
+ if (panel != null)
+ {
+ dto.isFadeScreenCovered = panel.CaptureFadeScreenCovered();
+ }
+ }
+
+ return dto;
+ }
+
+ public static void Restore(ScreenSnapshotDto dto)
+ {
+ if (dto == null) return;
+
+ if (ScreenEffectManager.Instance != null && ScreenEffectManager.Instance.saturation != null)
+ {
+ ScreenEffectManager.Instance.saturation.weight = dto.saturationWeight;
+ }
+
+ if (UIManager.Instance != null)
+ {
+ var panel = UIManager.Instance.GetPanel();
+ panel?.ApplyFadeScreenCovered(dto.isFadeScreenCovered);
+ }
+ }
+ }
+}
diff --git a/Assets/Scripts/SaveSystem/ScreenSnapshotHelper.cs.meta b/Assets/Scripts/SaveSystem/ScreenSnapshotHelper.cs.meta
new file mode 100644
index 000000000..a5d42bef9
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/ScreenSnapshotHelper.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c1dd311abda2a88428fbc0d2b4e21236
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/SaveSystem/SnapshotBootstrap.cs b/Assets/Scripts/SaveSystem/SnapshotBootstrap.cs
new file mode 100644
index 000000000..7b337e755
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/SnapshotBootstrap.cs
@@ -0,0 +1,26 @@
+namespace AibisDream.SaveSystem
+{
+ ///
+ /// 一次性注册 P1 所需的全部 。
+ /// 由 与 在首次使用时调用。
+ ///
+ public static class SnapshotBootstrap
+ {
+ private static bool _initialized;
+
+ /// 幂等;重复调用无副作用。
+ public static void EnsureInitialized()
+ {
+ if (_initialized) return;
+ _initialized = true;
+
+ SnapshotRegistry.Register(new SceneSnapshotProvider());
+ SnapshotRegistry.Register(new MacroSnapshotProvider());
+ SnapshotRegistry.Register(new EnvironmentSnapshotProvider());
+ SnapshotRegistry.Register(new ActorSnapshotProvider());
+ SnapshotRegistry.Register(new AudioSnapshotProvider());
+ SnapshotRegistry.Register(new TimelineSnapshotProvider());
+ SnapshotRegistry.Register(new ScreenSnapshotProvider());
+ }
+ }
+}
diff --git a/Assets/Scripts/SaveSystem/SnapshotBootstrap.cs.meta b/Assets/Scripts/SaveSystem/SnapshotBootstrap.cs.meta
new file mode 100644
index 000000000..f5858fb00
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/SnapshotBootstrap.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8deaa7a6856bb384fba9e42453a06e1d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/SaveSystem/SnapshotCapture.cs b/Assets/Scripts/SaveSystem/SnapshotCapture.cs
new file mode 100644
index 000000000..44246a85d
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/SnapshotCapture.cs
@@ -0,0 +1,67 @@
+using System;
+using UnityEngine;
+using Yarn.Unity;
+
+namespace AibisDream.SaveSystem
+{
+ ///
+ /// 从运行时组装 (纯内存,不写盘)。
+ ///
+ /// 流程:元数据 → 锚点 → Yarn 变量 → 各 Provider.Capture。
+ /// 宏观信息(scene/macro)由对应 Provider 写入 sections,不再额外汇总到顶层。
+ ///
+ ///
+ public static class SnapshotCapture
+ {
+ /// 捕获当前完整快照。调用前需已注册全部 provider。
+ public static SaveSnapshot Capture(YarnVariableStorage storage)
+ {
+ SnapshotRegistry.ValidateRequiredProviders();
+
+ var snapshot = new SaveSnapshot
+ {
+ gameVersion = Application.version,
+ savedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
+ };
+
+ CaptureAnchor(snapshot);
+ CaptureYarnVariables(storage, snapshot);
+
+ foreach (var provider in SnapshotRegistry.GetOrderedProviders())
+ {
+ var dto = provider.Capture();
+ if (dto == null) continue;
+ snapshot.sections[provider.SaveId] = dto;
+ }
+
+ return snapshot;
+ }
+
+ private static void CaptureAnchor(SaveSnapshot snapshot)
+ {
+ var dialog = DialogController.Instance;
+ if (dialog == null) return;
+
+ var (nodeName, _) = dialog.GetCurrentNodeContext();
+ var yarnProject = dialog.DialogueRunner?.YarnProject;
+ var projectId = yarnProject != null ? yarnProject.name : string.Empty;
+
+ snapshot.anchor = new AnchorSnapshot
+ {
+ yarnProjectId = projectId,
+ nodeName = nodeName ?? string.Empty
+ };
+ }
+
+ private static void CaptureYarnVariables(YarnVariableStorage storage, SaveSnapshot snapshot)
+ {
+ var (floats, strings, bools) = storage.GetAllVariables();
+ snapshot.yarnVariables = new YarnVariablesSnapshot
+ {
+ floats = floats ?? new(),
+ strings = strings ?? new(),
+ bools = bools ?? new()
+ };
+ }
+ }
+}
diff --git a/Assets/Scripts/SaveSystem/SnapshotCapture.cs.meta b/Assets/Scripts/SaveSystem/SnapshotCapture.cs.meta
new file mode 100644
index 000000000..63e2672db
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/SnapshotCapture.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8980db10670d2ec4685bc1564407cb9e
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/SaveSystem/SnapshotPersistence.cs b/Assets/Scripts/SaveSystem/SnapshotPersistence.cs
new file mode 100644
index 000000000..d1c9fea55
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/SnapshotPersistence.cs
@@ -0,0 +1,51 @@
+using System.IO;
+using AibisDream.Utility;
+using Newtonsoft.Json.Linq;
+
+namespace AibisDream.SaveSystem
+{
+ ///
+ /// 快照与磁盘的边界:读写 JSON 文件、检测旧档格式。
+ ///
+ /// P1 使用固定测试路径();
+ /// P2 将扩展为槽位目录、原子写、sidecar,接口可保持不变。
+ ///
+ ///
+ public static class SnapshotPersistence
+ {
+ /// P1 自测落盘:SnapshotTestPath/latest_snapshot.json。
+ public static string GetTestFilePath()
+ {
+ return Path.Combine(ConstRef.SnapshotTestPath, ConstRef.SnapshotTestFileName);
+ }
+
+ /// 将快照序列化并写入 path(自动补 .json 后缀)。
+ public static void Save(SaveSnapshot snapshot, string pathWithoutExtension)
+ {
+ SnapshotSerializer.SaveToFile(snapshot, pathWithoutExtension);
+ }
+
+ /// 从 path 读取并反序列化为 。
+ public static SaveSnapshot Load(string pathWithoutExtension)
+ {
+ return SnapshotSerializer.LoadFromFile(pathWithoutExtension);
+ }
+
+ ///
+ /// 是否为旧 StorageSystem 格式(含 systemData、无 schemaVersion)。
+ /// 此类文件应走 的 legacy 分支。
+ ///
+ public static bool IsLegacyFormat(string savePath)
+ {
+ var json = File.ReadAllText(JsonUtil.FormatAsJsonPath(savePath));
+ var jobj = JObject.Parse(json);
+ return jobj.Value("schemaVersion") == null && jobj["systemData"] != null;
+ }
+
+ /// 读取旧格式存档根 JObject,供 legacy 还原使用。
+ public static JObject ReadLegacySaveRoot(string savePath)
+ {
+ return JsonUtil.ReadJObject(savePath);
+ }
+ }
+}
diff --git a/Assets/Scripts/SaveSystem/SnapshotPersistence.cs.meta b/Assets/Scripts/SaveSystem/SnapshotPersistence.cs.meta
new file mode 100644
index 000000000..85905f285
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/SnapshotPersistence.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 4d7e9858d7009984ab1d2c139945e65e
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/SaveSystem/SnapshotProviderIds.cs b/Assets/Scripts/SaveSystem/SnapshotProviderIds.cs
new file mode 100644
index 000000000..3c902f458
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/SnapshotProviderIds.cs
@@ -0,0 +1,26 @@
+namespace AibisDream.SaveSystem
+{
+ ///
+ /// 快照 section 的稳定字符串 id。
+ /// 用于 JSON key、Provider 注册与反序列化,避免使用 AssemblyQualifiedName。
+ ///
+ public static class SnapshotProviderIds
+ {
+ public const string Scene = "scene";
+ public const string Macro = "macro";
+ public const string Environment = "env";
+ public const string Actor = "actor";
+ public const string Audio = "audio";
+ public const string Timeline = "timeline";
+ public const string Screen = "screen";
+
+ ///
+ /// P1 应参与 Capture 的 provider 清单。
+ /// 据此告警缺失项;不含深度维修。
+ ///
+ public static readonly string[] RequiredForCapture =
+ {
+ Scene, Macro, Environment, Actor, Audio, Timeline, Screen
+ };
+ }
+}
diff --git a/Assets/Scripts/SaveSystem/SnapshotProviderIds.cs.meta b/Assets/Scripts/SaveSystem/SnapshotProviderIds.cs.meta
new file mode 100644
index 000000000..041c1bf2a
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/SnapshotProviderIds.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 961a9f94f713b334388b4d1a0b1018c4
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/SaveSystem/SnapshotRegistry.cs b/Assets/Scripts/SaveSystem/SnapshotRegistry.cs
new file mode 100644
index 000000000..7f2d5fdfe
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/SnapshotRegistry.cs
@@ -0,0 +1,54 @@
+using System.Collections.Generic;
+using System.Linq;
+using UnityEngine;
+
+namespace AibisDream.SaveSystem
+{
+ ///
+ /// 运行时登记全部 。
+ /// Capture/Restore 时按 迭代;
+ /// 不使用「谁注册了谁才存」的隐式行为,RequiredForCapture 用于显式校验。
+ ///
+ public static class SnapshotRegistry
+ {
+ private static readonly Dictionary Providers = new();
+
+ /// 注册 provider;同 SaveId 后者覆盖前者。
+ public static void Register(ISnapshotProvider provider)
+ {
+ if (provider == null) return;
+ Providers[provider.SaveId] = provider;
+ }
+
+ public static void Unregister(string saveId)
+ {
+ Providers.Remove(saveId);
+ }
+
+ public static bool TryGet(string saveId, out ISnapshotProvider provider)
+ {
+ return Providers.TryGetValue(saveId, out provider);
+ }
+
+ /// 按 RestoreOrder 升序返回,供还原流水线使用。
+ public static IReadOnlyList GetOrderedProviders()
+ {
+ return Providers.Values.OrderBy(p => p.RestoreOrder).ToList();
+ }
+
+ ///
+ /// 校验 ;
+ /// 缺失时 LogWarning 但不阻断 Capture。
+ ///
+ public static void ValidateRequiredProviders()
+ {
+ foreach (var saveId in SnapshotProviderIds.RequiredForCapture)
+ {
+ if (!Providers.ContainsKey(saveId))
+ {
+ Debug.LogWarning($"[SnapshotRegistry] 缺少快照 provider: {saveId}");
+ }
+ }
+ }
+ }
+}
diff --git a/Assets/Scripts/SaveSystem/SnapshotRegistry.cs.meta b/Assets/Scripts/SaveSystem/SnapshotRegistry.cs.meta
new file mode 100644
index 000000000..e7ba300a4
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/SnapshotRegistry.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: be5181c78333ff348987644e3653b4d8
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/SaveSystem/SnapshotRestore.cs b/Assets/Scripts/SaveSystem/SnapshotRestore.cs
new file mode 100644
index 000000000..89ae1ddea
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/SnapshotRestore.cs
@@ -0,0 +1,85 @@
+using System.Collections;
+using UnityEngine;
+
+namespace AibisDream.SaveSystem
+{
+ ///
+ /// 将 逐项写回运行时(纯内存,不读盘)。
+ ///
+ /// 顺序:Yarn 变量 → 各 Provider(按 RestoreOrder)→(可选)锚点重进节点。
+ /// 完整淡入淡出时序由 P4 在 扩展。
+ ///
+ ///
+ public static class SnapshotRestore
+ {
+ /// 还原 Yarn 变量与各 section;不包含锚点重进(由 或上层编排)。
+ public static IEnumerator Restore(YarnVariableStorage storage, SaveSnapshot snapshot)
+ {
+ if (snapshot == null)
+ {
+ Debug.LogError("[SnapshotRestore] snapshot 为 null");
+ yield break;
+ }
+
+ RestoreYarnVariables(storage, snapshot);
+
+ foreach (var provider in SnapshotRegistry.GetOrderedProviders())
+ {
+ if (!snapshot.sections.TryGetValue(provider.SaveId, out var dto) || dto == null)
+ {
+ continue;
+ }
+
+ dto = SnapshotSectionDeserializer.Coerce(provider.SaveId, dto);
+ yield return provider.Restore(dto);
+ }
+
+ if (snapshot.deepRepair != null && snapshot.deepRepair.sections != null
+ && snapshot.deepRepair.sections.Count > 0)
+ {
+ Debug.LogWarning("[SnapshotRestore] DeepRepair 段尚未实现,已跳过(P5)。");
+ }
+ }
+
+ ///
+ /// 按 重新进入 Yarn 节点,实现「所见即所存」。
+ /// 调用前应已完成场景加载与各 provider 还原。
+ ///
+ public static IEnumerator RestoreAnchor(SaveSnapshot snapshot)
+ {
+ if (snapshot?.anchor == null || string.IsNullOrEmpty(snapshot.anchor.nodeName))
+ {
+ yield break;
+ }
+
+ var dialog = DialogController.Instance;
+ if (dialog == null) yield break;
+
+ var runner = dialog.DialogueRunner;
+ if (runner == null) yield break;
+
+ if (!string.IsNullOrEmpty(snapshot.anchor.yarnProjectId)
+ && runner.YarnProject != null
+ && runner.YarnProject.name != snapshot.anchor.yarnProjectId)
+ {
+ Debug.LogWarning(
+ $"[SnapshotRestore] YarnProject 不匹配:当前 {runner.YarnProject.name},存档 {snapshot.anchor.yarnProjectId}");
+ }
+
+ if (runner.IsDialogueRunning)
+ {
+ yield return runner.Stop();
+ }
+
+ yield return runner.StartDialogue(snapshot.anchor.nodeName);
+ }
+
+ private static void RestoreYarnVariables(YarnVariableStorage storage, SaveSnapshot snapshot)
+ {
+ var vars = snapshot.yarnVariables;
+ if (vars == null) return;
+
+ storage.SetAllVariables(vars.floats, vars.strings, vars.bools);
+ }
+ }
+}
diff --git a/Assets/Scripts/SaveSystem/SnapshotRestore.cs.meta b/Assets/Scripts/SaveSystem/SnapshotRestore.cs.meta
new file mode 100644
index 000000000..9d49a8397
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/SnapshotRestore.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 3bcdb9cb4405940409b03b54446a2c5e
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/SaveSystem/SnapshotSectionDeserializer.cs b/Assets/Scripts/SaveSystem/SnapshotSectionDeserializer.cs
new file mode 100644
index 000000000..f8f2b76fc
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/SnapshotSectionDeserializer.cs
@@ -0,0 +1,32 @@
+using Newtonsoft.Json.Linq;
+
+namespace AibisDream.SaveSystem
+{
+ ///
+ /// 读盘后 中的值常为 ;
+ /// 本类按 SaveId 转回各 Provider 所需的强类型 DTO。
+ ///
+ public static class SnapshotSectionDeserializer
+ {
+ /// 若 dto 已是强类型则原样返回;若为 JObject 则 ToObject 到对应 DTO。
+ public static object Coerce(string saveId, object dto)
+ {
+ if (dto is not JObject jobj)
+ {
+ return dto;
+ }
+
+ return saveId switch
+ {
+ SnapshotProviderIds.Scene => jobj.ToObject(),
+ SnapshotProviderIds.Macro => jobj.ToObject(),
+ SnapshotProviderIds.Environment => jobj.ToObject(),
+ SnapshotProviderIds.Actor => jobj.ToObject(),
+ SnapshotProviderIds.Audio => jobj.ToObject(),
+ SnapshotProviderIds.Timeline => jobj.ToObject(),
+ SnapshotProviderIds.Screen => jobj.ToObject(),
+ _ => jobj
+ };
+ }
+ }
+}
diff --git a/Assets/Scripts/SaveSystem/SnapshotSectionDeserializer.cs.meta b/Assets/Scripts/SaveSystem/SnapshotSectionDeserializer.cs.meta
new file mode 100644
index 000000000..a62a9a546
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/SnapshotSectionDeserializer.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: d7def29588434ab48b6f6768daf76635
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/SaveSystem/SnapshotSerializer.cs b/Assets/Scripts/SaveSystem/SnapshotSerializer.cs
new file mode 100644
index 000000000..dc3ffdc00
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/SnapshotSerializer.cs
@@ -0,0 +1,115 @@
+using System;
+
+using AibisDream.Utility;
+
+using Newtonsoft.Json;
+
+using UnityEngine;
+
+
+
+namespace AibisDream.SaveSystem
+
+{
+
+ ///
+
+ /// 与 JSON 互转;写入时带上 。
+
+ /// 被 调用,业务层通常不直接使用。
+
+ ///
+
+ public static class SnapshotSerializer
+
+ {
+
+ private static readonly JsonSerializerSettings Settings = new()
+
+ {
+
+ TypeNameHandling = TypeNameHandling.None,
+
+ ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
+
+ NullValueHandling = NullValueHandling.Ignore
+
+ };
+
+
+
+ public static string Serialize(SaveSnapshot snapshot)
+
+ {
+
+ snapshot.schemaVersion = SaveSnapshotSchema.CurrentVersion;
+
+ return JsonConvert.SerializeObject(snapshot, Formatting.Indented, Settings);
+
+ }
+
+
+
+ public static SaveSnapshot Deserialize(string json)
+
+ {
+
+ var snapshot = JsonConvert.DeserializeObject(json, Settings);
+
+ if (snapshot.schemaVersion != SaveSnapshotSchema.CurrentVersion)
+
+ {
+
+ Debug.LogWarning(
+
+ $"[SnapshotSerializer] schemaVersion {snapshot.schemaVersion} != {SaveSnapshotSchema.CurrentVersion},按当前结构读取。");
+
+ }
+
+
+
+ return snapshot;
+
+ }
+
+
+
+ public static void SaveToFile(SaveSnapshot snapshot, string pathWithoutExtension)
+
+ {
+
+ var json = Serialize(snapshot);
+
+ var dir = System.IO.Path.GetDirectoryName(JsonUtil.FormatAsJsonPath(pathWithoutExtension));
+
+ if (!string.IsNullOrEmpty(dir))
+
+ {
+
+ System.IO.Directory.CreateDirectory(dir);
+
+ }
+
+
+
+ System.IO.File.WriteAllText(JsonUtil.FormatAsJsonPath(pathWithoutExtension), json);
+
+ }
+
+
+
+ public static SaveSnapshot LoadFromFile(string pathWithoutExtension)
+
+ {
+
+ var json = System.IO.File.ReadAllText(JsonUtil.FormatAsJsonPath(pathWithoutExtension));
+
+ return Deserialize(json);
+
+ }
+
+ }
+
+}
+
+
diff --git a/Assets/Scripts/SaveSystem/SnapshotSerializer.cs.meta b/Assets/Scripts/SaveSystem/SnapshotSerializer.cs.meta
new file mode 100644
index 000000000..12a8127fb
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/SnapshotSerializer.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f57d93779e83c064f9ba2490e623e5b4
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/SaveSystem/SnapshotService.cs b/Assets/Scripts/SaveSystem/SnapshotService.cs
new file mode 100644
index 000000000..7e6d6b4b1
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/SnapshotService.cs
@@ -0,0 +1,38 @@
+using System.Collections;
+
+namespace AibisDream.SaveSystem
+{
+ ///
+ /// 快照层对外 API:Capture / Restore,不涉及文件与 UI。
+ /// 游戏逻辑应优先使用 完成存读档流程。
+ ///
+ public static class SnapshotService
+ {
+ /// 捕获当前运行时快照。
+ public static SaveSnapshot Capture()
+ {
+ SnapshotBootstrap.EnsureInitialized();
+ return SnapshotCapture.Capture(YarnVariableStorage.Instance);
+ }
+
+ ///
+ /// 还原快照。默认在 provider 还原后继续执行 。
+ ///
+ /// 为 false 时仅还原状态,不重进 Yarn 节点(供 P4 编排使用)。
+ public static IEnumerator Restore(SaveSnapshot snapshot, bool restoreAnchor = true)
+ {
+ if (snapshot == null)
+ {
+ yield break;
+ }
+
+ SnapshotBootstrap.EnsureInitialized();
+ yield return SnapshotRestore.Restore(YarnVariableStorage.Instance, snapshot);
+
+ if (restoreAnchor)
+ {
+ yield return SnapshotRestore.RestoreAnchor(snapshot);
+ }
+ }
+ }
+}
diff --git a/Assets/Scripts/SaveSystem/SnapshotService.cs.meta b/Assets/Scripts/SaveSystem/SnapshotService.cs.meta
new file mode 100644
index 000000000..efd8b259a
--- /dev/null
+++ b/Assets/Scripts/SaveSystem/SnapshotService.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 2326ffbf13edbdb4fb0a438e7724edaa
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant: