diff --git a/Assets/Editor/SaveSystemValidation.meta b/Assets/Editor/SaveSystemValidation.meta new file mode 100644 index 000000000..2bc8e86e8 --- /dev/null +++ b/Assets/Editor/SaveSystemValidation.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8d4c5a2b82ac1ca459e228aaad3255c6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/SaveSystemValidation/SaveSystemValidationWindow.cs b/Assets/Editor/SaveSystemValidation/SaveSystemValidationWindow.cs new file mode 100644 index 000000000..2e7cefdcc --- /dev/null +++ b/Assets/Editor/SaveSystemValidation/SaveSystemValidationWindow.cs @@ -0,0 +1,404 @@ +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 Vector2 _scrollPosition; + private string _lastLog = string.Empty; + private int _manualSlotIndex = 1; + private int _deleteSlotIndex = 1; + private string _testNodeName = "SomeNode"; + private string _testTags = ""; + + [MenuItem(MenuPath)] + public static void Open() + { + GetWindow(WindowTitle); + } + + private void OnGUI() + { + _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition); + + DrawHeader(); + DrawSlotOverview(); + GUILayout.Space(12); + DrawDirectCaptureSection(); + GUILayout.Space(12); + DrawAutoSaveSimulationSection(); + GUILayout.Space(12); + DrawManualSlotSection(); + 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}"); + + 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 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 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); + + SaveRestoreOrchestrator.TryAutoSave(); + + 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 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")}", + "JSON preview (first 1500 chars):", + json.Length > 1500 ? json.Substring(0, 1500) + "..." : json + }; + + Log(string.Join("\n", lines)); + } + + private void Log(string message) + { + _lastLog = $"[{DateTime.Now:HH:mm:ss}] {message}"; + Debug.Log($"[SaveSystemValidator] {message}"); + Repaint(); + } + + #endregion + } +} diff --git a/Assets/Editor/SaveSystemValidation/SaveSystemValidationWindow.cs.meta b/Assets/Editor/SaveSystemValidation/SaveSystemValidationWindow.cs.meta new file mode 100644 index 000000000..c912867a3 --- /dev/null +++ b/Assets/Editor/SaveSystemValidation/SaveSystemValidationWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9719ab6f265683c4484a170f6c4dc843 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: