feat(save): 添加快照存档系统核心框架
This commit is contained in:
@@ -23,6 +23,12 @@ namespace AibisDream.Utility
|
||||
|
||||
public static readonly string SaveFilePath = Path.Combine(Application.persistentDataPath, "AllOurBrokenParts", "saves");
|
||||
|
||||
/// <summary>P1 快照层自测落盘路径(P2 槽位层接管后迁移)。</summary>
|
||||
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");
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Collections;
|
||||
using AibisDream.Framework;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace AibisDream.SaveSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// 深度维修子系统的旧 IData 注册表(基于 <see cref="DataContainer"/>)。
|
||||
/// <para>
|
||||
/// FixSystem / Eye / Chip 等仍通过此处 RegisterData;尚未纳入新 <see cref="SaveSnapshot.deepRepair"/>。
|
||||
/// P5 评估后迁移或废弃;与 <see cref="YarnVariableStorage"/>、快照 I/O 分离。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class DeepRepairDataRegistry
|
||||
{
|
||||
private static readonly DataContainer Container = new();
|
||||
|
||||
public static void RegisterData<T>(T data) where T : IData
|
||||
{
|
||||
Container.Register(data);
|
||||
}
|
||||
|
||||
public static void UnregisterData<T>() where T : class, IData
|
||||
{
|
||||
Container.Unregister<T>();
|
||||
}
|
||||
|
||||
/// <summary>旧格式存档 systemData 段还原;按 LoadIndex 顺序执行各 IData.Load()。</summary>
|
||||
public static IEnumerator LoadByJson(JObject dataJson)
|
||||
{
|
||||
if (dataJson == null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return Container.LoadByJson(dataJson);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba4cbc590c8177045b16b8a02db31121
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace AibisDream.SaveSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// 深度维修是否纳入新快照、以何种粒度纳入(P5 逐个定案)。
|
||||
/// </summary>
|
||||
public enum DeepRepairSavePolicy
|
||||
{
|
||||
/// <summary>完整阶段终点状态写入 deepRepair.sections。</summary>
|
||||
Supported,
|
||||
|
||||
/// <summary>只存简化后的关键字段。</summary>
|
||||
Simplified,
|
||||
|
||||
/// <summary>不存;读档回到进入该维修前的可存点。</summary>
|
||||
Dropped
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="SaveSnapshot.deepRepair"/> 占位段。
|
||||
/// P1 捕获时不写入;读档时缺省或 sections 为空则不报错。
|
||||
/// 正式实现后替代/收敛 <see cref="DeepRepairDataRegistry"/> 的旧 IData 路径。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class DeepRepairSnapshot
|
||||
{
|
||||
public DeepRepairSavePolicy policy = DeepRepairSavePolicy.Dropped;
|
||||
|
||||
/// <summary>按维修模块 id 分子段,具体 key 在 P5 定义。</summary>
|
||||
public Dictionary<string, JObject> sections = new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6ee71c8af7b93d64aa87335a011eccf5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Collections;
|
||||
|
||||
namespace AibisDream.SaveSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// 快照提供者契约:将某一子系统的运行时状态与纯数据 DTO 互相转换。
|
||||
/// <para>
|
||||
/// 设计约定:DTO 不含事件、协程或 Unity 引用;<see cref="Capture"/> 只读,
|
||||
/// <see cref="Restore"/> 负责逐项写回。实现类通常很薄,实际逻辑在各 Manager 中。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public interface ISnapshotProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// 稳定显式 id,用作 <see cref="SaveSnapshot.sections"/> 的 key。
|
||||
/// 见 <see cref="SnapshotProviderIds"/>,不使用 C# 类型全名。
|
||||
/// </summary>
|
||||
string SaveId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 读档还原顺序。数值越小越先执行(如 scene=10 先于 actor=40)。
|
||||
/// </summary>
|
||||
int RestoreOrder { get; }
|
||||
|
||||
/// <summary>从当前运行时捕获该子系统的快照 DTO;无可存内容时可返回 null。</summary>
|
||||
object Capture();
|
||||
|
||||
/// <summary>将 DTO 逐项还原到运行时;可为协程以等待异步加载。</summary>
|
||||
IEnumerator Restore(object dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6a7b39d650ea48340bba1daa7387d119
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ce5fddeb465208c4ea3dbd9eacaa8d7b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Collections;
|
||||
|
||||
namespace AibisDream.SaveSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// sections["actor"]:场景中各角色显隐、槽位、动画状态、透明度等(ActorManager)。
|
||||
/// RestoreOrder=40,依赖 scene 与 env 已就绪。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0e062fe96fad9b545bc01559345476cb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.Collections;
|
||||
using AibisDream.Kit;
|
||||
|
||||
namespace AibisDream.SaveSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// sections["audio"]:FMOD 音乐/SFX 实例路径与 AMB 状态终态(AudioManager)。
|
||||
/// 不记录播放进度,读档后按路径重新 CreateInstance 并 start。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2158be13aca7fb74985d11effbf0304d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Collections;
|
||||
|
||||
namespace AibisDream.SaveSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// sections["env"]:场景内时间/天气(EnvironmentManager)。
|
||||
/// RestoreOrder=20,在 scene 加载完成后、macro 之前还原环境。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a879f9bc4b096c346be7af81876c792c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,56 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.SaveSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// sections["macro"]:章节 TalkSceneSO 与 YarnProject 绑定。
|
||||
/// RestoreOrder=30,在场景与环境就绪后设置 GameManager 并 LoadDialog(不 Start 节点,锚点由 RestoreAnchor 负责)。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af663fafb5b2a9748902e2e34e1f6be3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Collections;
|
||||
|
||||
namespace AibisDream.SaveSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// sections["scene"]:当前 Addressable 场景。
|
||||
/// RestoreOrder=10,读档时最先加载场景,供后续 env/actor 等依赖。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d814b6c56374b4446baa86e09f510098
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.Collections;
|
||||
|
||||
namespace AibisDream.SaveSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// sections["screen"]:屏幕饱和度与演出淡入淡出遮罩。
|
||||
/// 委托 <see cref="ScreenSnapshotHelper"/>;RestoreOrder=70,通常在表现类最后还原。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 07b9ceaefa3550f4583465dd32e420f2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.Collections;
|
||||
|
||||
namespace AibisDream.SaveSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// sections["timeline"]:各 PlayableDirector 的终/初态(TimelineCenter)。
|
||||
/// 按 D1.1 只存 AtStart/AtEnd/Stopped,不存时间轴中间帧。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2630a928f06aef14bb4a8520c743f3b5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
// 存盘(<<auto_save>>、调试)
|
||||
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 协程链 |
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 存读档流程编排层:连接 UI、<see cref="SnapshotService"/> 与 <see cref="SnapshotPersistence"/>。
|
||||
/// <para>
|
||||
/// 对外入口:<see cref="SaveToTestPath"/>(<<auto_save>>)、<see cref="RestoreFromFile"/>(读档)。
|
||||
/// P4 在此扩展淡入淡出与完整时序;P2 将替换测试路径为槽位 API。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class SaveRestoreOrchestrator
|
||||
{
|
||||
/// <summary>捕获快照并写入 P1 测试路径;含保存 Loading UI。</summary>
|
||||
public static void SaveToTestPath()
|
||||
{
|
||||
var host = YarnVariableStorage.Instance;
|
||||
if (host == null)
|
||||
{
|
||||
Debug.LogError("[SaveRestoreOrchestrator] YarnVariableStorage 未初始化");
|
||||
return;
|
||||
}
|
||||
|
||||
ActionKit.Sequence()
|
||||
.Callback(() => UIManager.Instance.GetPanel<InfoPanel>().ShowSaveLoading())
|
||||
.Delay(1f)
|
||||
.Callback(() => UIManager.Instance.GetPanel<InfoPanel>().HideSaveLoading())
|
||||
.Start(host);
|
||||
|
||||
using (new CodeTimer("SaveSnapshot"))
|
||||
{
|
||||
var snapshot = SnapshotService.Capture();
|
||||
SnapshotPersistence.Save(snapshot, SnapshotPersistence.GetTestFilePath());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>从文件读档并还原;自动区分新快照格式与 legacy 格式。</summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>旧档:Yarn 变量 + DeepRepairDataRegistry systemData。</summary>
|
||||
private static IEnumerator RestoreLegacy(string savePath)
|
||||
{
|
||||
var saveData = SnapshotPersistence.ReadLegacySaveRoot(savePath);
|
||||
var floatDict = saveData["floatDict"]?.ToObject<Dictionary<string, float>>();
|
||||
var stringDict = saveData["stringDict"]?.ToObject<Dictionary<string, string>>();
|
||||
var boolDict = saveData["boolDict"]?.ToObject<Dictionary<string, bool>>();
|
||||
YarnVariableStorage.Instance.SetAllVariables(floatDict, stringDict, boolDict);
|
||||
|
||||
if (saveData["systemData"] is JObject systemDataJson)
|
||||
{
|
||||
yield return DeepRepairDataRegistry.LoadByJson(systemDataJson);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 359343f1db2912f49bdcad4fbe443f6a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,181 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AibisDream.SaveSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前快照 schema 的版本号。结构变更时递增。
|
||||
/// </summary>
|
||||
public static class SaveSnapshotSchema
|
||||
{
|
||||
public const int CurrentVersion = 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 存档快照根对象:描述「进入可存节点那一刻」的完整游戏状态。
|
||||
/// <para>
|
||||
/// 与槽位/文件路径解耦;序列化后写入 JSON。对应需求§3 四分类:
|
||||
/// anchor(恢复锚点)、sections(宏观 scene/macro + 表现类)、yarnVariables、deepRepair(深度维修,P5)。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 宏观阶段(场景 / 章节 SO / YarnProject)不单独冗余存储,统一以 sections["scene"] / sections["macro"]
|
||||
/// 为唯一来源;槽位 UI 等需要摘要时通过 <see cref="SaveSnapshotSummary"/> 现算派生,避免重复字段漂移。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class SaveSnapshot
|
||||
{
|
||||
/// <summary>快照结构版本,对应 <see cref="SaveSnapshotSchema.CurrentVersion"/>。</summary>
|
||||
public int schemaVersion = SaveSnapshotSchema.CurrentVersion;
|
||||
|
||||
/// <summary>保存时的 <c>Application.version</c>。</summary>
|
||||
public string gameVersion;
|
||||
|
||||
/// <summary>保存时间(本地),格式 <c>yyyy-MM-dd HH:mm:ss</c>。</summary>
|
||||
public string savedAt;
|
||||
|
||||
/// <summary>恢复锚点:读档后从此 Yarn 节点重新进入(所见即所存)。</summary>
|
||||
public AnchorSnapshot anchor = new();
|
||||
|
||||
/// <summary>全部 Yarn 运行时变量(来自 <see cref="YarnVariableStorage"/>)。</summary>
|
||||
public YarnVariablesSnapshot yarnVariables = new();
|
||||
|
||||
/// <summary>
|
||||
/// 各子系统表现类快照,key 为 <see cref="SnapshotProviderIds"/> 中的稳定 id。
|
||||
/// </summary>
|
||||
public Dictionary<string, object> sections = new();
|
||||
|
||||
/// <summary>
|
||||
/// 深度维修段。P1 捕获时不写入(null);P5 按模块策略填充。
|
||||
/// </summary>
|
||||
public DeepRepairSnapshot deepRepair;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 恢复锚点。读档时在场景与状态还原完成后,从 <see cref="nodeName"/> 重新 <c>StartDialogue</c>。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class AnchorSnapshot
|
||||
{
|
||||
public string yarnProjectId;
|
||||
public string nodeName;
|
||||
}
|
||||
|
||||
/// <summary>Yarn 变量三分组,与 Yarn Spinner 支持的类型一致。</summary>
|
||||
[Serializable]
|
||||
public class YarnVariablesSnapshot
|
||||
{
|
||||
public Dictionary<string, float> floats = new();
|
||||
public Dictionary<string, string> strings = new();
|
||||
public Dictionary<string, bool> bools = new();
|
||||
}
|
||||
|
||||
#region Provider DTOs
|
||||
|
||||
/// <summary>sections["scene"]:当前加载的场景。</summary>
|
||||
[Serializable]
|
||||
public class SceneSnapshotDto
|
||||
{
|
||||
public string sceneName;
|
||||
}
|
||||
|
||||
/// <summary>sections["macro"]:章节与 Yarn 工程绑定。</summary>
|
||||
[Serializable]
|
||||
public class MacroSnapshotDto
|
||||
{
|
||||
public string sceneSoName;
|
||||
public string yarnProjectId;
|
||||
}
|
||||
|
||||
/// <summary>sections["env"]:环境时间/天气。</summary>
|
||||
[Serializable]
|
||||
public class EnvironmentSnapshotDto
|
||||
{
|
||||
/// <summary><see cref="TimesOfDay"/> 枚举名。</summary>
|
||||
public string curTime;
|
||||
|
||||
/// <summary><see cref="Weather"/> 枚举名。</summary>
|
||||
public string curWeather;
|
||||
|
||||
public bool isInitialized;
|
||||
}
|
||||
|
||||
/// <summary>sections["actor"]:场景中所有角色的逐项状态。</summary>
|
||||
[Serializable]
|
||||
public class ActorSnapshotDto
|
||||
{
|
||||
public List<ActorEntrySnapshotDto> actors = new();
|
||||
}
|
||||
|
||||
/// <summary>单个角色的快照条目。</summary>
|
||||
[Serializable]
|
||||
public class ActorEntrySnapshotDto
|
||||
{
|
||||
public string actorName;
|
||||
public string slotName;
|
||||
public string actorType;
|
||||
public string stateName;
|
||||
public float alpha;
|
||||
public float faceParam;
|
||||
}
|
||||
|
||||
/// <summary>sections["audio"]:FMOD 音乐/SFX 终态与 AMB 状态(不记播放进度)。</summary>
|
||||
[Serializable]
|
||||
public class AudioSnapshotDto
|
||||
{
|
||||
/// <summary>key → FMOD event path。</summary>
|
||||
public Dictionary<string, string> musicPaths = new();
|
||||
|
||||
public Dictionary<string, string> sfxPaths = new();
|
||||
|
||||
/// <summary><see cref="AmbState"/> 枚举名。</summary>
|
||||
public string ambState;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Timeline 终/初态标记(决策 D1.1:不按时间轴逐帧还原)。
|
||||
/// 序列化进 JSON 时使用字符串形式存入 <see cref="TimelineEntrySnapshotDto.phase"/>。
|
||||
/// </summary>
|
||||
public enum TimelinePlaybackPhase
|
||||
{
|
||||
AtStart,
|
||||
AtEnd,
|
||||
Stopped
|
||||
}
|
||||
|
||||
/// <summary>sections["timeline"]:各 PlayableDirector 的终/初态。</summary>
|
||||
[Serializable]
|
||||
public class TimelineSnapshotDto
|
||||
{
|
||||
public List<TimelineEntrySnapshotDto> entries = new();
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class TimelineEntrySnapshotDto
|
||||
{
|
||||
public string directorName;
|
||||
public bool isActive;
|
||||
|
||||
/// <summary><see cref="TimelinePlaybackPhase"/> 的字符串名。</summary>
|
||||
public string phase;
|
||||
|
||||
/// <summary>若由 Addressables 加载 Timeline,记录 key;否则为空。</summary>
|
||||
public string loadedAddressableKey;
|
||||
}
|
||||
|
||||
/// <summary>sections["screen"]:屏幕饱和度与演出淡入淡出遮罩。</summary>
|
||||
[Serializable]
|
||||
public class ScreenSnapshotDto
|
||||
{
|
||||
/// <summary>Post-process Volume weight(0=正常,1=完全褪色)。</summary>
|
||||
public float saturationWeight;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="PlayToolPanel"/> 演出遮罩终态:<c>true</c> = 黑屏盖住(<c>FadeIn</c> 完成);
|
||||
/// <c>false</c> = 画面可见(<c>FadeOut</c> 完成或遮罩未启用)。不记录转场中间 alpha。
|
||||
/// </summary>
|
||||
public bool isFadeScreenCovered;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 067e34fac1df0c1409cabbab3af2084b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,60 @@
|
||||
namespace AibisDream.SaveSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// 从 <see cref="SaveSnapshot.sections"/> 现算的宏观摘要(场景 / 章节 SO / YarnProject)。
|
||||
/// <para>
|
||||
/// 替代已移除的顶层 <c>macroStage</c> 冗余字段:sections["scene"] / sections["macro"] 为唯一来源,
|
||||
/// 槽位 / 「继续游戏」等 UI 在需要展示时调用 <see cref="From"/> 派生,避免重复字段与读写漂移。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public readonly struct SaveSnapshotSummary
|
||||
{
|
||||
/// <summary>Addressable 场景 key,如 <c>Scene/ClinicOut</c>(来自 sections["scene"])。</summary>
|
||||
public readonly string SceneName;
|
||||
|
||||
/// <summary>当前 <c>TalkSceneSO</c> 的 asset 名称(来自 sections["macro"])。</summary>
|
||||
public readonly string SceneSoName;
|
||||
|
||||
/// <summary>当前 <c>YarnProject</c> 的 name(来自 sections["macro"],缺省回退到 anchor)。</summary>
|
||||
public readonly string YarnProjectId;
|
||||
|
||||
public SaveSnapshotSummary(string sceneName, string sceneSoName, string yarnProjectId)
|
||||
{
|
||||
SceneName = sceneName;
|
||||
SceneSoName = sceneSoName;
|
||||
YarnProjectId = yarnProjectId;
|
||||
}
|
||||
|
||||
/// <summary>从快照派生宏观摘要;兼容 sections 为强类型 DTO 或读盘后的 JObject。</summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5b1c9a2f7d3e4c8a9b6f0d1e2a3c4b5d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,50 @@
|
||||
using AibisDream.UI;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.SaveSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// screen section 的捕获/还原实现:屏幕后处理饱和度 + PlayToolPanel 遮罩终态(黑屏盖住 / 画面可见)。
|
||||
/// screen section:饱和度 + PlayToolPanel 遮罩终态(<see cref="PlayToolPanel.CaptureFadeScreenCovered"/>)。
|
||||
/// 供 <see cref="ScreenSnapshotProvider"/> 委托,避免 Provider 直接依赖 UI 细节。
|
||||
/// </summary>
|
||||
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<PlayToolPanel>();
|
||||
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<PlayToolPanel>();
|
||||
panel?.ApplyFadeScreenCovered(dto.isFadeScreenCovered);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c1dd311abda2a88428fbc0d2b4e21236
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace AibisDream.SaveSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// 一次性注册 P1 所需的全部 <see cref="ISnapshotProvider"/>。
|
||||
/// 由 <see cref="YarnVariableStorage"/> 与 <see cref="SnapshotService"/> 在首次使用时调用。
|
||||
/// </summary>
|
||||
public static class SnapshotBootstrap
|
||||
{
|
||||
private static bool _initialized;
|
||||
|
||||
/// <summary>幂等;重复调用无副作用。</summary>
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8deaa7a6856bb384fba9e42453a06e1d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using Yarn.Unity;
|
||||
|
||||
namespace AibisDream.SaveSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// 从运行时组装 <see cref="SaveSnapshot"/>(纯内存,不写盘)。
|
||||
/// <para>
|
||||
/// 流程:元数据 → 锚点 → Yarn 变量 → 各 Provider.Capture。
|
||||
/// 宏观信息(scene/macro)由对应 Provider 写入 sections,不再额外汇总到顶层。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class SnapshotCapture
|
||||
{
|
||||
/// <summary>捕获当前完整快照。调用前需已注册全部 provider。</summary>
|
||||
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()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8980db10670d2ec4685bc1564407cb9e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.IO;
|
||||
using AibisDream.Utility;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace AibisDream.SaveSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// 快照与磁盘的边界:读写 JSON 文件、检测旧档格式。
|
||||
/// <para>
|
||||
/// P1 使用固定测试路径(<see cref="GetTestFilePath"/>);
|
||||
/// P2 将扩展为槽位目录、原子写、sidecar,接口可保持不变。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class SnapshotPersistence
|
||||
{
|
||||
/// <summary>P1 自测落盘:<c>SnapshotTestPath/latest_snapshot.json</c>。</summary>
|
||||
public static string GetTestFilePath()
|
||||
{
|
||||
return Path.Combine(ConstRef.SnapshotTestPath, ConstRef.SnapshotTestFileName);
|
||||
}
|
||||
|
||||
/// <summary>将快照序列化并写入 path(自动补 .json 后缀)。</summary>
|
||||
public static void Save(SaveSnapshot snapshot, string pathWithoutExtension)
|
||||
{
|
||||
SnapshotSerializer.SaveToFile(snapshot, pathWithoutExtension);
|
||||
}
|
||||
|
||||
/// <summary>从 path 读取并反序列化为 <see cref="SaveSnapshot"/>。</summary>
|
||||
public static SaveSnapshot Load(string pathWithoutExtension)
|
||||
{
|
||||
return SnapshotSerializer.LoadFromFile(pathWithoutExtension);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否为旧 StorageSystem 格式(含 systemData、无 schemaVersion)。
|
||||
/// 此类文件应走 <see cref="SaveRestoreOrchestrator"/> 的 legacy 分支。
|
||||
/// </summary>
|
||||
public static bool IsLegacyFormat(string savePath)
|
||||
{
|
||||
var json = File.ReadAllText(JsonUtil.FormatAsJsonPath(savePath));
|
||||
var jobj = JObject.Parse(json);
|
||||
return jobj.Value<int?>("schemaVersion") == null && jobj["systemData"] != null;
|
||||
}
|
||||
|
||||
/// <summary>读取旧格式存档根 JObject,供 legacy 还原使用。</summary>
|
||||
public static JObject ReadLegacySaveRoot(string savePath)
|
||||
{
|
||||
return JsonUtil.ReadJObject(savePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4d7e9858d7009984ab1d2c139945e65e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace AibisDream.SaveSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// 快照 section 的稳定字符串 id。
|
||||
/// 用于 JSON key、Provider 注册与反序列化,避免使用 AssemblyQualifiedName。
|
||||
/// </summary>
|
||||
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";
|
||||
|
||||
/// <summary>
|
||||
/// P1 应参与 Capture 的 provider 清单。
|
||||
/// <see cref="SnapshotRegistry.ValidateRequiredProviders"/> 据此告警缺失项;不含深度维修。
|
||||
/// </summary>
|
||||
public static readonly string[] RequiredForCapture =
|
||||
{
|
||||
Scene, Macro, Environment, Actor, Audio, Timeline, Screen
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 961a9f94f713b334388b4d1a0b1018c4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,54 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.SaveSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// 运行时登记全部 <see cref="ISnapshotProvider"/>。
|
||||
/// Capture/Restore 时按 <see cref="ISnapshotProvider.RestoreOrder"/> 迭代;
|
||||
/// 不使用「谁注册了谁才存」的隐式行为,RequiredForCapture 用于显式校验。
|
||||
/// </summary>
|
||||
public static class SnapshotRegistry
|
||||
{
|
||||
private static readonly Dictionary<string, ISnapshotProvider> Providers = new();
|
||||
|
||||
/// <summary>注册 provider;同 SaveId 后者覆盖前者。</summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>按 RestoreOrder 升序返回,供还原流水线使用。</summary>
|
||||
public static IReadOnlyList<ISnapshotProvider> GetOrderedProviders()
|
||||
{
|
||||
return Providers.Values.OrderBy(p => p.RestoreOrder).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验 <see cref="SnapshotProviderIds.RequiredForCapture"/>;
|
||||
/// 缺失时 <c>LogWarning</c> 但不阻断 Capture。
|
||||
/// </summary>
|
||||
public static void ValidateRequiredProviders()
|
||||
{
|
||||
foreach (var saveId in SnapshotProviderIds.RequiredForCapture)
|
||||
{
|
||||
if (!Providers.ContainsKey(saveId))
|
||||
{
|
||||
Debug.LogWarning($"[SnapshotRegistry] 缺少快照 provider: {saveId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: be5181c78333ff348987644e3653b4d8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,85 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.SaveSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// 将 <see cref="SaveSnapshot"/> 逐项写回运行时(纯内存,不读盘)。
|
||||
/// <para>
|
||||
/// 顺序:Yarn 变量 → 各 Provider(按 RestoreOrder)→(可选)锚点重进节点。
|
||||
/// 完整淡入淡出时序由 P4 在 <see cref="SaveRestoreOrchestrator"/> 扩展。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class SnapshotRestore
|
||||
{
|
||||
/// <summary>还原 Yarn 变量与各 section;不包含锚点重进(由 <see cref="RestoreAnchor"/> 或上层编排)。</summary>
|
||||
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)。");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按 <see cref="AnchorSnapshot"/> 重新进入 Yarn 节点,实现「所见即所存」。
|
||||
/// 调用前应已完成场景加载与各 provider 还原。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3bcdb9cb4405940409b03b54446a2c5e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,32 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace AibisDream.SaveSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// 读盘后 <see cref="SaveSnapshot.sections"/> 中的值常为 <see cref="JObject"/>;
|
||||
/// 本类按 SaveId 转回各 Provider 所需的强类型 DTO。
|
||||
/// </summary>
|
||||
public static class SnapshotSectionDeserializer
|
||||
{
|
||||
/// <summary>若 dto 已是强类型则原样返回;若为 JObject 则 ToObject 到对应 DTO。</summary>
|
||||
public static object Coerce(string saveId, object dto)
|
||||
{
|
||||
if (dto is not JObject jobj)
|
||||
{
|
||||
return dto;
|
||||
}
|
||||
|
||||
return saveId switch
|
||||
{
|
||||
SnapshotProviderIds.Scene => jobj.ToObject<SceneSnapshotDto>(),
|
||||
SnapshotProviderIds.Macro => jobj.ToObject<MacroSnapshotDto>(),
|
||||
SnapshotProviderIds.Environment => jobj.ToObject<EnvironmentSnapshotDto>(),
|
||||
SnapshotProviderIds.Actor => jobj.ToObject<ActorSnapshotDto>(),
|
||||
SnapshotProviderIds.Audio => jobj.ToObject<AudioSnapshotDto>(),
|
||||
SnapshotProviderIds.Timeline => jobj.ToObject<TimelineSnapshotDto>(),
|
||||
SnapshotProviderIds.Screen => jobj.ToObject<ScreenSnapshotDto>(),
|
||||
_ => jobj
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d7def29588434ab48b6f6768daf76635
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
|
||||
using AibisDream.Utility;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
|
||||
|
||||
namespace AibisDream.SaveSystem
|
||||
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
|
||||
/// <see cref="SaveSnapshot"/> 与 JSON 互转;写入时带上 <see cref="SaveSnapshotSchema.CurrentVersion"/>。
|
||||
|
||||
/// 被 <see cref="SnapshotPersistence"/> 调用,业务层通常不直接使用。
|
||||
|
||||
/// </summary>
|
||||
|
||||
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<SaveSnapshot>(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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f57d93779e83c064f9ba2490e623e5b4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Collections;
|
||||
|
||||
namespace AibisDream.SaveSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// 快照层对外 API:Capture / Restore,不涉及文件与 UI。
|
||||
/// 游戏逻辑应优先使用 <see cref="SaveRestoreOrchestrator"/> 完成存读档流程。
|
||||
/// </summary>
|
||||
public static class SnapshotService
|
||||
{
|
||||
/// <summary>捕获当前运行时快照。</summary>
|
||||
public static SaveSnapshot Capture()
|
||||
{
|
||||
SnapshotBootstrap.EnsureInitialized();
|
||||
return SnapshotCapture.Capture(YarnVariableStorage.Instance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 还原快照。默认在 provider 还原后继续执行 <see cref="SnapshotRestore.RestoreAnchor"/>。
|
||||
/// </summary>
|
||||
/// <param name="restoreAnchor">为 false 时仅还原状态,不重进 Yarn 节点(供 P4 编排使用)。</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2326ffbf13edbdb4fb0a438e7724edaa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user