660 lines
23 KiB
C#
660 lines
23 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 const float SaveHistoryBoxMinHeight = 400f;
|
|
|
|
private Vector2 _scrollPosition;
|
|
private string _lastLog = string.Empty;
|
|
private int _manualSlotIndex = 1;
|
|
private int _deleteSlotIndex = 1;
|
|
private string _testNodeName = "SomeNode";
|
|
private string _testTags = "";
|
|
private int _restoreSlotIndex;
|
|
|
|
private readonly List<SaveHistoryEntry> _saveHistory = new();
|
|
private string _lastObservedSavedAt;
|
|
|
|
/// <summary>运行时自动存档历史条目,仅内存缓存。</summary>
|
|
private class SaveHistoryEntry
|
|
{
|
|
public string SavedAt;
|
|
public string NodeName;
|
|
public string SceneName;
|
|
}
|
|
|
|
[MenuItem(MenuPath)]
|
|
public static void Open()
|
|
{
|
|
var window = GetWindow<SaveSystemValidationWindow>(WindowTitle);
|
|
window.minSize = new Vector2(420, 640);
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
EditorApplication.update += OnEditorUpdate;
|
|
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
|
|
InitializeLastObservedSavedAt();
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
EditorApplication.update -= OnEditorUpdate;
|
|
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
|
|
}
|
|
|
|
private void OnPlayModeStateChanged(PlayModeStateChange state)
|
|
{
|
|
if (state == PlayModeStateChange.EnteredPlayMode)
|
|
{
|
|
_saveHistory.Clear();
|
|
InitializeLastObservedSavedAt();
|
|
TrySeedHistoryFromCurrentAutoSlot();
|
|
Repaint();
|
|
return;
|
|
}
|
|
|
|
if (state == PlayModeStateChange.ExitingPlayMode)
|
|
{
|
|
_saveHistory.Clear();
|
|
_lastObservedSavedAt = null;
|
|
}
|
|
}
|
|
|
|
private void TrySeedHistoryFromCurrentAutoSlot()
|
|
{
|
|
var meta = SlotManager.LoadMeta(SlotIndex.Auto);
|
|
if (meta == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_saveHistory.Add(CreateHistoryEntry(meta));
|
|
_lastObservedSavedAt = meta.savedAt;
|
|
}
|
|
|
|
private static SaveHistoryEntry CreateHistoryEntry(SlotMeta meta)
|
|
{
|
|
return new SaveHistoryEntry
|
|
{
|
|
SavedAt = meta.savedAt,
|
|
NodeName = string.IsNullOrEmpty(meta.nodeName) ? "(无节点)" : meta.nodeName,
|
|
SceneName = meta.sceneName
|
|
};
|
|
}
|
|
|
|
private void InitializeLastObservedSavedAt()
|
|
{
|
|
var meta = SlotManager.LoadMeta(SlotIndex.Auto);
|
|
_lastObservedSavedAt = meta?.savedAt;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 监听 slot_0 的 meta 变化,运行时每次自动存档都会记录到内存列表中。
|
|
/// 纯 Editor 行为,不侵入运行时代码。
|
|
/// </summary>
|
|
private void OnEditorUpdate()
|
|
{
|
|
if (!Application.isPlaying)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var meta = SlotManager.LoadMeta(SlotIndex.Auto);
|
|
if (meta == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(_lastObservedSavedAt) && _lastObservedSavedAt == meta.savedAt)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_lastObservedSavedAt = meta.savedAt;
|
|
_saveHistory.Add(CreateHistoryEntry(meta));
|
|
|
|
Repaint();
|
|
}
|
|
|
|
private void OnGUI()
|
|
{
|
|
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
|
|
|
|
DrawHeader();
|
|
DrawSlotOverview();
|
|
GUILayout.Space(12);
|
|
DrawSaveHistorySection();
|
|
GUILayout.Space(12);
|
|
DrawDirectCaptureSection();
|
|
GUILayout.Space(12);
|
|
DrawAutoSaveSimulationSection();
|
|
GUILayout.Space(12);
|
|
DrawManualSlotSection();
|
|
GUILayout.Space(12);
|
|
DrawRestoreSection();
|
|
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}");
|
|
EditorGUILayout.LabelField(
|
|
$"读档状态: IsRestoring={SaveRestoreOrchestrator.IsRestoring}, " +
|
|
$"SuppressAutoSave={SaveRestoreOrchestrator.IsAutoSaveSuppressed}");
|
|
|
|
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 DrawSaveHistorySection()
|
|
{
|
|
EditorGUILayout.LabelField("自动存档历史 (运行时)", EditorStyles.boldLabel);
|
|
|
|
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox, GUILayout.MinHeight(SaveHistoryBoxMinHeight)))
|
|
{
|
|
if (!Application.isPlaying)
|
|
{
|
|
EditorGUILayout.HelpBox(
|
|
"进入 Play Mode 后,每次 slot_0 自动存档都会在此记录节点名与时间。\n" +
|
|
"数据仅保存在当前窗口内存中,退出 Play Mode 后清空。",
|
|
MessageType.Info);
|
|
}
|
|
else if (_saveHistory.Count == 0)
|
|
{
|
|
EditorGUILayout.LabelField("等待自动存档…", EditorStyles.wordWrappedLabel);
|
|
EditorGUILayout.LabelField(
|
|
"触发 content / hub / linear 节点,或点击下方 TryAutoSave。",
|
|
EditorStyles.miniLabel);
|
|
}
|
|
else
|
|
{
|
|
EditorGUILayout.LabelField($"共 {_saveHistory.Count} 条记录", EditorStyles.miniLabel);
|
|
|
|
for (var i = 0; i < _saveHistory.Count; i++)
|
|
{
|
|
var entry = _saveHistory[i];
|
|
EditorGUILayout.LabelField(
|
|
$"{i + 1}. [{entry.SavedAt}] {entry.NodeName}",
|
|
EditorStyles.wordWrappedLabel);
|
|
EditorGUILayout.LabelField(
|
|
$" 场景: {entry.SceneName ?? "N/A"}",
|
|
EditorStyles.miniLabel);
|
|
}
|
|
|
|
GUILayout.FlexibleSpace();
|
|
|
|
if (GUILayout.Button("清空历史记录"))
|
|
{
|
|
_saveHistory.Clear();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 DrawRestoreSection()
|
|
{
|
|
EditorGUILayout.LabelField("读档验证 (P4)", EditorStyles.boldLabel);
|
|
EditorGUILayout.HelpBox(
|
|
"触发 SaveRestoreOrchestrator.RestoreFromSlot,验证 Phase + Barrier 编排、自动存档抑制与 Provider 还原日志。\n" +
|
|
"需要在 Play Mode 下执行;不代表正式玩家 UI。",
|
|
MessageType.None);
|
|
|
|
using (new EditorGUILayout.HorizontalScope())
|
|
{
|
|
if (GUILayout.Button("Restore Latest"))
|
|
{
|
|
RestoreLatestSlot();
|
|
}
|
|
|
|
if (GUILayout.Button("Restore slot_0"))
|
|
{
|
|
RestoreSlot(SlotIndex.Auto);
|
|
}
|
|
}
|
|
|
|
_restoreSlotIndex = EditorGUILayout.IntSlider("Restore 槽位", _restoreSlotIndex, 0, SlotIndex.ManualEnd);
|
|
if (GUILayout.Button($"Restore slot_{_restoreSlotIndex}"))
|
|
{
|
|
RestoreSlot(_restoreSlotIndex);
|
|
}
|
|
|
|
var restoreLog = SaveRestoreOrchestrator.LastRestoreLog;
|
|
if (restoreLog.Count > 0)
|
|
{
|
|
EditorGUILayout.LabelField("Restore Pipeline Log", EditorStyles.boldLabel);
|
|
foreach (var line in restoreLog)
|
|
{
|
|
EditorGUILayout.LabelField(line, EditorStyles.wordWrappedMiniLabel);
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
if (!SavePointEvaluator.CanAutoSave(out var rejectReason))
|
|
{
|
|
Log($"CanAutoSave 拒绝: {rejectReason}");
|
|
return;
|
|
}
|
|
|
|
var routine = SaveRestoreOrchestrator.AutoSaveRoutine();
|
|
while (routine.MoveNext())
|
|
{
|
|
}
|
|
|
|
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 RestoreLatestSlot()
|
|
{
|
|
var latest = SlotManager.GetLatestSlotIndex();
|
|
if (!latest.HasValue)
|
|
{
|
|
Log("没有 latest_slot,无法读档。");
|
|
return;
|
|
}
|
|
|
|
RestoreSlot(latest.Value);
|
|
}
|
|
|
|
private void RestoreSlot(int slotIndex)
|
|
{
|
|
if (!Application.isPlaying)
|
|
{
|
|
Log("Restore 需要在 Play Mode 下执行。");
|
|
return;
|
|
}
|
|
|
|
if (GameManager.Instance == null)
|
|
{
|
|
Log("GameManager 未初始化,无法启动读档协程。");
|
|
return;
|
|
}
|
|
|
|
if (!SlotDirectory.Exists(slotIndex))
|
|
{
|
|
Log($"slot_{slotIndex} 不存在,无法读档。");
|
|
return;
|
|
}
|
|
|
|
GameManager.Instance.StartWithSlot(slotIndex);
|
|
Log($"已启动 Restore slot_{slotIndex}。请观察 Restore Pipeline Log 与场景状态。");
|
|
}
|
|
|
|
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")}",
|
|
};
|
|
|
|
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<string> 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}";
|
|
Debug.Log($"[SaveSystemValidator] {message}");
|
|
Repaint();
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|