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
{
///
/// Editor / Development Build 中使用的统一运行时开发模式。
/// 这是非模态覆盖层,所有改变游戏状态的操作仍通过正式 GameManager/SaveSystem 入口执行。
///
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 Button reloadSavesButton;
[SerializeField] private TMP_Text saveStatusText;
[SerializeField] private RectTransform slotContent;
[SerializeField] private DeveloperModeListRow slotRowTemplate;
[Header("Paths")]
[SerializeField] private Button openSaveFolderButton;
[SerializeField] private Button copySaveFolderButton;
[SerializeField] private Button openLogFolderButton;
[SerializeField] private Button copyLogFolderButton;
[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 _logRows = new();
private readonly List _variableRows = new();
private readonly List _slotRows = 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(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);
reloadSavesButton?.onClick.AddListener(() =>
{
_savesDirty = true;
RefreshSaves();
});
BindPathButtons();
BindDisplayControls();
UpdateFilterLabels();
}
private void SubscribeEvents()
{
EnumEventSystem.Global.Register(StorageEvent.VariableSet, OnVariableSet);
EnumEventSystem.Global.Register(StorageEvent.VariablesCleared, OnVariablesCleared);
EnumEventSystem.Global.Register(GameLifecycleEvent.SessionEnded, OnSessionEnded);
}
private void UnsubscribeEvents()
{
EnumEventSystem.Global.UnRegister(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 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();
}
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 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 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));
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 (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()?.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 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 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);
}
}
}