using System; using System.Collections; using System.Collections.Generic; using System.Threading.Tasks; using AibisDream.Framework; using AibisDream.UI; using UnityEngine; namespace AibisDream.SaveSystem { /// /// 存读档流程编排层:连接 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 => _isAutoSaving; public static IReadOnlyList LastRestoreLog => _lastRestoreLog; private static bool _isAutoSaving; private static int _autoSaveSuppressDepth; private static readonly List _lastRestoreLog = new(); /// 启动自动存档协程(手动/调试入口)。 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)); } /// 自动存档协程:settle 一帧 → 主线程 Capture → 后台序列化写盘(不阻塞主线程)。 /// ;显式 save 传 null。 /// 为 true 时不写入 Yarn 节点 anchor。 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?.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(); _ = 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 的状态。"); } } /// /// Yarn <<save>> 显式存档:默认 omit anchor,调用方需已通过 。 /// public static IEnumerator ExplicitSaveRoutine() { yield return AutoSaveRoutine(triggerNodeName: null, omitAnchor: true); } /// 将当前自动档复制到指定手动档。 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 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."); } 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 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}"); } } } }