refactor(save): 合并 SaveSystem 过度拆分的文件

将以下小文件合并到相关核心类中,减少文件数量并保持职责清晰:
- SaveSnapshotSummary -> SaveSnapshot
- SnapshotProviderIds + SnapshotBootstrap -> SnapshotRegistry
- SnapshotSectionDeserializer -> SnapshotRestore
- SnapshotSerializer -> SnapshotPersistence

同步更新 SnapshotService 与 YarnVariableStorage 的调用点。
This commit is contained in:
2026-06-12 17:40:36 +08:00
parent 789ca207bd
commit cc3e1dd138
16 changed files with 165 additions and 307 deletions
@@ -211,7 +211,7 @@ namespace AibisDream
}
Instance = this;
SnapshotBootstrap.EnsureInitialized();
SnapshotRegistry.EnsureInitialized();
}
#endregion
+41
View File
@@ -198,4 +198,45 @@ namespace AibisDream.SaveSystem
}
#endregion
/// <summary>
/// 从 <see cref="SaveSnapshot"/> 根对象直接读取的宏观摘要(场景 / 章节 SO / YarnProject)。
/// <para>
/// scene 与 anchor 已提升到根对象,无需再从 sections 字典派生。
/// 槽位 / 「继续游戏」等 UI 在需要展示时调用 <see cref="SaveSnapshotSummary.From"/> 即可。
/// </para>
/// </summary>
public readonly struct SaveSnapshotSummary
{
/// <summary>Addressable 场景 key(来自 <see cref="SaveSnapshot.scene"/>)。</summary>
public readonly string SceneName;
/// <summary>当前 <c>TalkSceneSO</c> 的 asset 名称(来自 <see cref="AnchorSnapshot.sceneSoName"/>)。</summary>
public readonly string SceneSoName;
/// <summary>当前 <c>YarnProject</c> 的 name(来自 <see cref="AnchorSnapshot.yarnProjectId"/>)。</summary>
public readonly string YarnProjectId;
public SaveSnapshotSummary(string sceneName, string sceneSoName, string yarnProjectId)
{
SceneName = sceneName;
SceneSoName = sceneSoName;
YarnProjectId = yarnProjectId;
}
/// <summary>从快照根对象直接读取宏观摘要。</summary>
public static SaveSnapshotSummary From(SaveSnapshot snapshot)
{
if (snapshot == null)
{
return new SaveSnapshotSummary(null, null, null);
}
var sceneName = snapshot.scene?.sceneName;
var sceneSoName = snapshot.anchor?.sceneSoName;
var yarnProjectId = snapshot.anchor?.yarnProjectId;
return new SaveSnapshotSummary(sceneName, sceneSoName, yarnProjectId);
}
}
}
@@ -1,43 +0,0 @@
namespace AibisDream.SaveSystem
{
/// <summary>
/// 从 <see cref="SaveSnapshot"/> 根对象直接读取的宏观摘要(场景 / 章节 SO / YarnProject)。
/// <para>
/// scene 与 anchor 已提升到根对象,无需再从 sections 字典派生。
/// 槽位 / 「继续游戏」等 UI 在需要展示时调用 <see cref="From"/> 即可。
/// </para>
/// </summary>
public readonly struct SaveSnapshotSummary
{
/// <summary>Addressable 场景 key(来自 <see cref="SaveSnapshot.scene"/>)。</summary>
public readonly string SceneName;
/// <summary>当前 <c>TalkSceneSO</c> 的 asset 名称(来自 <see cref="AnchorSnapshot.sceneSoName"/>)。</summary>
public readonly string SceneSoName;
/// <summary>当前 <c>YarnProject</c> 的 name(来自 <see cref="AnchorSnapshot.yarnProjectId"/>)。</summary>
public readonly string YarnProjectId;
public SaveSnapshotSummary(string sceneName, string sceneSoName, string yarnProjectId)
{
SceneName = sceneName;
SceneSoName = sceneSoName;
YarnProjectId = yarnProjectId;
}
/// <summary>从快照根对象直接读取宏观摘要。</summary>
public static SaveSnapshotSummary From(SaveSnapshot snapshot)
{
if (snapshot == null)
{
return new SaveSnapshotSummary(null, null, null);
}
var sceneName = snapshot.scene?.sceneName;
var sceneSoName = snapshot.anchor?.sceneSoName;
var yarnProjectId = snapshot.anchor?.yarnProjectId;
return new SaveSnapshotSummary(sceneName, sceneSoName, yarnProjectId);
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 5b1c9a2f7d3e4c8a9b6f0d1e2a3c4b5d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,26 +0,0 @@
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 EnvironmentSnapshotProvider());
SnapshotRegistry.Register(new ActorSnapshotProvider());
SnapshotRegistry.Register(new AudioSnapshotProvider());
SnapshotRegistry.Register(new TimelineSnapshotProvider());
SnapshotRegistry.Register(new FixSnapshotProvider());
SnapshotRegistry.Register(new ScreenSnapshotProvider());
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 8deaa7a6856bb384fba9e42453a06e1d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,34 +1,84 @@
using System.IO;
using AibisDream.Utility;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEngine;
namespace AibisDream.SaveSystem
{
/// <summary>
/// 快照与磁盘的边界:读写 JSON 文件、检测旧档格式。
/// 快照与磁盘的边界:序列化、读写 JSON 文件、检测旧档格式。
/// <para>
/// P1 使用固定测试路径(<see cref="GetTestFilePath"/>
/// P2 将扩展为槽位目录、原子写、sidecar,接口可保持不变
/// P2 起,业务存档统一通过 <see cref="SlotManager"/> 写入槽位目录
/// 本类保留按路径读写的低级接口,供旧档兼容、迁移和外部调试使用
/// </para>
/// </summary>
public static class SnapshotPersistence
{
/// <summary>P1 自测落盘:<c>SnapshotTestPath/latest_snapshot.json</c>。</summary>
private static readonly JsonSerializerSettings Settings = new()
{
TypeNameHandling = TypeNameHandling.None,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
NullValueHandling = NullValueHandling.Ignore
};
/// <summary>P1 自测落盘:<c>SnapshotTestPath/latest_snapshot.json</c>P2 迁移源)。</summary>
public static string GetTestFilePath()
{
return Path.Combine(ConstRef.SnapshotTestPath, ConstRef.SnapshotTestFileName);
}
/// <summary>将快照序列化并写入指定槽位。</summary>
public static void SaveToSlot(int slotIndex, SaveSnapshot snapshot)
{
SlotManager.SaveToSlot(slotIndex, snapshot, null);
}
/// <summary>从指定槽位读取快照。</summary>
public static SaveSnapshot LoadFromSlot(int slotIndex)
{
return SlotManager.LoadSnapshot(slotIndex);
}
/// <summary>将快照序列化为 JSON 字符串。</summary>
public static string Serialize(SaveSnapshot snapshot)
{
snapshot.schemaVersion = SaveSnapshotSchema.CurrentVersion;
return JsonConvert.SerializeObject(snapshot, Formatting.Indented, Settings);
}
/// <summary>从 JSON 字符串反序列化为快照。</summary>
public static SaveSnapshot Deserialize(string json)
{
var snapshot = JsonConvert.DeserializeObject<SaveSnapshot>(json, Settings);
if (snapshot != null && snapshot.schemaVersion != SaveSnapshotSchema.CurrentVersion)
{
Debug.LogWarning(
$"[SnapshotPersistence] schemaVersion {snapshot.schemaVersion} != {SaveSnapshotSchema.CurrentVersion},按当前结构读取。");
}
return snapshot;
}
/// <summary>将快照序列化并写入 path(自动补 .json 后缀)。</summary>
public static void Save(SaveSnapshot snapshot, string pathWithoutExtension)
{
SnapshotSerializer.SaveToFile(snapshot, pathWithoutExtension);
var json = Serialize(snapshot);
var path = JsonUtil.FormatAsJsonPath(pathWithoutExtension);
var dir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(dir))
{
Directory.CreateDirectory(dir);
}
File.WriteAllText(path, json);
}
/// <summary>从 path 读取并反序列化为 <see cref="SaveSnapshot"/>。</summary>
public static SaveSnapshot Load(string pathWithoutExtension)
{
return SnapshotSerializer.LoadFromFile(pathWithoutExtension);
var json = File.ReadAllText(JsonUtil.FormatAsJsonPath(pathWithoutExtension));
return Deserialize(json);
}
/// <summary>
@@ -1,26 +0,0 @@
namespace AibisDream.SaveSystem
{
/// <summary>
/// 快照 section 的稳定字符串 id。
/// 用于 JSON key、Provider 注册与反序列化,避免使用 AssemblyQualifiedName。
/// </summary>
public static class SnapshotProviderIds
{
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";
public const string Fix = "fix";
/// <summary>
/// P1 应参与 Capture 的 provider 清单。
/// <see cref="SnapshotRegistry.ValidateRequiredProviders"/> 据此告警缺失项;不含深度维修。
/// scene 与 macro 已提升到 <see cref="SaveSnapshot"/> 根对象,由框架直管。
/// </summary>
public static readonly string[] RequiredForCapture =
{
Environment, Actor, Audio, Timeline, Fix, Screen
};
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 961a9f94f713b334388b4d1a0b1018c4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+40 -1
View File
@@ -1,9 +1,33 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace AibisDream.SaveSystem
{
/// <summary>
/// 快照 section 的稳定字符串 id。
/// 用于 JSON key、Provider 注册与反序列化,避免使用 AssemblyQualifiedName。
/// </summary>
public static class SnapshotProviderIds
{
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";
public const string Fix = "fix";
/// <summary>
/// P1 应参与 Capture 的 provider 清单。
/// <see cref="SnapshotRegistry.ValidateRequiredProviders"/> 据此告警缺失项;不含深度维修。
/// scene 与 macro 已提升到 <see cref="SaveSnapshot"/> 根对象,由框架直管。
/// </summary>
public static readonly string[] RequiredForCapture =
{
Environment, Actor, Audio, Timeline, Fix, Screen
};
}
/// <summary>
/// 运行时登记全部 <see cref="ISnapshotProvider"/>。
/// Capture/Restore 时按 <see cref="ISnapshotProvider.RestoreOrder"/> 迭代;
@@ -12,6 +36,21 @@ namespace AibisDream.SaveSystem
public static class SnapshotRegistry
{
private static readonly Dictionary<string, ISnapshotProvider> Providers = new();
private static bool _initialized;
/// <summary>幂等初始化:注册 P1 所需的全部 Provider。</summary>
public static void EnsureInitialized()
{
if (_initialized) return;
_initialized = true;
Register(new EnvironmentSnapshotProvider());
Register(new ActorSnapshotProvider());
Register(new AudioSnapshotProvider());
Register(new TimelineSnapshotProvider());
Register(new FixSnapshotProvider());
Register(new ScreenSnapshotProvider());
}
/// <summary>注册 provider;同 SaveId 后者覆盖前者。</summary>
public static void Register(ISnapshotProvider provider)
+25 -1
View File
@@ -1,4 +1,5 @@
using System.Collections;
using Newtonsoft.Json.Linq;
using UnityEngine;
namespace AibisDream.SaveSystem
@@ -34,7 +35,7 @@ namespace AibisDream.SaveSystem
continue;
}
dto = SnapshotSectionDeserializer.Coerce(provider.SaveId, dto);
dto = CoerceSectionDto(provider.SaveId, dto);
yield return provider.Restore(dto);
}
@@ -129,5 +130,28 @@ namespace AibisDream.SaveSystem
gameManager.SetSceneSoByName(snapshot.anchor.sceneSoName);
}
/// <summary>
/// 读盘后 <see cref="SaveSnapshot.sections"/> 中的值常为 <see cref="JObject"/>
/// 按 SaveId 转回各 Provider 所需的强类型 DTO。
/// </summary>
private static object CoerceSectionDto(string saveId, object dto)
{
if (dto is not JObject jobj)
{
return dto;
}
return saveId switch
{
SnapshotProviderIds.Environment => jobj.ToObject<EnvironmentSnapshotDto>(),
SnapshotProviderIds.Actor => jobj.ToObject<ActorSnapshotDto>(),
SnapshotProviderIds.Audio => jobj.ToObject<AudioSnapshotDto>(),
SnapshotProviderIds.Timeline => jobj.ToObject<TimelineSnapshotDto>(),
SnapshotProviderIds.Fix => jobj.ToObject<FixSnapshotDto>(),
SnapshotProviderIds.Screen => jobj.ToObject<ScreenSnapshotDto>(),
_ => jobj
};
}
}
}
@@ -1,31 +0,0 @@
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.Environment => jobj.ToObject<EnvironmentSnapshotDto>(),
SnapshotProviderIds.Actor => jobj.ToObject<ActorSnapshotDto>(),
SnapshotProviderIds.Audio => jobj.ToObject<AudioSnapshotDto>(),
SnapshotProviderIds.Timeline => jobj.ToObject<TimelineSnapshotDto>(),
SnapshotProviderIds.Fix => jobj.ToObject<FixSnapshotDto>(),
SnapshotProviderIds.Screen => jobj.ToObject<ScreenSnapshotDto>(),
_ => jobj
};
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: d7def29588434ab48b6f6768daf76635
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,115 +0,0 @@
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);
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: f57d93779e83c064f9ba2490e623e5b4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+2 -2
View File
@@ -11,7 +11,7 @@ namespace AibisDream.SaveSystem
/// <summary>捕获当前运行时快照。</summary>
public static SaveSnapshot Capture()
{
SnapshotBootstrap.EnsureInitialized();
SnapshotRegistry.EnsureInitialized();
return SnapshotCapture.Capture(YarnVariableStorage.Instance);
}
@@ -26,7 +26,7 @@ namespace AibisDream.SaveSystem
yield break;
}
SnapshotBootstrap.EnsureInitialized();
SnapshotRegistry.EnsureInitialized();
yield return SnapshotRestore.Restore(YarnVariableStorage.Instance, snapshot);
if (restoreAnchor)