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 } }