feat: GameManager相关重构

This commit is contained in:
2026-07-17 14:24:02 +08:00
parent 2dcfeec938
commit 21d51945c3
47 changed files with 2064 additions and 1022 deletions
+35 -5
View File
@@ -8,13 +8,15 @@ namespace AibisDream.SaveSystem
[Serializable]
public sealed class RestoreOptions
{
public bool CleanSessionFirst;
public bool StrictValidation;
public bool StrictValidation = true;
public static RestoreOptions Default => new()
{
StrictValidation = true
};
public static RestoreOptions Default => new();
public static RestoreOptions DevJump => new()
{
CleanSessionFirst = true,
StrictValidation = true
};
}
@@ -22,10 +24,32 @@ namespace AibisDream.SaveSystem
public sealed class RestoreResult
{
public bool Success { get; internal set; }
public bool Cancelled { get; internal set; }
public string FailedPhase { get; internal set; }
public IReadOnlyList<string> Errors { get; internal set; } = Array.Empty<string>();
public IReadOnlyList<string> Warnings { get; internal set; } = Array.Empty<string>();
public IReadOnlyList<string> Log { get; internal set; } = Array.Empty<string>();
public TalkSceneSO RestoredTalkScene { get; internal set; }
public static RestoreResult CreateFailure(string phase, string error)
{
return new RestoreResult
{
Success = false,
FailedPhase = phase,
Errors = new[] { error }
};
}
}
internal sealed class RestorePreparation
{
internal bool Success => Result?.Success == true;
internal SaveSnapshot Snapshot { get; set; }
internal TalkSceneSO TargetTalkScene { get; set; }
internal string SourceLabel { get; set; }
internal RestoreOptions Options { get; set; }
internal RestoreResult Result { get; set; } = new();
}
/// <summary>
@@ -70,14 +94,20 @@ namespace AibisDream.SaveSystem
/// </summary>
public sealed class SnapshotRestoreContext
{
public SnapshotRestoreContext(SaveSnapshot snapshot, bool strictMode = false, Action<string> logStep = null)
public SnapshotRestoreContext(
SaveSnapshot snapshot,
TalkSceneSO targetTalkScene = null,
bool strictMode = false,
Action<string> logStep = null)
{
Snapshot = snapshot;
TargetTalkScene = targetTalkScene;
StrictMode = strictMode;
LogStep = logStep;
}
public SaveSnapshot Snapshot { get; }
public TalkSceneSO TargetTalkScene { get; }
public bool StrictMode { get; }
public Action<string> LogStep { get; }
public string CurrentPhase { get; private set; }
+18 -34
View File
@@ -24,22 +24,17 @@ yield return SaveRestoreOrchestrator.ExplicitSaveRoutine();
// 手动存档:复制最近落盘档(含 OnNodeStart 与 <<save>>
SaveRestoreOrchestrator.CreateManualSlot(slotIndex);
// 槽位读档
yield return SaveRestoreOrchestrator.RestoreFromSlot(slotIndex);
// 槽位读档:会话命令统一从 GameManager 进入
GameManager.Instance.TryRestoreSlot(slotIndex, result => { /* 处理结果 */ });
// 文件读档(旧档 / 调试)
yield return SaveRestoreOrchestrator.RestoreFromFile(path);
// 严格开发跳转:完整清理旧会话,并取得实际成功/失败结果
RestoreResult result = null;
yield return SaveRestoreOrchestrator.RestoreFromFile(
// 严格文件读档 / 开发跳转
GameManager.Instance.TryRestoreFile(
path,
RestoreOptions.DevJump,
value => result = value);
result => { /* 处理结果 */ });
// 仅内存快照
// 仅捕获内存快照;恢复仍须通过 GameManager,以保证会话互斥和失败清理
var snap = SnapshotService.Capture();
yield return SnapshotService.Restore(snap);
// Yarn 变量(与存档无关)
YarnVariableStorage.Instance.SetValue("$foo", 1f);
@@ -118,17 +113,18 @@ SaveSystem/
→ SnapshotPersistence.Save()
读盘:
SaveRestoreOrchestrator
→ SnapshotPersistence.Load() (或 legacy 分支
→ SnapshotService.Restore()
→ SnapshotRestore见「读档还原编排」
GameManager
→ SaveRestoreOrchestrator.PrepareRestore(读盘 + 预检,不修改运行时
→ SaveRestoreOrchestrator.ExecutePreparedRestore
→ SnapshotRestore变量 + Scene + Providers + Anchor
→ GameManager 提交章节和会话状态
```
## 读档还原编排(终态 vs P1 代码)
## 读档还原编排
> **勿将当前代码当作终态。** P1 中 `ISnapshotProvider.Restore` 统一返回 `IEnumerator``SnapshotRestore` 对每个 Provider 做 `yield return`——这是受旧 `IData.Load()` 影响的**临时 scaffolding**。终态见设计文档 **§4.1 / D6**P4 实施前应 refactor
读档已经采用 Prepare / Execute 边界与 Phase + Barrier 模型。所有文件统一反序列化,并由 schema 与必填字段预检判断是否兼容;不存在旧格式专用识别、恢复或转写分支
### 终态:Phase + Barrier
### Phase + Barrier
读档分阶段推进,仅在**必须等待**的边界上 `yield`
@@ -137,35 +133,23 @@ Phase 0 Yarn 变量(sync
Phase 1 场景加载(BarrierLoadSceneAsync)← 框架直管,不走 Provider
Phase 1.5 设置章节 SOsync)← 框架直管,从 anchor.sceneSoName 读取
Phase 2 env / actor / audio / timeline / fix / screenProvider 按 RestoreOrder
└─ timeline`Stopped` = untouched(从未 Evaluate),读档仅还原 `isActive`,不 Reset/Evaluate
Phase 2 可选 Barrier(如 Timeline Addressable 须显式等待)
Phase 2.5 SceneReadinessBarrier
Phase 3 加载对话工程 + RestoreAnchorBarrierStartDialogue)← 框架直管
Phase 4 P4:淡入淡出等演出时序
Postflight 校验
```
- **核心层**`scene``anchor``yarnVariables`)由 `SnapshotRestore` 框架直管,不通过 Provider 注册。
- **表现层**`sections` 内各子系统)通过 `ISnapshotProvider` 扩展;`RestoreOrder` 表示同 Phase 内的建议顺序或软依赖。
- **逐项还原(D1)**指各子系统各自写回状态,**不是** Provider 之间逐步 `yield return`
### P1 临时形态 vs P4 目标
| | P1(当前) | P4 目标 |
| --- | --- | --- |
| 核心层还原 | 硬编码在 `SnapshotRestore` | 保持框架直管 |
| Provider 还原 | 全部 `IEnumerator Restore` | 默认 `void Restore`;仅需 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 协程链」模式。
Provider 使用 `ISyncSnapshotProvider``IAsyncSnapshotProvider` 显式声明同步/异步恢复;异步 Provider 必须把等待过程返回给编排层,禁止内部 fire-and-forget。
## 快照 JSON 结构(schemaVersion = 1
@@ -230,7 +230,7 @@ namespace AibisDream.SaveSystem
return false;
}
if (GameManager.Instance != null && GameManager.Instance.state.isInPause)
if (GameManager.Instance != null && GameManager.Session.IsPaused)
{
reason = SavePointRejectReason.GamePaused;
return false;
@@ -25,8 +25,6 @@ namespace AibisDream.SaveSystem
private static int _autoSaveSuppressDepth;
private static readonly List<string> _lastRestoreLog = new();
private const float RestoreFadeDuration = 0.2f;
/// <summary>启动自动存档协程(手动/调试入口)。</summary>
public static void TryAutoSave()
{
@@ -138,86 +136,145 @@ namespace AibisDream.SaveSystem
SlotManager.CopyAutoToManual(slotIndex);
}
/// <summary>从指定槽位读档并还原。</summary>
public static IEnumerator RestoreFromSlot(int slotIndex)
internal static RestorePreparation PrepareRestoreFromSlot(
int slotIndex,
TalkSceneGraphIndex talkSceneIndex,
RestoreOptions options)
{
var snapshot = SlotManager.LoadSnapshot(slotIndex);
if (snapshot == null)
{
Debug.LogError($"[SaveRestoreOrchestrator] 槽位 {slotIndex} 不存在或读取失败");
yield break;
}
yield return RestoreSnapshot(snapshot, $"slot_{slotIndex}");
}
/// <summary>从文件读档并还原;自动区分新快照格式与 legacy 格式。</summary>
public static IEnumerator RestoreFromFile(string savePath)
{
yield return RestoreFromFile(savePath, RestoreOptions.Default, null);
}
/// <summary>从文件恢复并返回结构化结果;开发跳转使用严格校验和完整会话清理。</summary>
public static IEnumerator RestoreFromFile(
string savePath,
RestoreOptions options,
Action<RestoreResult> completed)
{
options ??= RestoreOptions.Default;
var result = new RestoreResult();
bool isLegacy;
var sourceLabel = $"slot_{slotIndex}";
SaveSnapshot snapshot;
try
{
isLegacy = SnapshotPersistence.IsLegacyFormat(savePath);
snapshot = SlotManager.LoadSnapshot(slotIndex);
}
catch (Exception ex)
{
result.Success = false;
result.FailedPhase = "LoadSnapshot";
result.Errors = new[] { ex.Message };
completed?.Invoke(result);
yield break;
return FailedPreparation(sourceLabel, "LoadSnapshot", ex.Message, options);
}
if (isLegacy)
if (snapshot == null)
{
if (options.StrictValidation)
{
result.Success = false;
result.FailedPhase = "Preflight";
result.Errors = new[] { "legacy format is not supported by strict restore" };
completed?.Invoke(result);
yield break;
}
Debug.LogWarning("[SaveRestoreOrchestrator] 检测到旧格式存档,请使用新快照格式重新保存。");
yield return RestoreLegacyWithFlow(savePath);
result.Success = true;
result.Log = new List<string>(_lastRestoreLog);
completed?.Invoke(result);
yield break;
return FailedPreparation(
sourceLabel,
"LoadSnapshot",
$"Slot {slotIndex} does not exist or could not be read.",
options);
}
SaveSnapshot snapshot = null;
return PrepareSnapshot(snapshot, sourceLabel, talkSceneIndex, options);
}
internal static RestorePreparation PrepareRestoreFromFile(
string savePath,
TalkSceneGraphIndex talkSceneIndex,
RestoreOptions options)
{
options ??= RestoreOptions.Default;
try
{
SaveSnapshot snapshot;
using (new CodeTimer("LoadSnapshot"))
{
snapshot = SnapshotPersistence.Load(savePath);
}
return PrepareSnapshot(snapshot, savePath, talkSceneIndex, options);
}
catch (Exception ex)
{
result.Success = false;
result.FailedPhase = "LoadSnapshot";
result.Errors = new[] { ex.Message };
completed?.Invoke(result);
return FailedPreparation(savePath, "LoadSnapshot", ex.Message, options);
}
}
internal static IEnumerator ExecutePreparedRestore(
RestorePreparation preparation,
Func<bool> isCancellationRequested)
{
if (preparation?.Success != true)
{
yield break;
}
yield return RestoreSnapshot(snapshot, savePath, options, result);
completed?.Invoke(result);
var result = preparation.Result;
result.Success = false;
using (SuppressAutoSaveScope($"RestoreSnapshot:{preparation.SourceLabel}"))
{
IsRestoring = true;
ResetRestoreLog(preparation.SourceLabel);
var context = new SnapshotRestoreContext(
preparation.Snapshot,
preparation.TargetTalkScene,
strictMode: preparation.Options?.StrictValidation ?? true,
logStep: AddRestoreLog);
try
{
SnapshotRegistry.EnsureInitialized();
yield return SnapshotRestore.RestoreState(
YarnVariableStorage.Instance,
preparation.Snapshot,
context,
isCancellationRequested);
if (ShouldCancel(isCancellationRequested, result, context))
{
yield break;
}
if (context.StrictMode && context.HasErrors)
{
yield break;
}
context.SetPhase("Phase 2.5: Scene readiness");
SceneReadinessResult readiness = null;
yield return SceneReadiness.WaitUntilReady(
SceneLoader.Instance.CurrentScene,
isCancellationRequested,
value => readiness = value);
if (readiness?.Cancelled == true)
{
result.Cancelled = true;
yield break;
}
if (readiness?.Success != true)
{
context.Error(readiness?.Error ?? "Scene readiness did not return a result.");
yield break;
}
if (ShouldCancel(isCancellationRequested, result, context))
{
yield break;
}
yield return SnapshotRestore.RestoreAnchor(preparation.Snapshot, context);
if (!context.StrictMode || !context.HasErrors)
{
context.SetPhase("Postflight validation");
ValidateRestoredRuntime(preparation, context);
}
result.Success = !context.HasErrors;
if (result.Success)
{
result.RestoredTalkScene = preparation.TargetTalkScene;
}
}
finally
{
IsRestoring = false;
AddRestoreLog("Restore finished");
result.Success = result.Success && !result.Cancelled && !context.HasErrors;
result.FailedPhase = result.Success || result.Cancelled ? null : context.CurrentPhase;
result.Errors = new List<string>(context.Errors);
result.Warnings = new List<string>(context.Warnings);
result.Log = new List<string>(_lastRestoreLog);
}
}
}
public static IDisposable SuppressAutoSaveScope(string reason)
@@ -227,180 +284,122 @@ namespace AibisDream.SaveSystem
return new AutoSaveSuppressScope(reason);
}
/// <summary>旧档:仅恢复 Yarn 变量,再 Capture 写入 slot_0 完成格式迁移。</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"] != null)
{
Debug.LogWarning(
"[SaveRestoreOrchestrator] 旧档 systemData 段已无法还原(IData 已移除);" +
"仅恢复 Yarn 变量并迁移为新快照格式。");
}
var snapshot = SnapshotService.Capture();
SlotManager.SaveToAutoSlot(snapshot, null);
yield break;
}
private static IEnumerator RestoreLegacyWithFlow(string savePath)
{
using (SuppressAutoSaveScope("RestoreLegacy"))
{
IsRestoring = true;
ResetRestoreLog("legacy");
try
{
PrepareGameSessionForRestore();
yield return FadeInForRestore();
yield return RestoreLegacy(savePath);
yield return null;
FinalizeGameSessionAfterRestore();
yield return FadeOutForRestore();
ScreenSnapshotHelper.ApplyDeferredFadeScreenIfNeeded();
}
finally
{
IsRestoring = false;
AddRestoreLog("Legacy restore finished");
}
}
}
private static IEnumerator RestoreSnapshot(SaveSnapshot snapshot, string sourceLabel)
{
yield return RestoreSnapshot(snapshot, sourceLabel, RestoreOptions.Default, new RestoreResult());
}
private static IEnumerator RestoreSnapshot(
private static RestorePreparation PrepareSnapshot(
SaveSnapshot snapshot,
string sourceLabel,
RestoreOptions options,
RestoreResult result)
TalkSceneGraphIndex talkSceneIndex,
RestoreOptions options)
{
options ??= RestoreOptions.Default;
var preparation = new RestorePreparation
{
Snapshot = snapshot,
SourceLabel = sourceLabel,
Options = options,
Result = new RestoreResult()
};
var errors = new List<string>();
if (snapshot == null)
errors.Add("Snapshot is null.");
else
{
Debug.LogError("[SaveRestoreOrchestrator] snapshot 为 null,无法读档。");
result.Success = false;
result.FailedPhase = "Preflight";
result.Errors = new[] { "snapshot is null" };
yield break;
if (snapshot.schemaVersion != SaveSnapshotSchema.CurrentVersion)
errors.Add($"Unsupported schema {snapshot.schemaVersion}; expected {SaveSnapshotSchema.CurrentVersion}.");
if (string.IsNullOrWhiteSpace(snapshot.scene?.sceneName))
errors.Add("Snapshot scene is missing.");
if (string.IsNullOrWhiteSpace(snapshot.anchor?.sceneSoName))
errors.Add("Snapshot TalkSceneSO is missing.");
if (string.IsNullOrWhiteSpace(snapshot.anchor?.yarnProjectId))
errors.Add("Snapshot YarnProject is missing.");
}
using (SuppressAutoSaveScope($"RestoreSnapshot:{sourceLabel}"))
TalkSceneSO targetScene = null;
if (errors.Count == 0
&& !talkSceneIndex.TryFindByName(snapshot.anchor.sceneSoName, out targetScene, out var lookupError))
{
IsRestoring = true;
ResetRestoreLog(sourceLabel);
var context = new SnapshotRestoreContext(
snapshot,
strictMode: options.StrictValidation,
logStep: AddRestoreLog);
try
{
context.SetPhase("Preflight");
ValidateSnapshotForRestore(snapshot, context);
if (context.StrictMode && context.HasErrors)
{
yield break;
}
if (options.CleanSessionFirst && GameManager.Instance != null)
{
context.SetPhase("Clean game session");
yield return GameManager.Instance.ResetGameSessionRoutine();
}
PrepareGameSessionForRestore();
yield return FadeInForRestore();
SnapshotRegistry.EnsureInitialized();
yield return SnapshotRestore.RestoreState(YarnVariableStorage.Instance, snapshot, context);
if (!context.StrictMode || !context.HasErrors)
{
yield return SnapshotRestore.RestoreAnchor(snapshot, context);
}
if (!context.StrictMode || !context.HasErrors)
{
context.SetPhase("Phase 3.5: settle one frame");
yield return null;
ValidateRestoredRuntime(snapshot, context);
}
if (!context.StrictMode || !context.HasErrors)
{
FinalizeGameSessionAfterRestore();
}
yield return FadeOutForRestore();
ScreenSnapshotHelper.ApplyDeferredFadeScreenIfNeeded();
}
finally
{
IsRestoring = false;
AddRestoreLog("Restore finished");
result.Success = !context.HasErrors;
result.FailedPhase = context.HasErrors ? context.CurrentPhase : null;
result.Errors = new List<string>(context.Errors);
result.Warnings = new List<string>(context.Warnings);
result.Log = new List<string>(_lastRestoreLog);
}
errors.Add(lookupError);
}
}
private static void ValidateSnapshotForRestore(SaveSnapshot snapshot, SnapshotRestoreContext context)
{
if (snapshot.schemaVersion != SaveSnapshotSchema.CurrentVersion)
context.Error($"Unsupported schema {snapshot.schemaVersion}; expected {SaveSnapshotSchema.CurrentVersion}.");
if (string.IsNullOrWhiteSpace(snapshot.scene?.sceneName))
context.Error("Snapshot scene is missing.");
if (string.IsNullOrWhiteSpace(snapshot.anchor?.sceneSoName))
context.Error("Snapshot TalkSceneSO is missing.");
if (string.IsNullOrWhiteSpace(snapshot.anchor?.yarnProjectId))
context.Error("Snapshot YarnProject is missing.");
if (string.IsNullOrWhiteSpace(snapshot.anchor?.nodeName))
context.Error("Snapshot Yarn node is missing.");
if (context.HasErrors) return;
var sceneSo = GameManager.Instance?.FindSceneSoByName(snapshot.anchor.sceneSoName);
if (sceneSo == null)
if (errors.Count == 0
&& (targetScene.yarnProject == null
|| !string.Equals(
targetScene.yarnProject.name,
snapshot.anchor.yarnProjectId,
StringComparison.Ordinal)))
{
context.Error($"TalkSceneSO does not exist: {snapshot.anchor.sceneSoName}.");
return;
errors.Add($"TalkSceneSO YarnProject mismatch: {snapshot.anchor.yarnProjectId}.");
}
if (sceneSo.yarnProject == null
|| !string.Equals(sceneSo.yarnProject.name, snapshot.anchor.yarnProjectId, StringComparison.Ordinal))
{
context.Error($"TalkSceneSO YarnProject mismatch: {snapshot.anchor.yarnProjectId}.");
return;
}
if (!Array.Exists(sceneSo.yarnProject.NodeNames,
if (errors.Count == 0
&& !string.IsNullOrWhiteSpace(snapshot.anchor.nodeName)
&& !Array.Exists(
targetScene.yarnProject.NodeNames,
node => string.Equals(node, snapshot.anchor.nodeName, StringComparison.Ordinal)))
{
context.Error($"Yarn node does not exist: {snapshot.anchor.nodeName}.");
errors.Add($"Yarn node does not exist: {snapshot.anchor.nodeName}.");
}
if (errors.Count > 0)
{
preparation.Result = new RestoreResult
{
Success = false,
FailedPhase = "Preflight",
Errors = errors
};
return preparation;
}
preparation.TargetTalkScene = targetScene;
preparation.Result.Success = true;
preparation.Result.RestoredTalkScene = targetScene;
return preparation;
}
private static void ValidateRestoredRuntime(SaveSnapshot snapshot, SnapshotRestoreContext context)
private static RestorePreparation FailedPreparation(
string sourceLabel,
string phase,
string error,
RestoreOptions options)
{
context.SetPhase("Postflight validation");
return new RestorePreparation
{
SourceLabel = sourceLabel,
Options = options ?? RestoreOptions.Default,
Result = RestoreResult.CreateFailure(phase, error)
};
}
private static bool ShouldCancel(
Func<bool> isCancellationRequested,
RestoreResult result,
SnapshotRestoreContext context)
{
if (isCancellationRequested?.Invoke() != true)
{
return false;
}
result.Cancelled = true;
context.Log("Restore cancelled between phases.");
return true;
}
private static void ValidateRestoredRuntime(
RestorePreparation preparation,
SnapshotRestoreContext context)
{
var snapshot = preparation.Snapshot;
var sceneLoader = SceneLoader.Instance;
if (sceneLoader == null || sceneLoader.IsLoading)
context.Error("SceneLoader is not ready.");
else if (!string.Equals(sceneLoader.CurrentSceneName, snapshot.scene.sceneName, StringComparison.Ordinal))
context.Error($"Scene mismatch: {sceneLoader.CurrentSceneName ?? "none"}.");
var sceneSo = GameManager.Instance?.GetCurrentTalkSceneSo();
if (!string.Equals(sceneSo?.name, snapshot.anchor.sceneSoName, StringComparison.Ordinal))
context.Error($"TalkSceneSO mismatch: {sceneSo?.name ?? "none"}.");
var targetScene = preparation.TargetTalkScene;
if (!string.Equals(targetScene?.name, snapshot.anchor.sceneSoName, StringComparison.Ordinal))
context.Error($"TalkSceneSO mismatch: {targetScene?.name ?? "none"}.");
var runner = DialogController.Instance?.DialogueRunner;
if (!string.Equals(runner?.YarnProject?.name, snapshot.anchor.yarnProjectId, StringComparison.Ordinal))
@@ -417,78 +416,6 @@ namespace AibisDream.SaveSystem
}
}
/// <summary>
/// 读档前对齐「已进入游戏」的 UI 与输入状态。
/// 编辑器测试工具等路径可能跳过 <see cref="GameLoopEnum.GameStart"/>,导致 TerminalPanel 未关闭、
/// <see cref="GameManager.state.isInGame"/> 为 false,从而无法推进对话。
/// </summary>
private static void PrepareGameSessionForRestore()
{
AddRestoreLog("Prepare game session for restore");
// 读档淡入使用 DOTween(受 timeScale 影响);若处于暂停态须先恢复,否则 FadeIn 永不结束、画面卡在诊室/终端。
Time.timeScale = 1f;
var gameManager = GameManager.Instance;
if (gameManager != null && !gameManager.state.isInGame)
{
EnumEventSystem.Global.Send(GameLoopEnum.GameStart);
}
var ui = UIManager.Instance;
if (ui != null)
{
ui.HidePanel<TerminalPanel>();
ui.HidePanel<InGameTerminalPanel>();
ui.ShowPanel<MainPanel>();
ui.HidePanel<SavesPanel>();
ui.HidePanel<EndPanel>();
}
if (gameManager != null && gameManager.state.isInPause)
{
gameManager.ContinueGame();
}
else
{
DialogUIManager.Instance?.OpenCanvas();
}
}
/// <summary>读档完成后确保对话 Canvas 可用、退出暂停态。</summary>
private static void FinalizeGameSessionAfterRestore()
{
AddRestoreLog("Finalize game session after restore");
DialogUIManager.Instance?.OpenCanvas();
var gameManager = GameManager.Instance;
if (gameManager != null && gameManager.state.isInPause)
{
gameManager.ContinueGame();
}
}
private static IEnumerator FadeInForRestore()
{
AddRestoreLog("Begin restore fade in");
var panel = UIManager.Instance?.GetPanel<ScreenTransitionPanel>();
if (panel != null)
{
yield return panel.FadeInAsync(RestoreFadeDuration, useUnscaledTime: true);
}
}
private static IEnumerator FadeOutForRestore()
{
AddRestoreLog("End restore fade out");
var panel = UIManager.Instance?.GetPanel<ScreenTransitionPanel>();
if (panel != null)
{
yield return panel.FadeOutAsync(RestoreFadeDuration, useUnscaledTime: true);
}
}
private static void ResetRestoreLog(string sourceLabel)
{
_lastRestoreLog.Clear();
+1 -1
View File
@@ -57,7 +57,7 @@ namespace AibisDream.SaveSystem
[Serializable]
public class AnchorSnapshot
{
/// <summary>当前章节 TalkSceneSO 名称,用于 <see cref="GameManager.SetSceneSoByName"/>。</summary>
/// <summary>当前章节 TalkSceneSO 名称,由读档预检解析为目标章节。</summary>
public string sceneSoName;
public string yarnProjectId;
public string nodeName;
+3 -2
View File
@@ -88,8 +88,9 @@ namespace AibisDream.SaveSystem
var yarnProject = dialog.DialogueRunner?.YarnProject;
var projectId = yarnProject != null ? yarnProject.name : string.Empty;
var gameManager = GameManager.Instance;
var sceneSoName = gameManager != null ? gameManager.GetSceneSoName() : null;
var sceneSoName = GameManager.Instance != null
? GameManager.Session.CurrentTalkScene?.name
: null;
snapshot.anchor = new AnchorSnapshot
{
@@ -1,16 +1,15 @@
using System.IO;
using AibisDream.Utility;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEngine;
namespace AibisDream.SaveSystem
{
/// <summary>
/// 快照与磁盘的边界:序列化读写 JSON 文件、检测旧档格式
/// 快照与磁盘的边界:序列化读写 JSON 文件。
/// <para>
/// P2 起,业务存档统一通过 <see cref="SlotManager"/> 写入槽位目录;
/// 本类保留按路径读写的低级接口,供旧档兼容、迁移和外部调试使用。
/// 本类保留按路径读写的低级接口,供迁移和外部调试使用。
/// </para>
/// </summary>
public static class SnapshotPersistence
@@ -80,21 +79,5 @@ namespace AibisDream.SaveSystem
return Deserialize(json);
}
/// <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);
}
}
}
+43 -30
View File
@@ -15,7 +15,8 @@ namespace AibisDream.SaveSystem
public static IEnumerator RestoreState(
YarnVariableStorage storage,
SaveSnapshot snapshot,
SnapshotRestoreContext context)
SnapshotRestoreContext context,
System.Func<bool> isCancellationRequested = null)
{
if (snapshot == null)
{
@@ -27,17 +28,15 @@ namespace AibisDream.SaveSystem
context.SetPhase("Phase 0: Restore Yarn variables");
RestoreYarnVariables(storage, snapshot);
if (IsCancellationRequested(isCancellationRequested, context)) yield break;
context.SetPhase("Phase 1: Load scene");
yield return RestoreScene(snapshot, context);
if (context.StrictMode && context.HasErrors) yield break;
context.SetPhase("Phase 1.5: Restore scene SO");
RestoreSceneSo(snapshot, context);
if (IsCancellationRequested(isCancellationRequested, context)) yield break;
if (context.StrictMode && context.HasErrors) yield break;
context.SetPhase("Phase 2: Restore providers (ordered by RestoreOrder)");
yield return RestoreProvidersInOrder(snapshot, context);
yield return RestoreProvidersInOrder(snapshot, context, isCancellationRequested);
}
/// <summary>
@@ -74,7 +73,7 @@ namespace AibisDream.SaveSystem
if (!string.IsNullOrEmpty(snapshot.anchor.yarnProjectId))
{
var sceneSo = GameManager.Instance?.GetCurrentTalkSceneSo();
var sceneSo = context.TargetTalkScene;
if (sceneSo?.yarnProject == null)
{
context.Error($"章节 {snapshot.anchor.sceneSoName} 没有可用的 YarnProject。");
@@ -138,41 +137,37 @@ namespace AibisDream.SaveSystem
yield break;
}
yield return sceneLoader.LoadSceneAsync(snapshot.scene.sceneName);
SceneOperationResult result = null;
yield return sceneLoader.LoadSceneAsync(
snapshot.scene.sceneName,
value => result = value);
if (result?.Success != true)
{
context.Error(result?.Error ?? "Scene load did not return a result.");
yield break;
}
// 再等一帧,等Awake执行完
yield return null;
}
private static void RestoreSceneSo(SaveSnapshot snapshot, SnapshotRestoreContext context)
{
if (snapshot.anchor == null || string.IsNullOrEmpty(snapshot.anchor.sceneSoName))
{
context.Warn("快照缺少 anchor.sceneSoName,跳过章节 SO 设置。");
return;
}
var gameManager = GameManager.Instance;
if (gameManager == null)
{
context.Warn("GameManager 未初始化,无法设置章节 SO。");
return;
}
if (!gameManager.SetSceneSoByName(snapshot.anchor.sceneSoName))
{
context.Error($"找不到章节 SO{snapshot.anchor.sceneSoName},无法恢复 YarnProject。");
}
}
/// <summary>
/// 按 <see cref="ISnapshotProvider.RestoreOrder"/> 依次还原;
/// 同步 Provider 连续调用,异步 Provider 作为 Barrier 挂起,二者混排而非分两批。
/// </summary>
private static IEnumerator RestoreProvidersInOrder(SaveSnapshot snapshot, SnapshotRestoreContext context)
private static IEnumerator RestoreProvidersInOrder(
SaveSnapshot snapshot,
SnapshotRestoreContext context,
System.Func<bool> isCancellationRequested)
{
foreach (var provider in SnapshotRegistry.GetOrderedProviders())
{
if (IsCancellationRequested(isCancellationRequested, context))
{
yield break;
}
if (!TryGetSectionDto(snapshot, provider, context, out var dto))
{
continue;
@@ -199,9 +194,27 @@ namespace AibisDream.SaveSystem
{
yield break;
}
if (IsCancellationRequested(isCancellationRequested, context))
{
yield break;
}
}
}
private static bool IsCancellationRequested(
System.Func<bool> isCancellationRequested,
SnapshotRestoreContext context)
{
if (isCancellationRequested?.Invoke() != true)
{
return false;
}
context.Log("Restore cancellation requested between phases.");
return true;
}
private static bool TryGetSectionDto(
SaveSnapshot snapshot,
ISnapshotProvider provider,
+1 -23
View File
@@ -1,9 +1,7 @@
using System.Collections;
namespace AibisDream.SaveSystem
{
/// <summary>
/// 快照层对外 APICapture / Restore,不涉及文件与 UI
/// 快照层捕获 API。恢复必须通过 GameManager 会话命令进入
/// 游戏逻辑应优先使用 <see cref="SaveRestoreOrchestrator"/> 完成存读档流程。
/// </summary>
public static class SnapshotService
@@ -17,25 +15,5 @@ namespace AibisDream.SaveSystem
return SnapshotCapture.Capture(YarnVariableStorage.Instance, triggerNodeName, omitAnchor);
}
/// <summary>
/// 还原快照状态;默认继续执行 <see cref="SnapshotRestore.RestoreAnchor"/>。
/// 业务读档流程应优先使用 <see cref="SaveRestoreOrchestrator"/>,以获得黑屏与自动存档抑制。
/// </summary>
public static IEnumerator Restore(SaveSnapshot snapshot, bool restoreAnchor = true)
{
if (snapshot == null)
{
yield break;
}
SnapshotRegistry.EnsureInitialized();
var context = new SnapshotRestoreContext(snapshot);
yield return SnapshotRestore.RestoreState(YarnVariableStorage.Instance, snapshot, context);
if (restoreAnchor)
{
yield return SnapshotRestore.RestoreAnchor(snapshot, context);
}
}
}
}