Files
aibis-dream/Assets/Scripts/SaveSystem/SaveRestoreOrchestrator.cs
T

456 lines
17 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using AibisDream.Framework;
using AibisDream.UI;
using UnityEngine;
namespace AibisDream.SaveSystem
{
/// <summary>
/// 存读档流程编排层:连接 UI、<see cref="SnapshotService"/> 与槽位系统。
/// </summary>
public static class SaveRestoreOrchestrator
{
/// <summary>是否正在读档还原中;供 UI / 日志 / 外部系统观察。</summary>
public static bool IsRestoring { get; private set; }
/// <summary>是否处于自动存档抑制作用域;真正用于防止读档期间覆盖 slot_0。</summary>
public static bool IsAutoSaveSuppressed => _autoSaveSuppressDepth > 0;
public static IReadOnlyList<string> LastRestoreLog => _lastRestoreLog;
private static bool _isAutoSaving;
private static int _autoSaveSuppressDepth;
private static readonly List<string> _lastRestoreLog = new();
/// <summary>启动自动存档协程(手动/调试入口)。</summary>
public static void TryAutoSave()
{
if (YarnVariableStorage.Instance == null)
{
Debug.LogError("[SaveRestoreOrchestrator] YarnVariableStorage 未初始化");
return;
}
if (!SavePointEvaluator.CanAutoSave(out var reason))
{
Debug.LogWarning($"[SaveRestoreOrchestrator] 当前不是可存点,跳过自动存档:{reason}");
return;
}
var (triggerNodeName, _) = DialogController.Instance.GetCurrentNodeContext();
DialogController.Instance.StartCoroutine(AutoSaveRoutine(triggerNodeName));
}
/// <summary>自动存档协程:settle 一帧 → 主线程 Capture → 后台序列化写盘(不阻塞主线程)。</summary>
/// <param name="triggerNodeName">见 <see cref="SnapshotCapture.Capture"/>;显式 save 传 null。</param>
/// <param name="omitAnchor">为 true 时不写入 Yarn 节点 anchor。</param>
public static IEnumerator AutoSaveRoutine(string triggerNodeName = null, bool omitAnchor = false)
{
if (_isAutoSaving)
{
Debug.LogWarning("[SaveRestoreOrchestrator] 存档进行中,跳过重复请求。");
yield break;
}
yield return null;
if (_isAutoSaving)
{
Debug.LogWarning("[SaveRestoreOrchestrator] 存档进行中,跳过重复请求。");
yield break;
}
_isAutoSaving = true;
var infoPanel = UIManager.Instance.GetPanel<InfoPanel>();
infoPanel?.ShowSaveLoading();
SaveSnapshot snapshot;
byte[] thumbnail;
try
{
using (new CodeTimer("SaveSnapshot"))
{
snapshot = SnapshotService.Capture(triggerNodeName, omitAnchor);
thumbnail = SlotThumbnailCapture.CapturePng();
}
}
catch (Exception ex)
{
Debug.LogError($"[SaveRestoreOrchestrator] 自动存档 Capture 失败: {ex}");
infoPanel?.HideSaveLoading();
_isAutoSaving = false;
yield break;
}
infoPanel?.HideSaveLoading();
if (TestSaveArchive.IsEnabled)
{
TestSaveArchive.Archive(snapshot, thumbnail);
Debug.Log("[SaveRestoreOrchestrator] 测试存档模式:已写入 testsavs(不覆盖 slot_0)。");
_isAutoSaving = false;
}
else
{
_ = SlotManager.SaveToAutoSlotAsync(snapshot, thumbnail).ContinueWith(
writeTask =>
{
if (writeTask.IsFaulted)
{
Debug.LogError(
$"[SaveRestoreOrchestrator] 自动存档写盘失败: {writeTask.Exception?.GetBaseException()}");
}
_isAutoSaving = false;
},
TaskContinuationOptions.ExecuteSynchronously);
}
if (omitAnchor || (snapshot?.anchor != null && string.IsNullOrEmpty(snapshot.anchor.nodeName)))
{
Debug.Log("[SaveRestoreOrchestrator] 已保存无 Yarn 节点 anchor 的状态。");
}
}
/// <summary>
/// Yarn <c>&lt;&lt;save&gt;&gt;</c> 显式存档:默认 omit anchor,调用方需已通过 <see cref="SavePointEvaluator.CanExplicitSave"/>。
/// </summary>
public static IEnumerator ExplicitSaveRoutine()
{
yield return AutoSaveRoutine(triggerNodeName: null, omitAnchor: true);
}
/// <summary>将当前自动档复制到指定手动档。</summary>
public static void CreateManualSlot(int slotIndex)
{
if (!SavePointEvaluator.CanManualSave(out var reason))
{
Debug.LogWarning($"[SaveRestoreOrchestrator] 当前不可手动存档:{reason}");
return;
}
SlotManager.CopyAutoToManual(slotIndex);
}
internal static RestorePreparation PrepareRestoreFromSlot(
int slotIndex,
TalkSceneGraphIndex talkSceneIndex,
RestoreOptions options)
{
var sourceLabel = $"slot_{slotIndex}";
SaveSnapshot snapshot;
try
{
snapshot = SlotManager.LoadSnapshot(slotIndex);
}
catch (Exception ex)
{
return FailedPreparation(sourceLabel, "LoadSnapshot", ex.Message, options);
}
if (snapshot == null)
{
return FailedPreparation(
sourceLabel,
"LoadSnapshot",
$"Slot {slotIndex} does not exist or could not be read.",
options);
}
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)
{
return FailedPreparation(savePath, "LoadSnapshot", ex.Message, options);
}
}
internal static IEnumerator ExecutePreparedRestore(
RestorePreparation preparation,
Func<bool> isCancellationRequested)
{
if (preparation?.Success != true)
{
yield break;
}
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)
{
_autoSaveSuppressDepth++;
Debug.Log($"[SaveRestoreOrchestrator] SuppressAutoSave begin: {reason}, depth={_autoSaveSuppressDepth}");
return new AutoSaveSuppressScope(reason);
}
private static RestorePreparation PrepareSnapshot(
SaveSnapshot snapshot,
string sourceLabel,
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
{
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.");
}
TalkSceneSO targetScene = null;
if (errors.Count == 0
&& !talkSceneIndex.TryFindByName(snapshot.anchor.sceneSoName, out targetScene, out var lookupError))
{
errors.Add(lookupError);
}
if (errors.Count == 0
&& (targetScene.yarnProject == null
|| !string.Equals(
targetScene.yarnProject.name,
snapshot.anchor.yarnProjectId,
StringComparison.Ordinal)))
{
errors.Add($"TalkSceneSO YarnProject mismatch: {snapshot.anchor.yarnProjectId}.");
}
if (errors.Count == 0
&& !string.IsNullOrWhiteSpace(snapshot.anchor.nodeName)
&& !Array.Exists(
targetScene.yarnProject.NodeNames,
node => string.Equals(node, snapshot.anchor.nodeName, StringComparison.Ordinal)))
{
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 RestorePreparation FailedPreparation(
string sourceLabel,
string phase,
string error,
RestoreOptions options)
{
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 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))
context.Error($"YarnProject mismatch: {runner?.YarnProject?.name ?? "none"}.");
if (!string.IsNullOrEmpty(snapshot.anchor.nodeName) && runner != null && !runner.IsDialogueRunning)
context.Error($"Yarn node did not start: {snapshot.anchor.nodeName}.");
if (snapshot.sections != null
&& snapshot.sections.ContainsKey(SnapshotProviderIds.Fix)
&& (FixSystem.FixSystemCenter.Instance == null
|| !FixSystem.FixSystemCenter.Instance.IsDirectorReady))
{
context.Error("FixSystem is not ready.");
}
}
private static void ResetRestoreLog(string sourceLabel)
{
_lastRestoreLog.Clear();
AddRestoreLog($"Restore source: {sourceLabel}");
}
private static void AddRestoreLog(string message)
{
_lastRestoreLog.Add($"[{DateTime.Now:HH:mm:ss}] {message}");
Debug.Log($"[SaveRestoreOrchestrator] {message}");
}
private sealed class AutoSaveSuppressScope : IDisposable
{
private readonly string _reason;
private bool _disposed;
public AutoSaveSuppressScope(string reason)
{
_reason = reason;
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
_autoSaveSuppressDepth = Math.Max(0, _autoSaveSuppressDepth - 1);
Debug.Log(
$"[SaveRestoreOrchestrator] SuppressAutoSave end: {_reason}, depth={_autoSaveSuppressDepth}");
}
}
}
}