842 lines
30 KiB
C#
842 lines
30 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
|
|
{
|
|
internal enum SaveTrigger
|
|
{
|
|
NodeEnter,
|
|
DialogueExit,
|
|
ExplicitCommand
|
|
}
|
|
|
|
internal readonly struct SaveRequest
|
|
{
|
|
public SaveTrigger Trigger { get; }
|
|
public SaveResumeMode ResumeMode { get; }
|
|
public string SourceNodeName { get; }
|
|
public string YarnProjectId { get; }
|
|
public string TalkSceneId { get; }
|
|
public long DialogueRunId { get; }
|
|
public long FlowRevision { get; }
|
|
|
|
public SaveRequest(
|
|
SaveTrigger trigger,
|
|
SaveResumeMode resumeMode,
|
|
string sourceNodeName,
|
|
string yarnProjectId,
|
|
string talkSceneId,
|
|
long dialogueRunId,
|
|
long flowRevision)
|
|
{
|
|
Trigger = trigger;
|
|
ResumeMode = resumeMode;
|
|
SourceNodeName = sourceNodeName;
|
|
YarnProjectId = yarnProjectId;
|
|
TalkSceneId = talkSceneId;
|
|
DialogueRunId = dialogueRunId;
|
|
FlowRevision = flowRevision;
|
|
}
|
|
|
|
public AnchorCaptureSpec AnchorSpec => new(
|
|
TalkSceneId,
|
|
YarnProjectId,
|
|
SourceNodeName,
|
|
ResumeMode == SaveResumeMode.RestartNode);
|
|
|
|
public static SaveRequest NodeEnter(
|
|
string nodeName,
|
|
string yarnProjectId,
|
|
string talkSceneId,
|
|
long dialogueRunId,
|
|
long flowRevision)
|
|
{
|
|
return new SaveRequest(
|
|
SaveTrigger.NodeEnter,
|
|
SaveResumeMode.RestartNode,
|
|
nodeName,
|
|
yarnProjectId,
|
|
talkSceneId,
|
|
dialogueRunId,
|
|
flowRevision);
|
|
}
|
|
|
|
public static SaveRequest DialogueExit(
|
|
string nodeName,
|
|
string yarnProjectId,
|
|
string talkSceneId,
|
|
long dialogueRunId,
|
|
long flowRevision)
|
|
{
|
|
return new SaveRequest(
|
|
SaveTrigger.DialogueExit,
|
|
SaveResumeMode.StateOnly,
|
|
nodeName,
|
|
yarnProjectId,
|
|
talkSceneId,
|
|
dialogueRunId,
|
|
flowRevision);
|
|
}
|
|
|
|
public static SaveRequest Explicit(
|
|
string nodeName,
|
|
string yarnProjectId,
|
|
string talkSceneId,
|
|
long dialogueRunId,
|
|
long flowRevision)
|
|
{
|
|
return new SaveRequest(
|
|
SaveTrigger.ExplicitCommand,
|
|
SaveResumeMode.StateOnly,
|
|
nodeName,
|
|
yarnProjectId,
|
|
talkSceneId,
|
|
dialogueRunId,
|
|
flowRevision);
|
|
}
|
|
}
|
|
|
|
internal readonly struct SerialTaskQueueEntry
|
|
{
|
|
public long Sequence { get; }
|
|
public Task Completion { get; }
|
|
|
|
public SerialTaskQueueEntry(long sequence, Task completion)
|
|
{
|
|
Sequence = sequence;
|
|
Completion = completion;
|
|
}
|
|
}
|
|
|
|
/// <summary>按请求顺序串行执行异步任务;前一个任务失败不会阻断后续任务。</summary>
|
|
internal sealed class SerialTaskQueue
|
|
{
|
|
private readonly object _gate = new();
|
|
private Task _tail = Task.CompletedTask;
|
|
private long _nextSequence;
|
|
private int _pendingCount;
|
|
|
|
public int PendingCount
|
|
{
|
|
get
|
|
{
|
|
lock (_gate)
|
|
{
|
|
return _pendingCount;
|
|
}
|
|
}
|
|
}
|
|
|
|
public SerialTaskQueueEntry Enqueue(Func<Task> operation)
|
|
{
|
|
if (operation == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(operation));
|
|
}
|
|
|
|
long sequence;
|
|
Task completion;
|
|
lock (_gate)
|
|
{
|
|
sequence = ++_nextSequence;
|
|
_pendingCount++;
|
|
completion = _tail.ContinueWith(
|
|
_ => operation(),
|
|
TaskScheduler.Default)
|
|
.Unwrap();
|
|
_tail = completion;
|
|
}
|
|
|
|
_ = completion.ContinueWith(
|
|
_ =>
|
|
{
|
|
lock (_gate)
|
|
{
|
|
_pendingCount = Math.Max(0, _pendingCount - 1);
|
|
}
|
|
},
|
|
TaskScheduler.Default);
|
|
|
|
return new SerialTaskQueueEntry(sequence, completion);
|
|
}
|
|
}
|
|
|
|
/// <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;
|
|
|
|
/// <summary>是否正在异步捕获或写入自动档;供只读诊断 UI 使用。</summary>
|
|
public static bool IsSaving
|
|
{
|
|
get
|
|
{
|
|
lock (SaveQueueGate)
|
|
{
|
|
return _activeCaptureCount > 0 || SaveWriteQueue.PendingCount > 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
public static IReadOnlyList<string> LastRestoreLog => _lastRestoreLog;
|
|
|
|
private static int _autoSaveSuppressDepth;
|
|
private static readonly List<string> _lastRestoreLog = new();
|
|
private static readonly object SaveQueueGate = new();
|
|
private static readonly SerialTaskQueue SaveWriteQueue = new();
|
|
private static int _activeCaptureCount;
|
|
|
|
/// <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 dialog = DialogController.Instance;
|
|
var (triggerNodeName, _) = dialog.GetCurrentNodeContext();
|
|
dialog.StartCoroutine(
|
|
SaveRoutine(SaveRequest.NodeEnter(
|
|
triggerNodeName,
|
|
dialog.DialogueRunner?.YarnProject?.name,
|
|
GetCurrentTalkSceneId(),
|
|
dialog.DialogueRunId,
|
|
dialog.DialogueFlowVersion)));
|
|
}
|
|
|
|
/// <summary>自动存档协程:settle 一帧 → 主线程 Capture → 后台序列化写盘(不阻塞主线程)。</summary>
|
|
public static IEnumerator AutoSaveRoutine(
|
|
string triggerNodeName,
|
|
string yarnProjectId,
|
|
string talkSceneId,
|
|
long dialogueRunId,
|
|
long flowRevision)
|
|
{
|
|
yield return SaveRoutine(SaveRequest.NodeEnter(
|
|
triggerNodeName,
|
|
yarnProjectId,
|
|
talkSceneId,
|
|
dialogueRunId,
|
|
flowRevision));
|
|
}
|
|
|
|
internal static IEnumerator DialogueExitSaveRoutine(
|
|
string sourceNodeName,
|
|
string yarnProjectId,
|
|
string talkSceneId,
|
|
long dialogueRunId,
|
|
long flowRevision)
|
|
{
|
|
yield return SaveRoutine(SaveRequest.DialogueExit(
|
|
sourceNodeName,
|
|
yarnProjectId,
|
|
talkSceneId,
|
|
dialogueRunId,
|
|
flowRevision));
|
|
}
|
|
|
|
private static IEnumerator SaveRoutine(SaveRequest request)
|
|
{
|
|
yield return null;
|
|
|
|
if (!ValidateSaveRequest(request, out var invalidReason))
|
|
{
|
|
Debug.LogWarning(
|
|
$"[SaveRestoreOrchestrator] 取消失效存档请求: trigger={request.Trigger}, " +
|
|
$"node={FormatNodeName(request.SourceNodeName)}, project={request.YarnProjectId ?? "(none)"}, " +
|
|
$"talkScene={request.TalkSceneId ?? "(none)"}, reason={invalidReason}");
|
|
yield break;
|
|
}
|
|
|
|
if (!SavePointEvaluator.CanExplicitSave(out var guardReason))
|
|
{
|
|
Debug.LogWarning(
|
|
$"[SaveRestoreOrchestrator] 取消存档请求: trigger={request.Trigger}, " +
|
|
$"node={FormatNodeName(request.SourceNodeName)}, reason={guardReason}");
|
|
yield break;
|
|
}
|
|
|
|
lock (SaveQueueGate)
|
|
{
|
|
_activeCaptureCount++;
|
|
}
|
|
|
|
var infoPanel = UIManager.Instance.GetPanel<InfoPanel>();
|
|
infoPanel?.ShowSaveLoading();
|
|
|
|
SaveSnapshot snapshot;
|
|
byte[] thumbnail;
|
|
|
|
try
|
|
{
|
|
using (new CodeTimer("SaveSnapshot"))
|
|
{
|
|
snapshot = SnapshotService.Capture(request.AnchorSpec);
|
|
thumbnail = SlotThumbnailCapture.CapturePng();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogError(
|
|
$"[SaveRestoreOrchestrator] 自动存档 Capture 失败: trigger={request.Trigger}, " +
|
|
$"node={FormatNodeName(request.SourceNodeName)}, error={ex}");
|
|
infoPanel?.HideSaveLoading();
|
|
lock (SaveQueueGate)
|
|
{
|
|
_activeCaptureCount = Math.Max(0, _activeCaptureCount - 1);
|
|
}
|
|
yield break;
|
|
}
|
|
|
|
infoPanel?.HideSaveLoading();
|
|
lock (SaveQueueGate)
|
|
{
|
|
_activeCaptureCount = Math.Max(0, _activeCaptureCount - 1);
|
|
}
|
|
|
|
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
|
TestSaveRecordRequest testSaveRequest = null;
|
|
try
|
|
{
|
|
testSaveRequest = TestSaveRecorder.CreateRequest(
|
|
snapshot,
|
|
thumbnail,
|
|
request.Trigger.ToString(),
|
|
request.ResumeMode.ToString());
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogError($"[SaveRestoreOrchestrator] 创建测试存档旁路请求失败,不影响正式存档:{ex}");
|
|
}
|
|
#endif
|
|
|
|
var sequence = EnqueueWrite(
|
|
snapshot,
|
|
thumbnail,
|
|
request
|
|
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
|
, testSaveRequest
|
|
#endif
|
|
);
|
|
|
|
if (request.ResumeMode == SaveResumeMode.StateOnly)
|
|
{
|
|
Debug.Log(
|
|
$"[SaveRestoreOrchestrator] 已捕获 state-only 自动档并加入写盘队列: " +
|
|
$"sequence={sequence}, trigger={request.Trigger}, node={FormatNodeName(request.SourceNodeName)}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Yarn <c><<save>></c> 显式存档:保留来源节点,但恢复时不启动 Yarn。
|
|
/// </summary>
|
|
public static IEnumerator ExplicitSaveRoutine()
|
|
{
|
|
var dialog = DialogController.Instance;
|
|
var (nodeName, _) = dialog != null
|
|
? dialog.GetCurrentNodeContext()
|
|
: (null, null);
|
|
var projectId = dialog?.DialogueRunner?.YarnProject?.name;
|
|
yield return SaveRoutine(SaveRequest.Explicit(
|
|
nodeName,
|
|
projectId,
|
|
GetCurrentTalkSceneId(),
|
|
dialog?.DialogueRunId ?? 0,
|
|
dialog?.DialogueFlowVersion ?? 0));
|
|
}
|
|
|
|
private static long EnqueueWrite(
|
|
SaveSnapshot snapshot,
|
|
byte[] thumbnail,
|
|
SaveRequest request
|
|
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
|
, TestSaveRecordRequest testSaveRequest
|
|
#endif
|
|
)
|
|
{
|
|
var queueEntry = SaveWriteQueue.Enqueue(
|
|
() => SlotManager.SaveToAutoSlotAsync(snapshot, thumbnail));
|
|
var sequence = queueEntry.Sequence;
|
|
var queuedTask = queueEntry.Completion;
|
|
|
|
_ = queuedTask.ContinueWith(
|
|
writeTask =>
|
|
{
|
|
if (writeTask.IsFaulted)
|
|
{
|
|
Debug.LogError(
|
|
$"[SaveRestoreOrchestrator] 自动存档写盘失败: sequence={sequence}, " +
|
|
$"trigger={request.Trigger}, node={FormatNodeName(request.SourceNodeName)}, " +
|
|
$"error={writeTask.Exception?.GetBaseException()}");
|
|
}
|
|
else if (writeTask.IsCanceled)
|
|
{
|
|
Debug.LogWarning(
|
|
$"[SaveRestoreOrchestrator] 自动存档写盘已取消: sequence={sequence}, " +
|
|
$"trigger={request.Trigger}, node={FormatNodeName(request.SourceNodeName)}");
|
|
}
|
|
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
|
else
|
|
{
|
|
TestSaveRecorder.Enqueue(testSaveRequest);
|
|
}
|
|
#endif
|
|
},
|
|
TaskScheduler.Default);
|
|
|
|
return sequence;
|
|
}
|
|
|
|
private static bool ValidateSaveRequest(SaveRequest request, out string reason)
|
|
{
|
|
if (!request.AnchorSpec.IsComplete)
|
|
{
|
|
reason = "anchor identity is incomplete";
|
|
return false;
|
|
}
|
|
|
|
var dialog = DialogController.Instance;
|
|
var runner = dialog?.DialogueRunner;
|
|
if (dialog == null || runner == null)
|
|
{
|
|
reason = "DialogController or DialogueRunner is unavailable";
|
|
return false;
|
|
}
|
|
|
|
if (dialog.DialogueRunId != request.DialogueRunId)
|
|
{
|
|
reason = $"dialogue run changed ({request.DialogueRunId} -> {dialog.DialogueRunId})";
|
|
return false;
|
|
}
|
|
|
|
if (dialog.DialogueFlowVersion != request.FlowRevision)
|
|
{
|
|
reason = $"flow revision changed ({request.FlowRevision} -> {dialog.DialogueFlowVersion})";
|
|
return false;
|
|
}
|
|
|
|
if (!string.Equals(runner.YarnProject?.name, request.YarnProjectId, StringComparison.Ordinal))
|
|
{
|
|
reason = $"YarnProject changed to {runner.YarnProject?.name ?? "(none)"}";
|
|
return false;
|
|
}
|
|
|
|
var currentTalkSceneId = GetCurrentTalkSceneId();
|
|
if (!string.Equals(currentTalkSceneId, request.TalkSceneId, StringComparison.Ordinal))
|
|
{
|
|
reason = $"TalkScene changed to {currentTalkSceneId ?? "(none)"}";
|
|
return false;
|
|
}
|
|
|
|
if (SceneLoader.Instance == null || SceneLoader.Instance.IsLoading)
|
|
{
|
|
reason = "scene is unavailable or loading";
|
|
return false;
|
|
}
|
|
|
|
var (currentNodeName, _) = dialog.GetCurrentNodeContext();
|
|
if (request.Trigger == SaveTrigger.DialogueExit)
|
|
{
|
|
if (runner.IsDialogueRunning)
|
|
{
|
|
reason = "DialogueRunner is still running";
|
|
return false;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(currentNodeName))
|
|
{
|
|
reason = $"a current Yarn node is still present: {currentNodeName}";
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!runner.IsDialogueRunning)
|
|
{
|
|
reason = "DialogueRunner stopped before capture";
|
|
return false;
|
|
}
|
|
|
|
if (!string.Equals(currentNodeName, request.SourceNodeName, StringComparison.Ordinal))
|
|
{
|
|
reason = $"current node changed to {currentNodeName ?? "(none)"}";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
reason = null;
|
|
return true;
|
|
}
|
|
|
|
private static string GetCurrentTalkSceneId()
|
|
{
|
|
return GameManager.Instance != null
|
|
? GameManager.Session.CurrentTalkScene?.name
|
|
: null;
|
|
}
|
|
|
|
private static string FormatNodeName(string nodeName)
|
|
{
|
|
return string.IsNullOrEmpty(nodeName) ? "(无节点)" : nodeName;
|
|
}
|
|
|
|
/// <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);
|
|
}
|
|
|
|
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
|
internal static RestorePreparation PrepareRestoreFromTestSave(
|
|
TestSaveEntry entry,
|
|
TalkSceneGraphIndex talkSceneIndex,
|
|
RestoreOptions options)
|
|
{
|
|
var sourceLabel = $"test:{entry?.Meta?.sceneSoName ?? "unknown"}/{entry?.NodeName ?? "unknown"}";
|
|
if (!TestSaveRecorder.Repository.TryLoad(entry, out var snapshot, out var error))
|
|
{
|
|
return FailedPreparation(sourceLabel, "LoadTestSnapshot", error, options);
|
|
}
|
|
|
|
return PrepareSnapshot(snapshot, sourceLabel, talkSceneIndex, options);
|
|
}
|
|
#endif
|
|
|
|
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.");
|
|
if (string.IsNullOrWhiteSpace(snapshot.anchor?.nodeName))
|
|
errors.Add("Snapshot Yarn node 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
|
|
&& snapshot.anchor.startDialogueOnRestore
|
|
&& !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 (snapshot.anchor.startDialogueOnRestore)
|
|
{
|
|
var (currentNodeName, _) = DialogController.Instance != null
|
|
? DialogController.Instance.GetCurrentNodeContext()
|
|
: (null, null);
|
|
if (runner == null || !runner.IsDialogueRunning)
|
|
context.Error($"Yarn node did not start: {snapshot.anchor.nodeName}.");
|
|
else if (!string.Equals(currentNodeName, snapshot.anchor.nodeName, StringComparison.Ordinal))
|
|
context.Error(
|
|
$"Yarn node mismatch: expected {snapshot.anchor.nodeName}, " +
|
|
$"actual {currentNodeName ?? "none"}.");
|
|
}
|
|
else if (runner != null && runner.IsDialogueRunning)
|
|
{
|
|
context.Error(
|
|
$"StateOnly restore unexpectedly started Yarn node {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}");
|
|
}
|
|
}
|
|
}
|
|
}
|