670 lines
21 KiB
C#
670 lines
21 KiB
C#
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" +
|
||
"正式存档目录: 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 下新建一份独立存档,不写入、不覆盖 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($"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("打开 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();
|
||
}
|
||
}
|
||
}
|