Files
aibis-dream/Assets/Editor/SaveSystemValidation/SaveSystemTestWindow.cs
T

670 lines
21 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.IO;
using AibisDream;
using AibisDream.SaveSystem;
using AibisDream.Utility;
using UnityEditor;
using UnityEngine;
namespace AibisDream.SaveSystem.Editor
{
/// <summary>
/// 存读档临时测试工具:触发存档、从槽位/测试归档读档、浏览 testsavs 历史。
/// </summary>
public class SaveSystemTestWindow : EditorWindow
{
private const string MenuPath = "AIBIS/存档测试工具";
private const string WindowTitle = "SaveSystem Test";
private Vector2 _scrollPosition;
private string _lastLog = string.Empty;
private int _restoreSlotIndex;
private int _manualSlotIndex = 1;
private bool _testArchiveEnabled;
private string _selectedArchiveEntryId;
private Texture2D _selectedThumbnail;
private List<TestSaveArchiveEntry> _archiveEntries = new();
private List<TestSaveArchiveEntry> _archiveEntriesSnapshot = new();
private double _lastArchiveRefreshTime;
private bool _repaintQueued;
private List<string> _slotOverviewLabels = new();
private List<string> _restoreLogSnapshot = new();
private string _displaySelectedArchiveEntryId;
private Texture2D _displayThumbnail;
private void RequestRepaint()
{
if (_repaintQueued)
{
return;
}
_repaintQueued = true;
EditorApplication.delayCall += () =>
{
_repaintQueued = false;
if (this != null)
{
Repaint();
}
};
}
[MenuItem(MenuPath)]
public static void Open()
{
var window = GetWindow<SaveSystemTestWindow>(WindowTitle);
window.minSize = new Vector2(480, 560);
}
private void OnEnable()
{
_testArchiveEnabled = EditorPrefs.GetBool(TestSaveArchive.EditorPrefsEnabledKey, true);
RefreshArchiveEntries(requestRepaint: false);
EditorApplication.update += OnEditorUpdate;
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
}
private void OnDisable()
{
EditorApplication.update -= OnEditorUpdate;
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
ReleaseThumbnail();
}
private void OnEditorUpdate()
{
if (!Application.isPlaying)
{
return;
}
if (EditorApplication.timeSinceStartup - _lastArchiveRefreshTime < 1.0)
{
return;
}
RefreshArchiveEntries(requestRepaint: true);
}
private void OnPlayModeStateChanged(PlayModeStateChange state)
{
if (state == PlayModeStateChange.EnteredPlayMode || state == PlayModeStateChange.ExitingPlayMode)
{
RefreshArchiveEntries();
}
}
private void OnGUI()
{
if (Event.current.type == EventType.Layout)
{
SnapshotDynamicGuiData();
}
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
DrawHeader();
GUILayout.Space(8);
DrawTestArchiveSettings();
GUILayout.Space(8);
DrawSaveActions();
GUILayout.Space(8);
DrawSlotRestoreSection();
GUILayout.Space(8);
DrawTestArchiveSection();
GUILayout.Space(8);
DrawRestoreLogSection();
GUILayout.Space(8);
DrawUtilitySection();
GUILayout.Space(8);
DrawLogSection();
EditorGUILayout.EndScrollView();
}
/// <summary>
/// 在 Layout/Repaint 之前快照可变列表,避免同一 GUI 帧内控件数量不一致。
/// </summary>
private void SnapshotDynamicGuiData()
{
_displaySelectedArchiveEntryId = _selectedArchiveEntryId;
_displayThumbnail = _selectedThumbnail;
_archiveEntriesSnapshot.Clear();
for (var i = 0; i < _archiveEntries.Count; i++)
{
_archiveEntriesSnapshot.Add(_archiveEntries[i]);
}
_slotOverviewLabels.Clear();
foreach (var vm in SlotManager.GetSlotViewModels())
{
if (vm.IsEmpty)
{
continue;
}
var meta = SlotManager.LoadMeta(vm.SlotIndex);
_slotOverviewLabels.Add(
$"slot_{vm.SlotIndex}" +
(vm.IsAutoSlot ? " [自动]" : "") +
$" | {vm.SavedAt} | {meta?.nodeName ?? ""} | {vm.SceneName}");
}
_restoreLogSnapshot.Clear();
var restoreLog = SaveRestoreOrchestrator.LastRestoreLog;
for (var i = 0; i < restoreLog.Count; i++)
{
_restoreLogSnapshot.Add(restoreLog[i]);
}
}
private void DrawHeader()
{
EditorGUILayout.LabelField("存读档测试工具", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"用于 Play Mode 下验证自动存档、槽位读档与 testsavs 测试归档。\n" +
"正式存档目录: demo_saves/ | 测试归档: testsavs/(每次自动存档独立子文件夹)",
MessageType.Info);
if (!Application.isPlaying)
{
EditorGUILayout.HelpBox("存档/读档操作需要在 Play Mode 下执行。", MessageType.Warning);
}
EditorGUILayout.LabelField(
$"状态: IsRestoring={SaveRestoreOrchestrator.IsRestoring}, " +
$"SuppressAutoSave={SaveRestoreOrchestrator.IsAutoSaveSuppressed}");
}
private void DrawTestArchiveSettings()
{
EditorGUILayout.LabelField("测试存档模式", EditorStyles.boldLabel);
EditorGUI.BeginChangeCheck();
_testArchiveEnabled = EditorGUILayout.Toggle("启用测试存档模式", _testArchiveEnabled);
if (EditorGUI.EndChangeCheck())
{
EditorPrefs.SetBool(TestSaveArchive.EditorPrefsEnabledKey, _testArchiveEnabled);
}
EditorGUILayout.LabelField($"testsavs 路径: {ConstRef.TestAutoSaveArchivePath}", EditorStyles.miniLabel);
EditorGUILayout.HelpBox(
"开启后:每次 AutoSave 仅在 testsavs 下新建一份独立存档,不写入、不覆盖 demo_saves/slot_0。\n" +
"存多少次就保留多少份;关闭后恢复正式行为(仅覆盖 slot_0)。",
MessageType.None);
}
private void DrawSaveActions()
{
EditorGUILayout.LabelField("存档", EditorStyles.boldLabel);
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("触发自动存档 (完整流程)"))
{
TriggerAutoSave();
}
if (GUILayout.Button("仅 Capture (不写盘)"))
{
CaptureInMemory();
}
}
if (GUILayout.Button("手动档: 复制 slot_0 → 上方指定槽位"))
{
CopyAutoToManual();
}
}
private void DrawSlotRestoreSection()
{
EditorGUILayout.LabelField("从正式槽位读档", EditorStyles.boldLabel);
EditorGUILayout.LabelField($"demo_saves 路径: {ConstRef.SaveFilePath}", EditorStyles.miniLabel);
var latest = SlotManager.GetLatestSlotIndex();
EditorGUILayout.LabelField($"最近槽位: {(latest.HasValue ? $"slot_{latest.Value}" : "")}");
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("读档: Latest"))
{
RestoreLatestSlot();
}
if (GUILayout.Button("读档: slot_0"))
{
RestoreSlot(SlotIndex.Auto);
}
}
_restoreSlotIndex = EditorGUILayout.IntSlider("槽位", _restoreSlotIndex, 0, SlotIndex.ManualEnd);
if (GUILayout.Button($"读档: slot_{_restoreSlotIndex}"))
{
RestoreSlot(_restoreSlotIndex);
}
GUILayout.Space(4);
_manualSlotIndex = EditorGUILayout.IntSlider(
"复制到手动槽位",
_manualSlotIndex,
SlotIndex.ManualStart,
SlotIndex.ManualEnd);
DrawSlotBriefOverview();
}
private void DrawSlotBriefOverview()
{
foreach (var label in _slotOverviewLabels)
{
EditorGUILayout.LabelField(label, EditorStyles.miniLabel);
}
}
private void DrawTestArchiveSection()
{
EditorGUILayout.LabelField("testsavs 测试归档", EditorStyles.boldLabel);
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("刷新列表"))
{
RefreshArchiveEntries();
}
if (GUILayout.Button("清空 testsavs"))
{
ClearTestArchives();
}
if (GUILayout.Button("打开 testsavs 文件夹"))
{
OpenDirectory(ConstRef.TestAutoSaveArchivePath);
}
}
EditorGUILayout.LabelField($"共 {_archiveEntriesSnapshot.Count} 条", EditorStyles.miniLabel);
if (_archiveEntriesSnapshot.Count == 0)
{
EditorGUILayout.HelpBox(
"暂无测试归档。开启「testsavs 独立归档」后触发自动存档,或从正式 slot_0 手动归档。",
MessageType.None);
}
else
{
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
foreach (var entry in _archiveEntriesSnapshot)
{
DrawArchiveEntry(entry);
}
}
}
if (GUILayout.Button("将当前 slot_0 手动归档到 testsavs"))
{
ArchiveCurrentAutoSlot();
}
}
private void DrawArchiveEntry(TestSaveArchiveEntry entry)
{
var isSelected = _displaySelectedArchiveEntryId == entry.EntryId;
var boxStyle = isSelected ? EditorStyles.helpBox : EditorStyles.textArea;
using (new EditorGUILayout.VerticalScope(boxStyle))
{
EditorGUILayout.LabelField(entry.EntryId, EditorStyles.boldLabel);
EditorGUILayout.LabelField(
$"时间: {entry.Meta?.savedAt ?? ""} | 节点: {entry.Meta?.nodeName ?? ""}",
EditorStyles.miniLabel);
EditorGUILayout.LabelField(
$"场景: {entry.Meta?.sceneName ?? ""} | SO: {entry.Meta?.sceneSoName ?? ""}",
EditorStyles.miniLabel);
if (isSelected)
{
var maxWidth = EditorGUIUtility.currentViewWidth - 48f;
var height = 120f;
if (_displayThumbnail != null)
{
height = Mathf.Min(120f, _displayThumbnail.height * maxWidth / _displayThumbnail.width);
GUILayout.Label(_displayThumbnail, GUILayout.Width(maxWidth), GUILayout.Height(height));
}
else
{
EditorGUILayout.LabelField(
"(无缩略图)",
EditorStyles.miniLabel,
GUILayout.Width(maxWidth),
GUILayout.Height(height));
}
}
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("选中"))
{
SelectArchiveEntry(entry);
}
if (GUILayout.Button("读档"))
{
RestoreFromArchive(entry);
}
if (GUILayout.Button("删除"))
{
DeleteArchiveEntry(entry);
}
}
}
GUILayout.Space(4);
}
private void DrawRestoreLogSection()
{
EditorGUILayout.LabelField("读档 Pipeline 日志", EditorStyles.boldLabel);
if (_restoreLogSnapshot.Count == 0)
{
EditorGUILayout.LabelField("(暂无)", EditorStyles.miniLabel);
return;
}
foreach (var line in _restoreLogSnapshot)
{
EditorGUILayout.LabelField(line, EditorStyles.wordWrappedMiniLabel);
}
}
private void DrawUtilitySection()
{
EditorGUILayout.LabelField("工具", EditorStyles.boldLabel);
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("打开 demo_saves 文件夹"))
{
OpenDirectory(ConstRef.SaveFilePath);
}
if (GUILayout.Button("打开存档验证工具"))
{
SaveSystemValidationWindow.Open();
}
}
}
private void DrawLogSection()
{
EditorGUILayout.LabelField("操作结果", EditorStyles.boldLabel);
EditorGUILayout.SelectableLabel(_lastLog, EditorStyles.textArea, GUILayout.MinHeight(80));
}
private void RefreshArchiveEntries(bool requestRepaint = true)
{
var newEntries = TestSaveArchive.ListEntries();
var entriesChanged = !ArchiveEntriesEqual(newEntries, _archiveEntries);
_archiveEntries = newEntries;
_lastArchiveRefreshTime = EditorApplication.timeSinceStartup;
if (!string.IsNullOrEmpty(_selectedArchiveEntryId) &&
!_archiveEntries.Exists(e => e.EntryId == _selectedArchiveEntryId))
{
_selectedArchiveEntryId = null;
ReleaseThumbnail();
entriesChanged = true;
}
if (requestRepaint && entriesChanged)
{
RequestRepaint();
}
}
private static bool ArchiveEntriesEqual(List<TestSaveArchiveEntry> a, List<TestSaveArchiveEntry> b)
{
if (a.Count != b.Count)
{
return false;
}
for (var i = 0; i < a.Count; i++)
{
if (a[i].EntryId != b[i].EntryId)
{
return false;
}
}
return true;
}
private void SelectArchiveEntry(TestSaveArchiveEntry entry)
{
_selectedArchiveEntryId = entry.EntryId;
ReleaseThumbnail();
if (!string.IsNullOrEmpty(entry.ThumbnailPath) && File.Exists(entry.ThumbnailPath))
{
var bytes = File.ReadAllBytes(entry.ThumbnailPath);
_selectedThumbnail = new Texture2D(2, 2);
_selectedThumbnail.LoadImage(bytes);
}
RequestRepaint();
}
private void ReleaseThumbnail()
{
if (_selectedThumbnail != null)
{
DestroyImmediate(_selectedThumbnail);
_selectedThumbnail = null;
}
}
private void TriggerAutoSave()
{
if (!Application.isPlaying)
{
Log("需要在 Play Mode 下执行。");
return;
}
try
{
if (!SavePointEvaluator.CanAutoSave(out var reason))
{
Log($"CanAutoSave 拒绝: {reason}");
return;
}
var routine = SaveRestoreOrchestrator.AutoSaveRoutine();
while (routine.MoveNext())
{
}
RefreshArchiveEntries();
Log(TestSaveArchive.IsEnabled
? "测试存档模式:已写入 testsavs(未覆盖 slot_0)。"
: "正式模式:已写入 slot_0。");
}
catch (Exception ex)
{
Log($"自动存档失败: {ex}");
}
}
private void CaptureInMemory()
{
try
{
if (YarnVariableStorage.Instance == null)
{
Log("YarnVariableStorage 未初始化。");
return;
}
var snapshot = SnapshotService.Capture();
Log($"Capture 成功: 场景={snapshot.scene?.sceneName}, 节点={snapshot.anchor?.nodeName}");
}
catch (Exception ex)
{
Log($"Capture 失败: {ex.Message}");
}
}
private void CopyAutoToManual()
{
if (!SlotDirectory.Exists(SlotIndex.Auto))
{
Log("slot_0 不存在。");
return;
}
SlotManager.CopyAutoToManual(_manualSlotIndex);
Log($"已复制到 slot_{_manualSlotIndex}。");
}
private void RestoreLatestSlot()
{
var latest = SlotManager.GetLatestSlotIndex();
if (!latest.HasValue)
{
Log("没有 latest_slot。");
return;
}
RestoreSlot(latest.Value);
}
private void RestoreSlot(int slotIndex)
{
if (!Application.isPlaying)
{
Log("读档需要在 Play Mode 下执行。");
return;
}
if (GameManager.Instance == null)
{
Log("GameManager 未初始化。");
return;
}
if (!SlotDirectory.Exists(slotIndex))
{
Log($"slot_{slotIndex} 不存在。");
return;
}
GameManager.Instance.TryRestoreSlot(slotIndex);
Log($"已启动读档 slot_{slotIndex}。");
}
private void RestoreFromArchive(TestSaveArchiveEntry entry)
{
if (!Application.isPlaying)
{
Log("读档需要在 Play Mode 下执行。");
return;
}
if (GameManager.Instance == null)
{
Log("GameManager 未初始化。");
return;
}
GameManager.Instance.TryRestoreFile(
entry.SnapshotPath,
RestoreOptions.DevJump);
Log($"已启动读档: {entry.EntryId}");
}
private void ArchiveCurrentAutoSlot()
{
var snapshot = SlotManager.LoadSnapshot(SlotIndex.Auto);
if (snapshot == null)
{
Log("slot_0 无快照。");
return;
}
var thumbnail = SlotManager.LoadThumbnail(SlotIndex.Auto);
var entryId = TestSaveArchive.Archive(snapshot, thumbnail);
RefreshArchiveEntries();
Log($"已手动归档: {entryId}");
}
private void DeleteArchiveEntry(TestSaveArchiveEntry entry)
{
var entryId = entry.EntryId;
EditorApplication.delayCall += () => ConfirmDeleteArchiveEntry(entryId);
}
private void ConfirmDeleteArchiveEntry(string entryId)
{
if (!EditorUtility.DisplayDialog("删除测试归档", $"删除 {entryId}", "删除", "取消"))
{
return;
}
TestSaveArchive.DeleteEntry(entryId);
if (_selectedArchiveEntryId == entryId)
{
_selectedArchiveEntryId = null;
ReleaseThumbnail();
}
RefreshArchiveEntries();
Log($"已删除: {entryId}");
}
private void ClearTestArchives()
{
EditorApplication.delayCall += ConfirmClearTestArchives;
}
private void ConfirmClearTestArchives()
{
if (!EditorUtility.DisplayDialog("清空 testsavs", "删除所有测试归档?不可撤销。", "删除", "取消"))
{
return;
}
TestSaveArchive.ClearAll();
_selectedArchiveEntryId = null;
ReleaseThumbnail();
RefreshArchiveEntries();
Log("已清空 testsavs。");
}
private static void OpenDirectory(string path)
{
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
EditorUtility.RevealInFinder(path);
}
private void Log(string message)
{
_lastLog = $"[{DateTime.Now:HH:mm:ss}] {message}";
Debug.Log($"[SaveSystemTest] {message}");
RequestRepaint();
}
}
}