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 { /// /// 存读档临时测试工具:触发存档、从槽位/测试归档读档、浏览 testsavs 历史。 /// 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 _archiveEntries = new(); private List _archiveEntriesSnapshot = new(); private double _lastArchiveRefreshTime; private bool _repaintQueued; private List _slotOverviewLabels = new(); private List _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(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(); } /// /// 在 Layout/Repaint 之前快照可变列表,避免同一 GUI 帧内控件数量不一致。 /// 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 a, List 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(); } } }