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: