From 43c3f8531d028483bd4575873110f3a2a43e5983 Mon Sep 17 00:00:00 2001
From: Ding Yuntian <1491671119@qq.com>
Date: Wed, 24 Jun 2026 22:45:11 +0800
Subject: [PATCH] =?UTF-8?q?feat(editor):=20=E6=96=B0=E5=A2=9E=20SaveSystem?=
=?UTF-8?q?=20=E6=B5=8B=E8=AF=95=E7=AA=97=E5=8F=A3=E5=B9=B6=E6=9B=B4?=
=?UTF-8?q?=E6=96=B0=E9=AA=8C=E8=AF=81=E5=B7=A5=E5=85=B7=E8=AF=BB=E6=A1=A3?=
=?UTF-8?q?=E5=85=A5=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../SaveSystemTestWindow.cs | 667 ++++++++++++++++++
.../SaveSystemTestWindow.cs.meta | 11 +
.../SaveSystemValidationWindow.cs | 24 +-
3 files changed, 699 insertions(+), 3 deletions(-)
create mode 100644 Assets/Editor/SaveSystemValidation/SaveSystemTestWindow.cs
create mode 100644 Assets/Editor/SaveSystemValidation/SaveSystemTestWindow.cs.meta
diff --git a/Assets/Editor/SaveSystemValidation/SaveSystemTestWindow.cs b/Assets/Editor/SaveSystemValidation/SaveSystemTestWindow.cs
new file mode 100644
index 000000000..902ecaae7
--- /dev/null
+++ b/Assets/Editor/SaveSystemValidation/SaveSystemTestWindow.cs
@@ -0,0 +1,667 @@
+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.StartWithSlot(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.StartWithSaveFile(entry.SnapshotPath);
+ 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();
+ }
+ }
+}
diff --git a/Assets/Editor/SaveSystemValidation/SaveSystemTestWindow.cs.meta b/Assets/Editor/SaveSystemValidation/SaveSystemTestWindow.cs.meta
new file mode 100644
index 000000000..8e6df22af
--- /dev/null
+++ b/Assets/Editor/SaveSystemValidation/SaveSystemTestWindow.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 27c49345ec3a4e849af3f9da7045f7a3
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Editor/SaveSystemValidation/SaveSystemValidationWindow.cs b/Assets/Editor/SaveSystemValidation/SaveSystemValidationWindow.cs
index 968e37814..e9c4e5683 100644
--- a/Assets/Editor/SaveSystemValidation/SaveSystemValidationWindow.cs
+++ b/Assets/Editor/SaveSystemValidation/SaveSystemValidationWindow.cs
@@ -586,7 +586,7 @@ namespace AibisDream.SaveSystem.Editor
return;
}
- GameManager.Instance.StartCoroutine(SaveRestoreOrchestrator.RestoreFromSlot(slotIndex));
+ GameManager.Instance.StartWithSlot(slotIndex);
Log($"已启动 Restore slot_{slotIndex}。请观察 Restore Pipeline Log 与场景状态。");
}
@@ -622,13 +622,31 @@ namespace AibisDream.SaveSystem.Editor
$"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
};
+ if (snapshot.sections != null)
+ {
+ AppendFixSectionHint(lines, snapshot, SnapshotProviderIds.PunchTape);
+ AppendFixSectionHint(lines, snapshot, SnapshotProviderIds.Fix);
+ AppendFixSectionHint(lines, snapshot, SnapshotProviderIds.FixPanel);
+ AppendFixSectionHint(lines, snapshot, SnapshotProviderIds.BodyModule);
+ AppendFixSectionHint(lines, snapshot, SnapshotProviderIds.Eye);
+ }
+
+ lines.Add("JSON preview (first 1500 chars):");
+ lines.Add(json.Length > 1500 ? json.Substring(0, 1500) + "..." : json);
+
Log(string.Join("\n", lines));
}
+ private static void AppendFixSectionHint(List lines, SaveSnapshot snapshot, string sectionId)
+ {
+ if (snapshot.sections.ContainsKey(sectionId))
+ {
+ lines.Add($" [fix-related] {sectionId}: captured");
+ }
+ }
+
private void Log(string message)
{
_lastLog = $"[{DateTime.Now:HH:mm:ss}] {message}";