Files
aibis-dream/Assets/Scripts/UI/Panel/DeveloperModePanel.cs
T
2026-07-20 14:29:47 +08:00

892 lines
36 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using AibisDream.Framework;
using AibisDream.Kit;
using AibisDream.SaveSystem;
using AibisDream.Utility;
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
namespace AibisDream.UI
{
/// <summary>
/// Editor / Development Build 中使用的统一运行时开发模式。
/// 这是非模态覆盖层,所有改变游戏状态的操作仍通过正式 GameManager/SaveSystem 入口执行。
/// </summary>
public sealed class DeveloperModePanel : MonoBehaviour
{
private const float OverviewRefreshInterval = 0.25f;
private const float SlowStateRefreshInterval = 1f;
private const int MaxRenderedLogRows = 300;
[Header("Navigation")]
[SerializeField] private Button closeButton;
[SerializeField] private Button[] tabButtons;
[SerializeField] private GameObject[] tabPages;
[Header("Overview")]
[SerializeField] private TMP_Text overviewText;
[SerializeField] private TMP_Text overviewStatusText;
[SerializeField] private Button copyDiagnosticsButton;
[Header("Logs")]
[SerializeField] private TMP_InputField logSearchInput;
[SerializeField] private Button logLevelButton;
[SerializeField] private TMP_Text logLevelLabel;
[SerializeField] private Button logCategoryButton;
[SerializeField] private TMP_Text logCategoryLabel;
[SerializeField] private Toggle pauseLogToggle;
[SerializeField] private Button clearLogsButton;
[SerializeField] private Button copyFilteredLogsButton;
[SerializeField] private Button copySelectedLogButton;
[SerializeField] private TMP_Text logCountText;
[SerializeField] private TMP_Text logDetailsText;
[SerializeField] private ScrollRect logScrollRect;
[SerializeField] private RectTransform logContent;
[SerializeField] private DeveloperModeListRow logRowTemplate;
[Header("Variables")]
[SerializeField] private TMP_InputField variableSearchInput;
[SerializeField] private Button variableScopeButton;
[SerializeField] private TMP_Text variableScopeLabel;
[SerializeField] private Button variableTypeButton;
[SerializeField] private TMP_Text variableTypeLabel;
[SerializeField] private TMP_Text variableCountText;
[SerializeField] private RectTransform variableContent;
[SerializeField] private DeveloperModeListRow variableRowTemplate;
[Header("Saves")]
[SerializeField] private TMP_InputField saveSearchInput;
[SerializeField] private Button reloadSavesButton;
[SerializeField] private TMP_Text saveStatusText;
[SerializeField] private RectTransform slotContent;
[SerializeField] private DeveloperModeListRow slotRowTemplate;
[SerializeField] private RectTransform devSaveContent;
[SerializeField] private DeveloperModeListRow devSaveRowTemplate;
[Header("Paths")]
[SerializeField] private Button openSaveFolderButton;
[SerializeField] private Button copySaveFolderButton;
[SerializeField] private Button openLogFolderButton;
[SerializeField] private Button copyLogFolderButton;
[SerializeField] private Button openDevSaveFolderButton;
[SerializeField] private Button copyDevSaveFolderButton;
[SerializeField] private Button openCurrentLogButton;
[SerializeField] private Button copyCurrentLogButton;
[Header("Display")]
[SerializeField] private Toggle showCursorToggle;
[SerializeField] private Toggle showVersionToggle;
[SerializeField] private Toggle showTopBarToggle;
[SerializeField] private Toggle showDialogToggle;
[SerializeField] private Button pauseGameButton;
[SerializeField] private Button resumeGameButton;
[SerializeField] private Button halfSpeedButton;
[SerializeField] private Button normalSpeedButton;
[SerializeField] private Button doubleSpeedButton;
[SerializeField] private TMP_Text controlStatusText;
private readonly List<DeveloperModeListRow> _logRows = new();
private readonly List<DeveloperModeListRow> _variableRows = new();
private readonly List<DeveloperModeListRow> _slotRows = new();
private readonly List<DeveloperModeListRow> _devSaveRows = new();
private VerText _verText;
private RuntimeLogRecord? _selectedLog;
private int _currentTab;
private int _logLevelIndex;
private int _logCategoryIndex;
private int _variableScopeIndex;
private int _variableTypeIndex;
private int _observedLogVersion = -1;
private string _observedYarnProject;
private string _latestSlotSummary = "N/A";
private bool _variablesDirty = true;
private bool _savesDirty = true;
private bool _isRestorePending;
private bool _showCursor = true;
private bool _showVersion = true;
private bool _showTopBar = true;
private bool _showDialog = true;
private float _nextOverviewRefresh;
private float _nextSlowStateRefresh;
private float _smoothedFps;
private bool _bound;
public bool IsOpen => gameObject.activeSelf;
public bool IsConfigured => tabPages is { Length: 5 } && closeButton != null && overviewText != null;
private void Awake()
{
if (!DeveloperModeGate.IsEnabled)
{
gameObject.SetActive(false);
enabled = false;
return;
}
_verText = FindObjectOfType<VerText>(true);
BindUi();
}
private void OnEnable()
{
if (!DeveloperModeGate.IsEnabled)
{
gameObject.SetActive(false);
return;
}
BindUi();
SubscribeEvents();
CursorManager.Instance?.SetCursorVisible(true);
_variablesDirty = true;
_savesDirty = true;
_nextOverviewRefresh = 0f;
_nextSlowStateRefresh = 0f;
ShowTab(_currentTab);
}
private void OnDisable()
{
if (!DeveloperModeGate.IsEnabled) return;
UnsubscribeEvents();
CursorManager.Instance?.SetCursorVisible(_showCursor);
}
private void Update()
{
var delta = Time.unscaledDeltaTime;
if (delta > 0.0001f)
{
var instantaneous = 1f / delta;
_smoothedFps = _smoothedFps <= 0f
? instantaneous
: Mathf.Lerp(_smoothedFps, instantaneous, 0.08f);
}
var now = Time.unscaledTime;
if (now >= _nextSlowStateRefresh)
{
_nextSlowStateRefresh = now + SlowStateRefreshInterval;
RefreshSlowState();
}
if (now >= _nextOverviewRefresh)
{
_nextOverviewRefresh = now + OverviewRefreshInterval;
RefreshOverview();
DetectYarnProjectChange();
}
if (_currentTab == 1 && pauseLogToggle != null && !pauseLogToggle.isOn
&& _observedLogVersion != LogKit.RuntimeLogVersion)
{
RefreshLogs(scrollToBottom: true);
}
if (_currentTab == 2 && _variablesDirty)
{
RefreshVariables();
}
if (_currentTab == 3 && _savesDirty)
{
RefreshSaves();
}
}
public void TogglePanel()
{
if (!DeveloperModeGate.IsEnabled) return;
if (gameObject.activeSelf)
ClosePanel();
else
gameObject.SetActive(true);
}
public bool HandleEscape()
{
if (!gameObject.activeSelf) return false;
ClosePanel();
return true;
}
public void ClosePanel()
{
gameObject.SetActive(false);
}
private void BindUi()
{
if (_bound) return;
_bound = true;
closeButton?.onClick.AddListener(ClosePanel);
if (tabButtons != null)
{
for (var i = 0; i < tabButtons.Length; i++)
{
var index = i;
tabButtons[i]?.onClick.AddListener(() => ShowTab(index));
}
}
copyDiagnosticsButton?.onClick.AddListener(CopyDiagnostics);
logSearchInput?.onValueChanged.AddListener(_ => RefreshLogs(false));
logLevelButton?.onClick.AddListener(CycleLogLevel);
logCategoryButton?.onClick.AddListener(CycleLogCategory);
pauseLogToggle?.onValueChanged.AddListener(paused =>
{
if (!paused) RefreshLogs(true);
});
clearLogsButton?.onClick.AddListener(ClearLogs);
copyFilteredLogsButton?.onClick.AddListener(CopyFilteredLogs);
copySelectedLogButton?.onClick.AddListener(CopySelectedLog);
variableSearchInput?.onValueChanged.AddListener(_ =>
{
_variablesDirty = true;
});
variableScopeButton?.onClick.AddListener(CycleVariableScope);
variableTypeButton?.onClick.AddListener(CycleVariableType);
saveSearchInput?.onValueChanged.AddListener(_ =>
{
_savesDirty = true;
});
reloadSavesButton?.onClick.AddListener(() =>
{
_savesDirty = true;
RefreshSaves();
});
BindPathButtons();
BindDisplayControls();
UpdateFilterLabels();
}
private void SubscribeEvents()
{
EnumEventSystem.Global.Register<StorageEvent, VariableItem>(StorageEvent.VariableSet, OnVariableSet);
EnumEventSystem.Global.Register(StorageEvent.VariablesCleared, OnVariablesCleared);
EnumEventSystem.Global.Register(GameLifecycleEvent.SessionEnded, OnSessionEnded);
}
private void UnsubscribeEvents()
{
EnumEventSystem.Global.UnRegister<StorageEvent, VariableItem>(StorageEvent.VariableSet, OnVariableSet);
EnumEventSystem.Global.UnRegister(StorageEvent.VariablesCleared, OnVariablesCleared);
EnumEventSystem.Global.UnRegister(GameLifecycleEvent.SessionEnded, OnSessionEnded);
}
private void ShowTab(int index)
{
if (tabPages == null || tabPages.Length == 0) return;
_currentTab = Mathf.Clamp(index, 0, tabPages.Length - 1);
for (var i = 0; i < tabPages.Length; i++)
{
tabPages[i]?.SetActive(i == _currentTab);
}
if (_currentTab == 1) RefreshLogs(true);
if (_currentTab == 2)
{
_variablesDirty = true;
RefreshVariables();
}
if (_currentTab == 3)
{
_savesDirty = true;
RefreshSaves();
}
}
private void RefreshSlowState()
{
try
{
var latest = SlotManager.GetLatestSlotIndex();
if (!latest.HasValue)
{
_latestSlotSummary = "N/A";
return;
}
var meta = SlotManager.LoadMeta(latest.Value);
_latestSlotSummary = meta == null
? $"slot_{latest.Value}meta 不可用)"
: $"slot_{latest.Value} | {meta.savedAt} | {meta.sceneSoName}/{meta.nodeName}";
}
catch (Exception ex)
{
_latestSlotSummary = $"读取失败:{ex.Message}";
}
}
private void RefreshOverview()
{
if (overviewText != null)
{
overviewText.text = BuildOverviewText();
}
}
private string BuildOverviewText()
{
var builder = new StringBuilder(1024);
var gameManager = GameManager.Instance;
var session = gameManager != null ? GameManager.Session : null;
var talkScene = session?.CurrentTalkScene;
var sceneLoader = SceneLoader.Instance;
var dialog = DialogController.Instance;
var runner = dialog?.DialogueRunner;
var node = dialog?.GetCurrentNodeContext();
var locale = LocalizationKit.CurrentLocale;
var persistentHandles = ResourceSystem.PersistentLoader?.TotalHandleCount ?? 0;
var sceneHandles = ResourceSystem.CurrentSceneLoader?.TotalHandleCount ?? 0;
builder.AppendLine($"构建 {Application.version} | {Application.platform} | Dev={Debug.isDebugBuild} | Unity {Application.unityVersion}");
builder.AppendLine($"性能 FPS {_smoothedFps:0.0} | timeScale {Time.timeScale:0.##} | frame {Time.frameCount}");
builder.AppendLine($"会话 {session?.Phase.ToString() ?? "N/A"} | paused={session?.IsPaused.ToString() ?? "N/A"} | dialogue={session?.IsDialogueActive.ToString() ?? "N/A"} | busy={session?.IsBusy.ToString() ?? "N/A"}");
builder.AppendLine($"章节 {talkScene?.name ?? "N/A"} | {talkScene?.chapter ?? "N/A"} | {talkScene?.title ?? "N/A"}");
builder.AppendLine($"场景 key={sceneLoader?.CurrentSceneName ?? "N/A"} | active={SceneManager.GetActiveScene().name}");
builder.AppendLine($"Yarn project={runner?.YarnProject?.name ?? "N/A"} | running={runner?.IsDialogueRunning.ToString() ?? "N/A"}");
builder.AppendLine($"节点 {node?.nodeName ?? "N/A"} | tags={FormatTags(node?.tags)}");
builder.AppendLine($"语言 {LocalizationKit.CurrentLanguageCode} | {locale?.name ?? "N/A"}");
builder.AppendLine($"存档 latest={_latestSlotSummary}");
builder.AppendLine($"存档状态 saving={SaveRestoreOrchestrator.IsSaving} | restoring={SaveRestoreOrchestrator.IsRestoring} | suppressAuto={SaveRestoreOrchestrator.IsAutoSaveSuppressed}");
builder.AppendLine($"资源句柄 persistent={persistentHandles} | scene={sceneHandles}");
builder.Append($"日志 memory={LogKit.RuntimeLogCount}/{RuntimeLogBuffer.DefaultCapacity} | file={(LogKit.IsFileLoggingEnabled ? LogKit.CurrentLogFilePath : "Editor profile disabled")}");
return builder.ToString();
}
private static string FormatTags(IReadOnlyList<string> tags)
{
return tags == null || tags.Count == 0 ? "-" : string.Join(",", tags);
}
private void DetectYarnProjectChange()
{
var current = DialogController.Instance?.DialogueRunner?.YarnProject?.name;
if (string.Equals(current, _observedYarnProject, StringComparison.Ordinal)) return;
_observedYarnProject = current;
_variablesDirty = true;
}
private void CopyDiagnostics()
{
var builder = new StringBuilder();
builder.AppendLine($"AIBIS Dream diagnostics @ {DateTime.Now:O}");
builder.AppendLine(BuildOverviewText());
builder.AppendLine();
builder.AppendLine("Recent warnings/errors:");
foreach (var record in LogKit.GetRuntimeLogSnapshot()
.Where(item => item.Level >= LogLevel.Warning)
.TakeLast(50))
{
builder.AppendLine(record.ToDisplayString(includeStackTrace: true));
}
GUIUtility.systemCopyBuffer = builder.ToString();
SetOverviewStatus("诊断摘要已复制到剪贴板。");
}
private void CycleLogLevel()
{
_logLevelIndex = (_logLevelIndex + 1) % (Enum.GetValues(typeof(LogLevel)).Length + 1);
UpdateFilterLabels();
RefreshLogs(false);
}
private void CycleLogCategory()
{
_logCategoryIndex = (_logCategoryIndex + 1) % (Enum.GetValues(typeof(LogCategory)).Length + 1);
UpdateFilterLabels();
RefreshLogs(false);
}
private void RefreshLogs(bool scrollToBottom)
{
if (logContent == null || logRowTemplate == null) return;
var snapshot = LogKit.GetRuntimeLogSnapshot();
_observedLogVersion = LogKit.RuntimeLogVersion;
var filtered = snapshot.Where(MatchesLogFilter).ToArray();
var rendered = filtered.Skip(Mathf.Max(0, filtered.Length - MaxRenderedLogRows)).ToArray();
for (var i = 0; i < rendered.Length; i++)
{
var record = rendered[i];
var row = GetRow(_logRows, logRowTemplate, logContent, i);
row.Bind(
Truncate(record.ToDisplayString(false).Replace('\n', ' '), 220),
() => SelectLog(record),
true,
GetLogColor(record.Level));
}
HideRows(_logRows, rendered.Length);
if (logCountText != null)
{
logCountText.text = filtered.Length > rendered.Length
? $"显示最新 {rendered.Length}/{filtered.Length},缓冲 {snapshot.Length}"
: $"显示 {rendered.Length},缓冲 {snapshot.Length}";
}
if (scrollToBottom && logScrollRect != null)
{
Canvas.ForceUpdateCanvases();
logScrollRect.verticalNormalizedPosition = 0f;
}
}
private bool MatchesLogFilter(RuntimeLogRecord record)
{
if (_logLevelIndex > 0 && record.Level < (LogLevel)(_logLevelIndex - 1)) return false;
if (_logCategoryIndex > 0 && record.Category != (LogCategory)(_logCategoryIndex - 1)) return false;
var search = logSearchInput?.text;
if (string.IsNullOrWhiteSpace(search)) return true;
return Contains(record.Message, search)
|| Contains(record.Context, search)
|| Contains(record.StackTrace, search)
|| Contains(record.SceneName, search)
|| Contains(record.Category.ToString(), search);
}
private void SelectLog(RuntimeLogRecord record)
{
_selectedLog = record;
if (logDetailsText != null)
{
logDetailsText.text = record.ToDisplayString(includeStackTrace: true);
}
}
private void ClearLogs()
{
LogKit.ClearRuntimeLogs();
_selectedLog = null;
if (logDetailsText != null) logDetailsText.text = "未选择日志。";
RefreshLogs(false);
}
private void CopyFilteredLogs()
{
GUIUtility.systemCopyBuffer = string.Join(
"\n",
LogKit.GetRuntimeLogSnapshot().Where(MatchesLogFilter).Select(item => item.ToDisplayString(true)));
}
private void CopySelectedLog()
{
if (_selectedLog.HasValue)
{
GUIUtility.systemCopyBuffer = _selectedLog.Value.ToDisplayString(true);
}
}
private void CycleVariableScope()
{
_variableScopeIndex = (_variableScopeIndex + 1) % 3;
UpdateFilterLabels();
_variablesDirty = true;
}
private void CycleVariableType()
{
_variableTypeIndex = (_variableTypeIndex + 1) % 4;
UpdateFilterLabels();
_variablesDirty = true;
}
private void RefreshVariables()
{
_variablesDirty = false;
if (variableContent == null || variableRowTemplate == null) return;
var storage = YarnVariableStorage.Instance;
var project = DialogController.Instance?.DialogueRunner?.YarnProject;
var snapshot = DeveloperVariableSnapshot.Capture(storage, project);
var filtered = snapshot.Where(MatchesVariableFilter).ToArray();
for (var i = 0; i < filtered.Length; i++)
{
var variable = filtered[i];
var line = $"{variable.Name} = {variable.Value} [{variable.Type}/{variable.Kind}/{(variable.HasRuntimeOverride ? "Runtime" : "Default")}]";
var row = GetRow(_variableRows, variableRowTemplate, variableContent, i);
row.Bind(
line,
() => GUIUtility.systemCopyBuffer = line,
true,
variable.IsGlobal ? new Color(0.35f, 0.9f, 1f) : Color.white);
}
HideRows(_variableRows, filtered.Length);
if (variableCountText != null)
{
variableCountText.text = $"{filtered.Length}/{snapshot.Length} variables";
}
}
private bool MatchesVariableFilter(DeveloperVariableRecord variable)
{
if (_variableScopeIndex == 1 && !variable.IsGlobal) return false;
if (_variableScopeIndex == 2 && variable.IsGlobal) return false;
if (_variableTypeIndex > 0)
{
var expected = _variableTypeIndex switch
{
1 => "Number",
2 => "String",
3 => "Bool",
_ => null
};
if (!string.Equals(variable.Type, expected, StringComparison.Ordinal)) return false;
}
var search = variableSearchInput?.text;
return string.IsNullOrWhiteSpace(search)
|| Contains(variable.Name, search)
|| Contains(variable.Value, search);
}
private void OnVariableSet(VariableItem _) => _variablesDirty = true;
private void OnVariablesCleared() => _variablesDirty = true;
private void RefreshSaves()
{
_savesDirty = false;
RefreshSlotRows();
RefreshDevSaveRows();
}
private void RefreshSlotRows()
{
if (slotContent == null || slotRowTemplate == null) return;
var latest = SlotManager.GetLatestSlotIndex();
var viewModels = SlotManager.GetSlotViewModels();
for (var i = 0; i < viewModels.Length; i++)
{
var view = viewModels[i];
var slotIndex = view.SlotIndex;
var meta = SlotManager.LoadMeta(slotIndex);
var valid = !view.IsEmpty && meta != null && File.Exists(SlotDirectory.GetSnapshotPath(slotIndex));
var prefix = latest == slotIndex ? "★" : " ";
var kind = view.IsAutoSlot ? "AUTO" : "MANUAL";
var text = valid
? $"{prefix} slot_{slotIndex} [{kind}] {meta.savedAt} | {meta.sceneName} | {meta.sceneSoName} | {meta.yarnProjectId}/{meta.nodeName} | schema {meta.schemaVersion} | game {meta.gameVersion}"
: $"{prefix} slot_{slotIndex} [{kind}] {(view.IsEmpty ? "" : "损坏或缺少 snapshot")}";
var row = GetRow(_slotRows, slotRowTemplate, slotContent, i);
row.Bind(
text,
() => RestoreSlot(slotIndex),
valid && CanStartRestore(),
valid ? Color.white : new Color(1f, 0.5f, 0.45f),
isError: !valid && !view.IsEmpty);
}
HideRows(_slotRows, viewModels.Length);
}
private void RefreshDevSaveRows()
{
if (devSaveContent == null || devSaveRowTemplate == null) return;
var sections = DevSaveCatalog.Load(ConstRef.TestSaveFilePath);
var search = saveSearchInput?.text;
var rowIndex = 0;
foreach (var section in sections)
{
var entries = section.Entries.Where(entry => string.IsNullOrWhiteSpace(search)
|| Contains(section.Title, search)
|| Contains(entry.Label, search)
|| Contains(entry.AnchorNode, search)).ToArray();
if (entries.Length == 0) continue;
GetRow(_devSaveRows, devSaveRowTemplate, devSaveContent, rowIndex++)
.Bind($"── {section.Title} ({entries.Count(entry => entry.IsValid)}/{entries.Length}) ──", null, false,
new Color(0.35f, 0.9f, 1f));
foreach (var entry in entries)
{
var captured = entry;
var text = entry.IsValid
? $"{entry.Order:000} {entry.Label} | node={entry.AnchorNode} | scene={entry.Snapshot?.scene?.sceneName}"
: $"{entry.Order:000} {entry.Label} | 不可用:{entry.Error}";
GetRow(_devSaveRows, devSaveRowTemplate, devSaveContent, rowIndex++)
.Bind(text, () => RestoreDevSave(captured), entry.IsValid && CanStartRestore(),
entry.IsValid ? Color.white : new Color(1f, 0.5f, 0.45f), isError: !entry.IsValid);
}
}
if (sections.Count == 0)
{
GetRow(_devSaveRows, devSaveRowTemplate, devSaveContent, rowIndex++)
.Bind(DevSaveCatalog.LastError ?? "开发检查点清单为空。", null, false,
new Color(1f, 0.5f, 0.45f), isError: true);
}
HideRows(_devSaveRows, rowIndex);
}
private bool CanStartRestore()
{
if (_isRestorePending || SaveRestoreOrchestrator.IsRestoring || GameManager.Instance == null) return false;
var session = GameManager.Session;
return !session.IsBusy && session.Phase is GameSessionPhase.MainMenu or GameSessionPhase.Playing;
}
private void RestoreSlot(int slotIndex)
{
if (!CanStartRestore())
{
SetSaveStatus("当前会话忙碌,无法读档。", true);
return;
}
_isRestorePending = true;
SetSaveStatus($"正在读取 slot_{slotIndex}...", false);
if (!GameManager.Instance.TryRestoreSlot(slotIndex, OnRestoreCompleted))
{
_isRestorePending = false;
SetSaveStatus("读档请求被 GameManager 拒绝。", true);
}
_savesDirty = true;
}
private void RestoreDevSave(DevSaveEntry entry)
{
if (entry == null || !entry.IsValid || !CanStartRestore())
{
SetSaveStatus("开发检查点当前不可加载。", true);
return;
}
_isRestorePending = true;
SetSaveStatus($"正在跳转:{entry.Label}...", false);
if (!GameManager.Instance.TryRestoreFile(entry.SnapshotPathWithoutExtension, RestoreOptions.DevJump, OnRestoreCompleted))
{
_isRestorePending = false;
SetSaveStatus("跳转请求被 GameManager 拒绝。", true);
}
_savesDirty = true;
}
private void OnRestoreCompleted(RestoreResult result)
{
_isRestorePending = false;
_savesDirty = true;
if (result?.Success == true)
{
var warnings = result.Warnings.Count == 0 ? string.Empty : $"\n{string.Join("\n", result.Warnings)}";
SetSaveStatus($"读档成功。{warnings}", false);
return;
}
var errors = result?.Errors == null || result.Errors.Count == 0
? "未返回错误详情。"
: string.Join("\n", result.Errors);
SetSaveStatus($"读档失败 [{result?.FailedPhase ?? "unknown"}]\n{errors}", true);
}
private void BindPathButtons()
{
openSaveFolderButton?.onClick.AddListener(() => RevealPath(ConstRef.SaveFilePath, true));
copySaveFolderButton?.onClick.AddListener(() => CopyPath(ConstRef.SaveFilePath));
openLogFolderButton?.onClick.AddListener(() => RevealPath(ConstRef.LogFilePath, true));
copyLogFolderButton?.onClick.AddListener(() => CopyPath(ConstRef.LogFilePath));
openDevSaveFolderButton?.onClick.AddListener(() => RevealPath(ConstRef.TestSaveFilePath, false));
copyDevSaveFolderButton?.onClick.AddListener(() => CopyPath(ConstRef.TestSaveFilePath));
openCurrentLogButton?.onClick.AddListener(() => RevealPath(LogKit.CurrentLogFilePath, false));
copyCurrentLogButton?.onClick.AddListener(() => CopyPath(LogKit.CurrentLogFilePath));
if (!DeveloperPathUtility.CanReveal)
{
if (openSaveFolderButton != null) openSaveFolderButton.interactable = false;
if (openLogFolderButton != null) openLogFolderButton.interactable = false;
if (openDevSaveFolderButton != null) openDevSaveFolderButton.interactable = false;
if (openCurrentLogButton != null) openCurrentLogButton.interactable = false;
}
}
private void RevealPath(string path, bool createDirectory)
{
if (DeveloperPathUtility.TryReveal(path, createDirectory, out var error))
SetSaveStatus($"已打开:{path}", false);
else
SetSaveStatus(error, true);
}
private void CopyPath(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
SetSaveStatus("当前没有日志文件;Editor 文件日志配置为关闭。", true);
return;
}
GUIUtility.systemCopyBuffer = Path.GetFullPath(path);
SetSaveStatus($"已复制路径:{path}", false);
}
private void BindDisplayControls()
{
showCursorToggle?.SetIsOnWithoutNotify(_showCursor);
showVersionToggle?.SetIsOnWithoutNotify(_showVersion);
showTopBarToggle?.SetIsOnWithoutNotify(_showTopBar);
showDialogToggle?.SetIsOnWithoutNotify(_showDialog);
showCursorToggle?.onValueChanged.AddListener(value =>
{
_showCursor = value;
if (!gameObject.activeSelf) CursorManager.Instance?.SetCursorVisible(value);
});
showVersionToggle?.onValueChanged.AddListener(value =>
{
_showVersion = value;
_verText?.SetVisible(value);
});
showTopBarToggle?.onValueChanged.AddListener(value =>
{
_showTopBar = value;
UIManager.Instance?.GetPanel<MainPanel>()?.SetTopBarVisible(value);
});
showDialogToggle?.onValueChanged.AddListener(value =>
{
_showDialog = value;
DialogUIManager.Instance?.SetDialogVisualVisible(value);
});
pauseGameButton?.onClick.AddListener(PauseGame);
resumeGameButton?.onClick.AddListener(ResumeGame);
halfSpeedButton?.onClick.AddListener(() => SetTimeScale(0.5f));
normalSpeedButton?.onClick.AddListener(() => SetTimeScale(1f));
doubleSpeedButton?.onClick.AddListener(() => SetTimeScale(2f));
}
private void PauseGame()
{
var success = GameManager.Instance != null && GameManager.Instance.TryPause();
SetControlStatus(success ? "游戏已暂停。" : "当前状态不可暂停。", !success);
}
private void ResumeGame()
{
var success = GameManager.Instance != null && GameManager.Instance.TryResume();
SetControlStatus(success ? "游戏已继续。" : "当前状态不可继续。", !success);
}
private void SetTimeScale(float value)
{
if (GameManager.Instance == null || GameManager.Session.IsBusy || GameManager.Session.IsPaused
|| GameManager.Session.Phase != GameSessionPhase.Playing)
{
SetControlStatus("仅在非暂停、非忙碌的 Playing 状态可调整倍速。", true);
return;
}
Time.timeScale = value;
SetControlStatus($"时间倍率已设为 {value:0.##}×。", false);
}
private void OnSessionEnded()
{
Time.timeScale = 1f;
_variablesDirty = true;
_savesDirty = true;
}
private void UpdateFilterLabels()
{
if (logLevelLabel != null)
{
logLevelLabel.text = _logLevelIndex == 0
? "最低等级:全部"
: $"最低等级:{(LogLevel)(_logLevelIndex - 1)}";
}
if (logCategoryLabel != null)
{
logCategoryLabel.text = _logCategoryIndex == 0
? "类别:全部"
: $"类别:{(LogCategory)(_logCategoryIndex - 1)}";
}
if (variableScopeLabel != null)
variableScopeLabel.text = new[] { "范围:全部", "范围:Global", "范围:Local" }[_variableScopeIndex];
if (variableTypeLabel != null)
variableTypeLabel.text = new[] { "类型:全部", "类型:Number", "类型:String", "类型:Bool" }[_variableTypeIndex];
}
private static DeveloperModeListRow GetRow(
IList<DeveloperModeListRow> pool,
DeveloperModeListRow template,
RectTransform parent,
int index)
{
while (pool.Count <= index)
{
var row = Instantiate(template, parent);
row.name = $"Row {pool.Count:000}";
pool.Add(row);
}
return pool[index];
}
private static void HideRows(IList<DeveloperModeListRow> rows, int fromIndex)
{
for (var i = fromIndex; i < rows.Count; i++)
{
rows[i].Hide();
}
}
private static Color GetLogColor(LogLevel level)
{
return level switch
{
LogLevel.Warning => new Color(1f, 0.82f, 0.3f),
LogLevel.Error or LogLevel.Fatal => new Color(1f, 0.42f, 0.38f),
LogLevel.Debug => new Color(0.6f, 0.7f, 0.75f),
_ => Color.white
};
}
private static bool Contains(string value, string search)
{
return !string.IsNullOrEmpty(value)
&& value.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0;
}
private static string Truncate(string value, int maxLength)
{
if (string.IsNullOrEmpty(value) || value.Length <= maxLength) return value;
return value.Substring(0, maxLength - 1) + "…";
}
private void SetOverviewStatus(string message)
{
if (overviewStatusText != null) overviewStatusText.text = message ?? string.Empty;
}
private void SetSaveStatus(string message, bool error)
{
if (saveStatusText == null) return;
saveStatusText.text = message ?? string.Empty;
saveStatusText.color = error ? new Color(1f, 0.45f, 0.4f) : new Color(0.45f, 1f, 0.65f);
}
private void SetControlStatus(string message, bool error)
{
if (controlStatusText == null) return;
controlStatusText.text = message ?? string.Empty;
controlStatusText.color = error ? new Color(1f, 0.45f, 0.4f) : new Color(0.45f, 1f, 0.65f);
}
}
}