405 lines
14 KiB
C#
405 lines
14 KiB
C#
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
|
|
}
|
|
}
|