test(save): 添加存档系统编辑器验证工具
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d4c5a2b82ac1ca459e228aaad3255c6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 存档系统 P1/P2/P3 存盘行为验证工具。
|
||||
/// 不验证读档(P4),不验证复杂 tags 边界(P3 后续)。
|
||||
/// </summary>
|
||||
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<SaveSystemValidationWindow>(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<string>())}\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<string>
|
||||
{
|
||||
$"=== {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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9719ab6f265683c4484a170f6c4dc843
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user