using System; using System.Collections.Generic; using System.IO; using System.Linq; using AibisDream; using AibisDream.SaveSystem; using AibisDream.Utility; using Newtonsoft.Json; using UnityEditor; using UnityEngine; namespace AibisDream.SaveSystem.Editor { /// /// 存档系统 P1/P2/P3 存盘行为验证工具。 /// 不验证读档(P4),不验证复杂 tags 边界(P3 后续)。 /// public class SaveSystemValidationWindow : EditorWindow { private const string MenuPath = "AIBIS/存档系统验证工具"; private const string WindowTitle = "SaveSystem Validator"; private const float SaveHistoryBoxMinHeight = 400f; private Vector2 _scrollPosition; private string _lastLog = string.Empty; private int _manualSlotIndex = 1; private int _deleteSlotIndex = 1; private string _testNodeName = "SomeNode"; private string _testTags = ""; private int _restoreSlotIndex; private readonly List _saveHistory = new(); private string _lastObservedSavedAt; /// 运行时自动存档历史条目,仅内存缓存。 private class SaveHistoryEntry { public string SavedAt; public string NodeName; public string SceneName; } [MenuItem(MenuPath)] public static void Open() { var window = GetWindow(WindowTitle); window.minSize = new Vector2(420, 640); } private void OnEnable() { EditorApplication.update += OnEditorUpdate; EditorApplication.playModeStateChanged += OnPlayModeStateChanged; InitializeLastObservedSavedAt(); } private void OnDisable() { EditorApplication.update -= OnEditorUpdate; EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; } private void OnPlayModeStateChanged(PlayModeStateChange state) { if (state == PlayModeStateChange.EnteredPlayMode) { _saveHistory.Clear(); InitializeLastObservedSavedAt(); TrySeedHistoryFromCurrentAutoSlot(); Repaint(); return; } if (state == PlayModeStateChange.ExitingPlayMode) { _saveHistory.Clear(); _lastObservedSavedAt = null; } } private void TrySeedHistoryFromCurrentAutoSlot() { var meta = SlotManager.LoadMeta(SlotIndex.Auto); if (meta == null) { return; } _saveHistory.Add(CreateHistoryEntry(meta)); _lastObservedSavedAt = meta.savedAt; } private static SaveHistoryEntry CreateHistoryEntry(SlotMeta meta) { return new SaveHistoryEntry { SavedAt = meta.savedAt, NodeName = string.IsNullOrEmpty(meta.nodeName) ? "(无节点)" : meta.nodeName, SceneName = meta.sceneName }; } private void InitializeLastObservedSavedAt() { var meta = SlotManager.LoadMeta(SlotIndex.Auto); _lastObservedSavedAt = meta?.savedAt; } /// /// 监听 slot_0 的 meta 变化,运行时每次自动存档都会记录到内存列表中。 /// 纯 Editor 行为,不侵入运行时代码。 /// private void OnEditorUpdate() { if (!Application.isPlaying) { return; } var meta = SlotManager.LoadMeta(SlotIndex.Auto); if (meta == null) { return; } if (!string.IsNullOrEmpty(_lastObservedSavedAt) && _lastObservedSavedAt == meta.savedAt) { return; } _lastObservedSavedAt = meta.savedAt; _saveHistory.Add(CreateHistoryEntry(meta)); Repaint(); } private void OnGUI() { _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition); DrawHeader(); DrawSlotOverview(); GUILayout.Space(12); DrawSaveHistorySection(); GUILayout.Space(12); DrawDirectCaptureSection(); GUILayout.Space(12); DrawAutoSaveSimulationSection(); GUILayout.Space(12); DrawManualSlotSection(); GUILayout.Space(12); DrawRestoreSection(); GUILayout.Space(12); DrawSavePointEvaluationSection(); GUILayout.Space(12); DrawUtilitySection(); GUILayout.Space(12); DrawLogSection(); EditorGUILayout.EndScrollView(); } #region Drawing private void DrawHeader() { EditorGUILayout.LabelField("存档系统存盘验证", EditorStyles.boldLabel); EditorGUILayout.HelpBox( "本工具仅验证 P1/P2/P3 的存盘行为:快照捕获、槽位落盘、元数据、可存点判定框架。\n" + "不验证读档(P4)和复杂 tags 边界(P3 后续)。\n" + "涉及运行时单例(DialogController / GameManager)的操作需要在 Play Mode 下执行。", MessageType.Info); if (!Application.isPlaying) { EditorGUILayout.HelpBox("当前不在 Play Mode。Capture / TryAutoSave 等依赖运行时单例的操作可能失败。", MessageType.Warning); } } private void DrawSlotOverview() { EditorGUILayout.LabelField("槽位概览", EditorStyles.boldLabel); var latest = SlotManager.GetLatestSlotIndex(); EditorGUILayout.LabelField($"最近槽位: {(latest.HasValue ? latest.Value.ToString() : "无")}"); EditorGUILayout.LabelField($"存档根目录: {ConstRef.SaveFilePath}"); EditorGUILayout.LabelField( $"读档状态: IsRestoring={SaveRestoreOrchestrator.IsRestoring}, " + $"SuppressAutoSave={SaveRestoreOrchestrator.IsAutoSaveSuppressed}"); var viewModels = SlotManager.GetSlotViewModels(); foreach (var vm in viewModels) { DrawSlotViewModel(vm, latest); } } private void DrawSlotViewModel(SlotViewModel vm, int? latest) { var isLatest = latest.HasValue && latest.Value == vm.SlotIndex; var nodeName = SlotManager.LoadMeta(vm.SlotIndex)?.nodeName; var label = vm.IsEmpty ? $"[{vm.SlotIndex}] (空)" : $"[{vm.SlotIndex}] {(vm.IsAutoSlot ? "[自动档]" : "")} {(isLatest ? "[最新]" : "")}\n" + $" 场景: {vm.SceneName}\n" + $" SO: {vm.SceneSoName}\n" + $" 节点: {nodeName}\n" + $" 时间: {vm.SavedAt}\n" + $" 缩略图: {(string.IsNullOrEmpty(vm.ThumbnailPath) ? "无" : Path.GetFileName(vm.ThumbnailPath))}"; EditorGUILayout.HelpBox(label, vm.IsEmpty ? MessageType.None : MessageType.Info); } private void DrawDirectCaptureSection() { EditorGUILayout.LabelField("直接捕获快照", EditorStyles.boldLabel); EditorGUILayout.HelpBox( "调用 SnapshotService.Capture() 组装快照,并在内存中检查结构。\n" + "此操作不写盘,不经过 SavePointEvaluator。", MessageType.None); if (GUILayout.Button("Capture Snapshot (内存)")) { CaptureSnapshotInMemory(); } } private void DrawAutoSaveSimulationSection() { EditorGUILayout.LabelField("模拟自动存档", EditorStyles.boldLabel); EditorGUILayout.HelpBox( "调用 SaveRestoreOrchestrator.TryAutoSave(),走完整的判定+捕获+落盘流程。\n" + "需要在 Play Mode 下且有 DialogController 运行时实例。", MessageType.None); if (GUILayout.Button("TryAutoSave (完整流程)")) { TryAutoSaveFullFlow(); } } private void DrawSaveHistorySection() { EditorGUILayout.LabelField("自动存档历史 (运行时)", EditorStyles.boldLabel); using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox, GUILayout.MinHeight(SaveHistoryBoxMinHeight))) { if (!Application.isPlaying) { EditorGUILayout.HelpBox( "进入 Play Mode 后,每次 slot_0 自动存档都会在此记录节点名与时间。\n" + "数据仅保存在当前窗口内存中,退出 Play Mode 后清空。", MessageType.Info); } else if (_saveHistory.Count == 0) { EditorGUILayout.LabelField("等待自动存档…", EditorStyles.wordWrappedLabel); EditorGUILayout.LabelField( "触发 content / hub / linear 节点,或点击下方 TryAutoSave。", EditorStyles.miniLabel); } else { EditorGUILayout.LabelField($"共 {_saveHistory.Count} 条记录", EditorStyles.miniLabel); for (var i = 0; i < _saveHistory.Count; i++) { var entry = _saveHistory[i]; EditorGUILayout.LabelField( $"{i + 1}. [{entry.SavedAt}] {entry.NodeName}", EditorStyles.wordWrappedLabel); EditorGUILayout.LabelField( $" 场景: {entry.SceneName ?? "N/A"}", EditorStyles.miniLabel); } GUILayout.FlexibleSpace(); if (GUILayout.Button("清空历史记录")) { _saveHistory.Clear(); } } } } private void DrawManualSlotSection() { EditorGUILayout.LabelField("手动档操作", EditorStyles.boldLabel); _manualSlotIndex = EditorGUILayout.IntSlider("复制到槽位", _manualSlotIndex, 1, SlotIndex.ManualEnd); if (GUILayout.Button("Copy Auto → Manual")) { CopyAutoToManual(_manualSlotIndex); } GUILayout.Space(8); _deleteSlotIndex = EditorGUILayout.IntSlider("删除槽位", _deleteSlotIndex, 0, SlotIndex.ManualEnd); if (GUILayout.Button("Delete Slot")) { DeleteSlot(_deleteSlotIndex); } GUILayout.Space(8); if (GUILayout.Button("Clear All Saves")) { ClearAllSaves(); } } private void DrawSavePointEvaluationSection() { EditorGUILayout.LabelField("可存点判定框架检查", EditorStyles.boldLabel); EditorGUILayout.HelpBox( "手动设置节点名和 tags,测试 SavePointEvaluator.CanAutoSave 的判定结果。\n" + "不涉及真实 DialogController,因此 IsRestoring/GamePaused 等状态仍按实际运行时取值。", MessageType.None); _testNodeName = EditorGUILayout.TextField("节点名", _testNodeName); _testTags = EditorGUILayout.TextField("Tags (逗号分隔)", _testTags); if (GUILayout.Button("Evaluate CanAutoSave")) { EvaluateSavePoint(); } } private void DrawRestoreSection() { EditorGUILayout.LabelField("读档验证 (P4)", EditorStyles.boldLabel); EditorGUILayout.HelpBox( "触发 SaveRestoreOrchestrator.RestoreFromSlot,验证 Phase + Barrier 编排、自动存档抑制与 Provider 还原日志。\n" + "需要在 Play Mode 下执行;不代表正式玩家 UI。", MessageType.None); using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("Restore Latest")) { RestoreLatestSlot(); } if (GUILayout.Button("Restore slot_0")) { RestoreSlot(SlotIndex.Auto); } } _restoreSlotIndex = EditorGUILayout.IntSlider("Restore 槽位", _restoreSlotIndex, 0, SlotIndex.ManualEnd); if (GUILayout.Button($"Restore slot_{_restoreSlotIndex}")) { RestoreSlot(_restoreSlotIndex); } var restoreLog = SaveRestoreOrchestrator.LastRestoreLog; if (restoreLog.Count > 0) { EditorGUILayout.LabelField("Restore Pipeline Log", EditorStyles.boldLabel); foreach (var line in restoreLog) { EditorGUILayout.LabelField(line, EditorStyles.wordWrappedMiniLabel); } } } private void DrawUtilitySection() { EditorGUILayout.LabelField("工具", EditorStyles.boldLabel); if (GUILayout.Button("打开存档目录")) { OpenSaveDirectory(); } if (GUILayout.Button("刷新")) { Repaint(); Log("已刷新。"); } } private void DrawLogSection() { GUILayout.Space(12); EditorGUILayout.LabelField("最后结果", EditorStyles.boldLabel); EditorGUILayout.SelectableLabel(_lastLog, EditorStyles.textArea, GUILayout.MinHeight(120)); } #endregion #region Actions private void CaptureSnapshotInMemory() { try { var storage = YarnVariableStorage.Instance; if (storage == null) { Log("YarnVariableStorage 未初始化,尝试直接 new SaveSnapshot 仅做结构演示。"); var demo = new SaveSnapshot { gameVersion = Application.version, savedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), scene = new SceneSnapshotDto { sceneName = "DemoScene" }, anchor = new AnchorSnapshot { nodeName = "DemoNode" }, yarnVariables = new YarnVariablesSnapshot() }; PrintSnapshotSummary(demo, "演示快照(无运行时)"); return; } var snapshot = SnapshotService.Capture(); PrintSnapshotSummary(snapshot, "内存快照"); } catch (Exception ex) { Log($"Capture 失败: {ex}"); } } private void TryAutoSaveFullFlow() { if (!Application.isPlaying) { Log("TryAutoSave 需要在 Play Mode 下执行。"); return; } try { var beforeLatest = SlotManager.GetLatestSlotIndex(); var beforeSnapshot = SlotManager.LoadSnapshot(0); if (!SavePointEvaluator.CanAutoSave(out var rejectReason)) { Log($"CanAutoSave 拒绝: {rejectReason}"); return; } var routine = SaveRestoreOrchestrator.AutoSaveRoutine(); while (routine.MoveNext()) { } var afterSnapshot = SlotManager.LoadSnapshot(0); var afterLatest = SlotManager.GetLatestSlotIndex(); var summary = string.Empty; if (afterSnapshot == null) { summary = "无快照写入(可能被 SavePointEvaluator 拒绝)。"; } else { summary = $"落盘成功。\n" + $"场景: {afterSnapshot.scene?.sceneName}\n" + $"节点: {afterSnapshot.anchor?.nodeName}\n" + $"SO: {afterSnapshot.anchor?.sceneSoName}\n" + $"YarnProject: {afterSnapshot.anchor?.yarnProjectId}\n" + $"Sections: {string.Join(", ", afterSnapshot.sections?.Keys ?? Enumerable.Empty())}\n" + $"latest_slot: {(afterLatest.HasValue ? afterLatest.Value.ToString() : "无")}"; } Log(summary); } catch (Exception ex) { Log($"TryAutoSave 失败: {ex}"); } } private void CopyAutoToManual(int slotIndex) { try { if (!SlotDirectory.Exists(SlotIndex.Auto)) { Log("自动档不存在,无法复制手动档。"); return; } SlotManager.CopyAutoToManual(slotIndex); Log($"已复制自动档到 slot_{slotIndex}。"); } catch (Exception ex) { Log($"复制失败: {ex.Message}"); } } private void DeleteSlot(int slotIndex) { try { SlotManager.DeleteSlot(slotIndex); Log($"已删除/清空 slot_{slotIndex}。"); } catch (Exception ex) { Log($"删除失败: {ex.Message}"); } } private void ClearAllSaves() { if (!EditorUtility.DisplayDialog("确认", "删除所有存档槽位?此操作不可撤销。", "删除", "取消")) { return; } try { for (var i = SlotIndex.Auto; i <= SlotIndex.ManualEnd; i++) { SlotManager.DeleteSlot(i); } var latestPath = SlotDirectory.GetLatestSlotIndexPath(); if (File.Exists(latestPath)) { File.Delete(latestPath); } Log("已清空所有槽位和 latest_slot 记录。"); } catch (Exception ex) { Log($"清空失败: {ex.Message}"); } } private void EvaluateSavePoint() { if (!Application.isPlaying) { Log("SavePointEvaluator 需要运行时状态(IsRestoring / GameManager pause)。请在 Play Mode 下执行。"); return; } try { var result = SavePointEvaluator.CanAutoSave(out var reason); Log($"CanAutoSave = {result}, Reason = {reason}\n" + $"(当前 IsRestoring = {SaveRestoreOrchestrator.IsRestoring}, " + $"GameManager.pause = {(GameManager.Instance != null ? GameManager.Instance.state.isInPause.ToString() : "N/A")})"); } catch (Exception ex) { Log($"判定失败: {ex.Message}"); } } private void RestoreLatestSlot() { var latest = SlotManager.GetLatestSlotIndex(); if (!latest.HasValue) { Log("没有 latest_slot,无法读档。"); return; } RestoreSlot(latest.Value); } private void RestoreSlot(int slotIndex) { if (!Application.isPlaying) { Log("Restore 需要在 Play Mode 下执行。"); return; } if (GameManager.Instance == null) { Log("GameManager 未初始化,无法启动读档协程。"); return; } if (!SlotDirectory.Exists(slotIndex)) { Log($"slot_{slotIndex} 不存在,无法读档。"); return; } GameManager.Instance.StartWithSlot(slotIndex); Log($"已启动 Restore slot_{slotIndex}。请观察 Restore Pipeline Log 与场景状态。"); } private void OpenSaveDirectory() { var path = ConstRef.SaveFilePath; if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } EditorUtility.RevealInFinder(path); } #endregion #region Helpers private void PrintSnapshotSummary(SaveSnapshot snapshot, string label) { var json = SnapshotPersistence.Serialize(snapshot); var lines = new List { $"=== {label} ===", $"gameVersion: {snapshot.gameVersion}", $"savedAt: {snapshot.savedAt}", $"schemaVersion: {snapshot.schemaVersion}", $"scene: {snapshot.scene?.sceneName}", $"anchor.sceneSoName: {snapshot.anchor?.sceneSoName}", $"anchor.yarnProjectId: {snapshot.anchor?.yarnProjectId}", $"anchor.nodeName: {snapshot.anchor?.nodeName}", $"yarnVariables.floats: {snapshot.yarnVariables?.floats?.Count ?? 0} entries", $"yarnVariables.strings: {snapshot.yarnVariables?.strings?.Count ?? 0} entries", $"yarnVariables.bools: {snapshot.yarnVariables?.bools?.Count ?? 0} entries", $"sections: {(snapshot.sections != null ? string.Join(", ", snapshot.sections.Keys) : "null")}", }; if (snapshot.sections != null) { AppendFixSectionHint(lines, snapshot, SnapshotProviderIds.PunchTape); AppendFixSectionHint(lines, snapshot, SnapshotProviderIds.Fix); AppendFixSectionHint(lines, snapshot, SnapshotProviderIds.FixPanel); AppendFixSectionHint(lines, snapshot, SnapshotProviderIds.BodyModule); AppendFixSectionHint(lines, snapshot, SnapshotProviderIds.Eye); } lines.Add("JSON preview (first 1500 chars):"); lines.Add(json.Length > 1500 ? json.Substring(0, 1500) + "..." : json); Log(string.Join("\n", lines)); } private static void AppendFixSectionHint(List lines, SaveSnapshot snapshot, string sectionId) { if (snapshot.sections.ContainsKey(sectionId)) { lines.Add($" [fix-related] {sectionId}: captured"); } } private void Log(string message) { _lastLog = $"[{DateTime.Now:HH:mm:ss}] {message}"; Debug.Log($"[SaveSystemValidator] {message}"); Repaint(); } #endregion } }