Files
aibis-dream/Assets/Scripts/Utility/DevSaveJumpTool.cs
T

468 lines
15 KiB
C#

#if UNITY_EDITOR || DEVELOPMENT_BUILD
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AibisDream.SaveSystem;
using AibisDream.Utility;
using Newtonsoft.Json;
using UnityEngine;
namespace AibisDream
{
/// <summary>
/// 开发用通用跳转工具:从 <see cref="ConstRef.TestSaveFilePath"/>(可提交)读档。
/// 目录约定:StreamingAssets/TestSaveFiles/{SectionId}/{EntryFolder}/snapshot.json
/// </summary>
public class DevSaveJumpTool : MonoBehaviour
{
private const float UiMargin = 16f;
private const float DevButtonWidth = 96f;
private const float DevButtonHeight = 34f;
private const float MenuWidth = 320f;
private const float MaxMenuHeight = 560f;
private const float RowHeight = 30f;
private const float HeaderHeight = 24f;
private const float StatusWidth = 360f;
private const float StatusHeight = 28f;
private static DevSaveJumpTool instance;
private bool isJumping;
private bool menuOpen;
private string status = "";
private float statusVisibleUntil;
private Vector2 menuScroll;
private GUIStyle buttonStyle;
private GUIStyle headerStyle;
private GUIStyle menuBoxStyle;
private GUIStyle statusStyle;
private List<DevSaveSection> sections = new();
private float cachedContentHeight;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
private static void Bootstrap()
{
if (instance != null)
return;
var existing = FindObjectOfType<DevSaveJumpTool>();
if (existing != null)
{
instance = existing;
DontDestroyOnLoad(existing.gameObject);
return;
}
var toolObject = new GameObject("[Dev] Save Jump Tool");
DontDestroyOnLoad(toolObject);
instance = toolObject.AddComponent<DevSaveJumpTool>();
}
private void Awake()
{
ReloadCatalog();
}
private void OnGUI()
{
EnsureStyles();
float buttonX = Screen.width - UiMargin - DevButtonWidth;
float buttonY = Screen.height - UiMargin - DevButtonHeight;
var devButtonRect = new Rect(buttonX, buttonY, DevButtonWidth, DevButtonHeight);
if (menuOpen)
DrawCommandMenu(devButtonRect);
string label = isJumping ? "Busy..." : "Dev";
if (GUI.Button(devButtonRect, label, buttonStyle))
{
if (!menuOpen)
ReloadCatalog();
menuOpen = !menuOpen;
}
if (!string.IsNullOrEmpty(status) && Time.unscaledTime < statusVisibleUntil)
{
float menuHeight = GetMenuHeight();
float statusY = menuOpen
? buttonY - menuHeight - StatusHeight - UiMargin
: buttonY - StatusHeight - 8f;
var statusRect = new Rect(
Screen.width - UiMargin - StatusWidth,
Mathf.Max(UiMargin, statusY),
StatusWidth,
StatusHeight);
GUI.Label(statusRect, status, statusStyle);
}
}
private void DrawCommandMenu(Rect devButtonRect)
{
float menuHeight = GetMenuHeight();
float menuX = Screen.width - UiMargin - MenuWidth;
float menuY = Mathf.Max(UiMargin, devButtonRect.y - menuHeight - 8f);
var menuRect = new Rect(menuX, menuY, MenuWidth, menuHeight);
GUI.Box(menuRect, GUIContent.none, menuBoxStyle);
var viewRect = new Rect(0f, 0f, MenuWidth - 28f, GetContentHeight());
var scrollRect = new Rect(
menuRect.x + 6f,
menuRect.y + 6f,
menuRect.width - 12f,
menuRect.height - 12f);
menuScroll = GUI.BeginScrollView(scrollRect, menuScroll, viewRect);
float y = 0f;
if (sections.Count == 0)
{
GUI.Label(new Rect(0f, y, MenuWidth - 32f, RowHeight), "No TestSaveFiles found", headerStyle);
}
else
{
for (int i = 0; i < sections.Count; i++)
{
var section = sections[i];
DrawHeader(ref y, section.Title);
if (section.Entries.Count == 0)
{
GUI.Label(
new Rect(0f, y, MenuWidth - 32f, RowHeight),
"(empty — promote saves first)",
statusStyle);
y += RowHeight;
}
else
{
DrawSaveEntries(ref y, section.Entries);
}
if (i < sections.Count - 1)
y += 6f;
}
}
GUI.EndScrollView();
}
private void DrawHeader(ref float y, string label)
{
GUI.Label(new Rect(0f, y, MenuWidth - 32f, HeaderHeight), label, headerStyle);
y += HeaderHeight;
}
private void DrawSaveEntries(ref float y, List<DevSaveEntry> entries)
{
foreach (var entry in entries)
{
var rect = new Rect(0f, y, MenuWidth - 32f, RowHeight);
var captured = entry;
DrawCommandButton(rect, captured.Label, !isJumping, () => StartCoroutine(LoadDevSave(captured)));
y += RowHeight;
}
}
private float GetMenuHeight()
{
return Mathf.Min(MaxMenuHeight, GetContentHeight() + 12f);
}
private float GetContentHeight()
{
if (cachedContentHeight > 0f)
return cachedContentHeight;
float height = 0f;
if (sections.Count == 0)
{
height = RowHeight;
}
else
{
for (int i = 0; i < sections.Count; i++)
{
height += HeaderHeight;
int rows = Math.Max(1, sections[i].Entries.Count);
height += rows * RowHeight;
if (i < sections.Count - 1)
height += 6f;
}
}
cachedContentHeight = height;
return height;
}
private void DrawCommandButton(Rect rect, string label, bool enabled, Action action)
{
GUI.enabled = enabled;
if (GUI.Button(rect, label, buttonStyle))
{
menuOpen = false;
action?.Invoke();
}
GUI.enabled = true;
}
private IEnumerator LoadDevSave(DevSaveEntry entry)
{
if (isJumping)
yield break;
string savePath = entry.SnapshotPathWithoutExtension;
if (string.IsNullOrEmpty(savePath) || !File.Exists(savePath + ".json"))
{
SetStatus($"Save missing: {entry.Label}", 5f);
yield break;
}
isJumping = true;
if (NeedsResetToMainMenu())
{
SetStatus($"Resetting: {entry.Label}");
GameManager.Instance.QuitGame();
yield return WaitForMainMenuReady();
}
SetStatus($"Loading: {entry.Label}");
yield return SaveRestoreOrchestrator.RestoreFromFile(savePath);
SetStatus($"Loaded: {entry.Label}", 3f);
isJumping = false;
}
private static bool NeedsResetToMainMenu()
{
var gameManager = GameManager.Instance;
if (gameManager == null)
return false;
if (gameManager.state.isInGame)
return true;
var sceneLoader = SceneLoader.Instance;
return sceneLoader != null && !string.IsNullOrEmpty(sceneLoader.CurrentSceneName);
}
private static IEnumerator WaitForMainMenuReady()
{
const float timeoutSeconds = 15f;
float elapsed = 0f;
while (elapsed < timeoutSeconds)
{
var gameManager = GameManager.Instance;
var sceneLoader = SceneLoader.Instance;
bool sceneCleared = sceneLoader == null || string.IsNullOrEmpty(sceneLoader.CurrentSceneName);
bool notInGame = gameManager == null || !gameManager.state.isInGame;
if (sceneCleared && notInGame)
yield break;
elapsed += Time.unscaledDeltaTime;
yield return null;
}
Debug.LogWarning("[DevSaveJumpTool] 等待主界面超时,继续读档。");
}
private void ReloadCatalog()
{
sections = DevSaveCatalog.Load(ConstRef.TestSaveFilePath);
cachedContentHeight = 0f;
}
private void SetStatus(string message, float visibleSeconds = 30f)
{
status = message;
statusVisibleUntil = Time.unscaledTime + visibleSeconds;
Debug.Log($"[DevSaveJumpTool] {message}");
}
private void EnsureStyles()
{
if (buttonStyle != null && statusStyle != null)
return;
buttonStyle = new GUIStyle(GUI.skin.button)
{
fontSize = 14,
alignment = TextAnchor.MiddleCenter
};
headerStyle = new GUIStyle(GUI.skin.label)
{
fontSize = 13,
fontStyle = FontStyle.Bold,
alignment = TextAnchor.MiddleLeft,
padding = new RectOffset(6, 0, 3, 0)
};
menuBoxStyle = new GUIStyle(GUI.skin.box)
{
padding = new RectOffset(6, 6, 6, 6)
};
statusStyle = new GUIStyle(GUI.skin.box)
{
fontSize = 13,
alignment = TextAnchor.MiddleLeft,
padding = new RectOffset(8, 8, 4, 4)
};
}
}
internal static class DevSaveCatalog
{
private const string CatalogFileName = "catalog.json";
public static List<DevSaveSection> Load(string root)
{
var result = new List<DevSaveSection>();
if (string.IsNullOrEmpty(root) || !Directory.Exists(root))
return result;
var catalogPath = Path.Combine(root, CatalogFileName);
if (File.Exists(catalogPath))
{
try
{
var catalog = JsonConvert.DeserializeObject<CatalogDto>(File.ReadAllText(catalogPath));
if (catalog?.sections != null)
{
foreach (var sectionDto in catalog.sections)
{
if (string.IsNullOrWhiteSpace(sectionDto.id))
continue;
var sectionDir = Path.Combine(root, sectionDto.id);
var title = string.IsNullOrWhiteSpace(sectionDto.title) ? sectionDto.id : sectionDto.title;
result.Add(BuildSection(sectionDto.id, title, sectionDir, sectionDto.entries));
}
return result;
}
}
catch (Exception ex)
{
Debug.LogWarning($"[DevSaveJumpTool] 读取 catalog.json 失败: {ex.Message}");
}
}
foreach (var dir in Directory.GetDirectories(root).OrderBy(d => d, StringComparer.OrdinalIgnoreCase))
{
var id = Path.GetFileName(dir);
result.Add(BuildSection(id, id, dir, null));
}
return result;
}
private static DevSaveSection BuildSection(
string id,
string title,
string sectionDir,
List<CatalogEntryDto> explicitEntries)
{
var entries = new List<DevSaveEntry>();
var root = Directory.GetParent(sectionDir)?.FullName;
if (explicitEntries != null && explicitEntries.Count > 0 && root != null)
{
foreach (var entryDto in explicitEntries)
{
if (string.IsNullOrWhiteSpace(entryDto.path))
continue;
var relative = entryDto.path.Replace('/', Path.DirectorySeparatorChar);
var entryDir = Path.IsPathRooted(relative)
? relative
: Path.Combine(root, relative);
var snapshot = Path.Combine(entryDir, ConstRef.SaveSnapshotFileName);
if (!File.Exists(snapshot + ".json"))
continue;
var label = string.IsNullOrWhiteSpace(entryDto.label)
? FormatFolderLabel(Path.GetFileName(entryDir))
: entryDto.label;
entries.Add(new DevSaveEntry(label, snapshot));
}
}
else if (Directory.Exists(sectionDir))
{
foreach (var entryDir in Directory.GetDirectories(sectionDir)
.OrderBy(d => d, StringComparer.OrdinalIgnoreCase))
{
var snapshot = Path.Combine(entryDir, ConstRef.SaveSnapshotFileName);
if (!File.Exists(snapshot + ".json"))
continue;
entries.Add(new DevSaveEntry(FormatFolderLabel(Path.GetFileName(entryDir)), snapshot));
}
}
return new DevSaveSection(id, title, entries);
}
private static string FormatFolderLabel(string folderName)
{
if (string.IsNullOrEmpty(folderName))
return folderName;
return folderName.Replace('_', ' ');
}
[Serializable]
private class CatalogDto
{
public List<CatalogSectionDto> sections;
}
[Serializable]
private class CatalogSectionDto
{
public string id;
public string title;
public List<CatalogEntryDto> entries;
}
[Serializable]
private class CatalogEntryDto
{
public string label;
public string path;
}
}
internal sealed class DevSaveSection
{
public readonly string Id;
public readonly string Title;
public readonly List<DevSaveEntry> Entries;
public DevSaveSection(string id, string title, List<DevSaveEntry> entries)
{
Id = id;
Title = title;
Entries = entries ?? new List<DevSaveEntry>();
}
}
internal readonly struct DevSaveEntry
{
public readonly string Label;
public readonly string SnapshotPathWithoutExtension;
public DevSaveEntry(string label, string snapshotPathWithoutExtension)
{
Label = label;
SnapshotPathWithoutExtension = snapshotPathWithoutExtension;
}
}
}
#endif