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; } } /// 按请求顺序串行执行异步任务;前一个任务失败不会阻断后续任务。 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 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); } } /// /// 存读档流程编排层:连接 UI、 与槽位系统。 /// public static class SaveRestoreOrchestrator { /// 是否正在读档还原中;供 UI / 日志 / 外部系统观察。 public static bool IsRestoring { get; private set; } /// 是否处于自动存档抑制作用域;真正用于防止读档期间覆盖 slot_0。 public static bool IsAutoSaveSuppressed => _autoSaveSuppressDepth > 0; /// 是否正在异步捕获或写入自动档;供只读诊断 UI 使用。 public static bool IsSaving { get { lock (SaveQueueGate) { return _activeCaptureCount > 0 || SaveWriteQueue.PendingCount > 0; } } } public static IReadOnlyList LastRestoreLog => _lastRestoreLog; private static int _autoSaveSuppressDepth; private static readonly List _lastRestoreLog = new(); private static readonly object SaveQueueGate = new(); private static readonly SerialTaskQueue SaveWriteQueue = new(); private static int _activeCaptureCount; /// 启动自动存档协程(手动/调试入口)。 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))); } /// 自动存档协程:settle 一帧 → 主线程 Capture → 后台序列化写盘(不阻塞主线程)。 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?.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)}"); } } /// /// Yarn <<save>> 显式存档:保留来源节点,但恢复时不启动 Yarn。 /// 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 (!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(); return ValidateDialogueTiming( request, dialog.DialogueRunId, dialog.DialogueFlowVersion, currentNodeName, runner.IsDialogueRunning, out reason); } internal static bool ValidateDialogueTiming( SaveRequest request, long currentDialogueRunId, long currentFlowRevision, string currentNodeName, bool isDialogueRunning, out string reason) { if (request.Trigger != SaveTrigger.DialogueExit) { reason = null; return true; } if (currentDialogueRunId != request.DialogueRunId) { reason = $"dialogue run changed ({request.DialogueRunId} -> {currentDialogueRunId})"; return false; } if (currentFlowRevision != request.FlowRevision) { reason = $"flow revision changed ({request.FlowRevision} -> {currentFlowRevision})"; return false; } if (isDialogueRunning) { reason = "DialogueRunner is still running"; return false; } if (!string.IsNullOrEmpty(currentNodeName)) { reason = $"a current Yarn node is still present: {currentNodeName}"; 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; } /// 将当前自动档复制到指定手动档。 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 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(context.Errors); result.Warnings = new List(context.Warnings); result.Log = new List(_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(); 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 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.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}"); } } } }