- 将 ShrinkEventBus、ShrinkDataSaver 及其 EventBus 集成从 gitlink 转为仓库直接维护的完整 UPM 包,补齐运行时、编辑器工具、测试与文档 - 新增 Command 和 Network 的 App 集成组件,支持 ContextLoader 服务发布、可逆注销及 Network Loopback 生命周期管理 - 更新 Starter 与演示组合逻辑,缺失模块时可注册、已有兼容安装器时可覆盖,并补充宿主启动断言 - 升级内部包依赖与 Shared CodeGen 包定义,放宽 Integration.App 包的 Git 忽略规则 - 将独立服务器生成器改为基于已编译程序集的语义扫描,支持 partial、复杂泛型、命名冲突检测及模板 SHA-256 覆写保护 - 新增 Network 语义扫描、模板保护和 App 组件生命周期测试 - 新增真实 UPM 消费工程验证脚本,校验内部版本一致性、程序集加载及 EditMode 测试 - 重构当前架构文档并归档已完成的 Cordis 迁移与旧代码地图
794 lines
31 KiB
C#
794 lines
31 KiB
C#
#if UNITY_EDITOR
|
||
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.Reflection;
|
||
using UnityEditor;
|
||
using UnityEditor.UIElements;
|
||
using UnityEngine;
|
||
using UnityEngine.UIElements;
|
||
|
||
namespace ShrinkEventBus.Editor
|
||
{
|
||
public class EventBusViewerWindow : EditorWindow
|
||
{
|
||
private enum LiveLogFilterMode
|
||
{
|
||
All,
|
||
HasListeners,
|
||
NoListeners
|
||
}
|
||
|
||
private sealed class HandlerDisplayData
|
||
{
|
||
public object TargetInstance { get; set; }
|
||
public MonoBehaviour PingTarget { get; set; }
|
||
public string TargetName { get; set; }
|
||
public string MethodName { get; set; }
|
||
public object Priority { get; set; }
|
||
public string ExecType { get; set; }
|
||
public bool IsStaticHandler { get; set; }
|
||
}
|
||
|
||
private sealed class EventLogRecord
|
||
{
|
||
public DateTime Timestamp { get; set; }
|
||
public string TimestampText { get; set; }
|
||
public string EventName { get; set; }
|
||
public string SenderInfo { get; set; }
|
||
public bool HasListeners { get; set; }
|
||
public bool IsCancelable { get; set; }
|
||
public bool IsCanceled { get; set; }
|
||
public string Result { get; set; }
|
||
public string Phase { get; set; }
|
||
public List<string> Parameters { get; set; }
|
||
public List<HandlerDisplayData> Handlers { get; set; }
|
||
public string MatchText { get; set; }
|
||
}
|
||
|
||
private sealed class EventLogGroup
|
||
{
|
||
public string EventName { get; set; }
|
||
public List<EventLogRecord> Records { get; } = new();
|
||
public EventLogRecord LatestRecord => Records[0];
|
||
}
|
||
|
||
private ScrollView _subscribersScrollView;
|
||
private ScrollView _liveTrackerScrollView;
|
||
private VisualElement _subscribersTab;
|
||
private VisualElement _liveTrackerTab;
|
||
private Label _instanceCountLabel;
|
||
private Label _liveLogStatsLabel;
|
||
private string _searchString = "";
|
||
private string _liveLogSearchString = "";
|
||
private LiveLogFilterMode _liveLogFilterMode = LiveLogFilterMode.All;
|
||
private bool _collapseSameEventType = true;
|
||
private const int MaxLogCount = 100;
|
||
private const double LiveTrackerRefreshIntervalSeconds = 0.1d;
|
||
private readonly List<EventLogRecord> _logRecords = new();
|
||
private bool _isLiveTrackerDirty;
|
||
private double _nextLiveTrackerRefreshTime;
|
||
|
||
[MenuItem("ShrinkSDK/事件总线/事件查看器")]
|
||
public static void ShowWindow()
|
||
{
|
||
var window = GetWindow<EventBusViewerWindow>("EventBus 控制台");
|
||
window.minSize = new Vector2(500, 600);
|
||
window.Show();
|
||
}
|
||
|
||
private void OnEnable()
|
||
{
|
||
EventBus.OnEventTriggeredForEditor += RecordEventLog;
|
||
EditorApplication.update += OnEditorUpdate;
|
||
}
|
||
|
||
private void OnDisable()
|
||
{
|
||
EventBus.OnEventTriggeredForEditor -= RecordEventLog;
|
||
EditorApplication.update -= OnEditorUpdate;
|
||
}
|
||
|
||
private void CreateGUI()
|
||
{
|
||
var root = rootVisualElement;
|
||
root.style.flexDirection = FlexDirection.Column;
|
||
root.Add(CreateHeader());
|
||
root.Add(CreateTabBar());
|
||
|
||
var contentContainer = new VisualElement { style = { flexGrow = 1 } };
|
||
_subscribersTab = CreateSubscribersTab();
|
||
_liveTrackerTab = CreateLiveTrackerTab();
|
||
_subscribersTab.style.display = DisplayStyle.Flex;
|
||
_liveTrackerTab.style.display = DisplayStyle.None;
|
||
|
||
contentContainer.Add(_subscribersTab);
|
||
contentContainer.Add(_liveTrackerTab);
|
||
root.Add(contentContainer);
|
||
|
||
RefreshSubscribersView();
|
||
|
||
root.schedule.Execute(() =>
|
||
{
|
||
_instanceCountLabel.text = Application.isPlaying
|
||
? $"活跃实例数: {EventBus.GetRegisteredInstanceCount()}"
|
||
: "游戏未运行";
|
||
}).Every(500);
|
||
}
|
||
|
||
private VisualElement CreateHeader()
|
||
{
|
||
var header = new VisualElement
|
||
{
|
||
style =
|
||
{
|
||
paddingTop = 10, paddingBottom = 10, paddingLeft = 10, paddingRight = 10,
|
||
backgroundColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f, 1f)), borderBottomWidth = 1,
|
||
borderBottomColor = new StyleColor(Color.black)
|
||
}
|
||
};
|
||
header.Add(new Label("Shrink Event Bus 控制台")
|
||
{ style = { fontSize = 16, unityFontStyleAndWeight = FontStyle.Bold } });
|
||
var debugToggle = new Toggle("启用 Debug 调试记录 (追踪实时事件)")
|
||
{ value = EventBus.EnableDebugRecord, style = { marginTop = 5 } };
|
||
debugToggle.RegisterValueChangedCallback(evt => EventBus.EnableDebugRecord = evt.newValue);
|
||
header.Add(debugToggle);
|
||
var infoRow = new VisualElement
|
||
{ style = { flexDirection = FlexDirection.Row, marginTop = 5, justifyContent = Justify.SpaceBetween } };
|
||
_instanceCountLabel = new Label("活跃实例数: 0") { style = { color = new StyleColor(Color.cyan) } };
|
||
infoRow.Add(_instanceCountLabel);
|
||
header.Add(infoRow);
|
||
return header;
|
||
}
|
||
|
||
private VisualElement CreateTabBar()
|
||
{
|
||
var toolbar = new Toolbar();
|
||
toolbar.Add(new ToolbarButton(() => SwitchTab(0))
|
||
{ text = "订阅者全览", style = { flexGrow = 1, unityTextAlign = TextAnchor.MiddleCenter } });
|
||
toolbar.Add(new ToolbarButton(() => SwitchTab(1))
|
||
{ text = "实时触发日志", style = { flexGrow = 1, unityTextAlign = TextAnchor.MiddleCenter } });
|
||
return toolbar;
|
||
}
|
||
|
||
private VisualElement CreateSubscribersTab()
|
||
{
|
||
var container = new VisualElement { style = { flexGrow = 1 } };
|
||
var toolbar = new Toolbar();
|
||
var searchField = new ToolbarSearchField { style = { flexGrow = 1 } };
|
||
searchField.RegisterValueChangedCallback(evt =>
|
||
{
|
||
_searchString = evt.newValue;
|
||
RefreshSubscribersView();
|
||
});
|
||
toolbar.Add(searchField);
|
||
toolbar.Add(new ToolbarButton(RefreshSubscribersView) { text = "刷新视图" });
|
||
container.Add(toolbar);
|
||
_subscribersScrollView = new ScrollView
|
||
{ style = { flexGrow = 1, paddingLeft = 5, paddingRight = 5, paddingTop = 5 } };
|
||
container.Add(_subscribersScrollView);
|
||
return container;
|
||
}
|
||
|
||
private VisualElement CreateLiveTrackerTab()
|
||
{
|
||
var container = new VisualElement { style = { flexGrow = 1 } };
|
||
var toolbar = new Toolbar();
|
||
var searchField = new ToolbarSearchField { style = { flexGrow = 1 } };
|
||
searchField.RegisterValueChangedCallback(evt =>
|
||
{
|
||
_liveLogSearchString = evt.newValue ?? string.Empty;
|
||
RequestLiveTrackerRefresh(true);
|
||
});
|
||
toolbar.Add(searchField);
|
||
|
||
var filterMenu = new ToolbarMenu { text = "全部事件" };
|
||
filterMenu.menu.AppendAction("全部事件", _ =>
|
||
{
|
||
_liveLogFilterMode = LiveLogFilterMode.All;
|
||
filterMenu.text = "全部事件";
|
||
RequestLiveTrackerRefresh(true);
|
||
});
|
||
filterMenu.menu.AppendAction("仅有监听者", _ =>
|
||
{
|
||
_liveLogFilterMode = LiveLogFilterMode.HasListeners;
|
||
filterMenu.text = "仅有监听者";
|
||
RequestLiveTrackerRefresh(true);
|
||
});
|
||
filterMenu.menu.AppendAction("仅无监听者", _ =>
|
||
{
|
||
_liveLogFilterMode = LiveLogFilterMode.NoListeners;
|
||
filterMenu.text = "仅无监听者";
|
||
RequestLiveTrackerRefresh(true);
|
||
});
|
||
toolbar.Add(filterMenu);
|
||
|
||
var collapseToggle = new ToolbarToggle
|
||
{
|
||
text = "折叠同类事件",
|
||
value = _collapseSameEventType
|
||
};
|
||
collapseToggle.RegisterValueChangedCallback(evt =>
|
||
{
|
||
_collapseSameEventType = evt.newValue;
|
||
RequestLiveTrackerRefresh(true);
|
||
});
|
||
toolbar.Add(collapseToggle);
|
||
toolbar.Add(new ToolbarButton(ClearLogs) { text = "清空日志" });
|
||
container.Add(toolbar);
|
||
|
||
_liveLogStatsLabel = new Label
|
||
{
|
||
style =
|
||
{
|
||
marginLeft = 8,
|
||
marginRight = 8,
|
||
marginTop = 4,
|
||
marginBottom = 4,
|
||
color = new StyleColor(Color.gray)
|
||
}
|
||
};
|
||
container.Add(_liveLogStatsLabel);
|
||
|
||
_liveTrackerScrollView = new ScrollView { style = { flexGrow = 1, paddingLeft = 5, paddingRight = 5 } };
|
||
container.Add(_liveTrackerScrollView);
|
||
RequestLiveTrackerRefresh(true);
|
||
return container;
|
||
}
|
||
|
||
private void SwitchTab(int index)
|
||
{
|
||
_subscribersTab.style.display = index == 0 ? DisplayStyle.Flex : DisplayStyle.None;
|
||
_liveTrackerTab.style.display = index == 1 ? DisplayStyle.Flex : DisplayStyle.None;
|
||
if (index == 0)
|
||
{
|
||
RefreshSubscribersView();
|
||
return;
|
||
}
|
||
RequestLiveTrackerRefresh(true);
|
||
}
|
||
|
||
private void OnEditorUpdate()
|
||
{
|
||
if (!_isLiveTrackerDirty) return;
|
||
if (_liveTrackerTab == null || _liveTrackerTab.style.display != DisplayStyle.Flex) return;
|
||
if (EditorApplication.timeSinceStartup < _nextLiveTrackerRefreshTime) return;
|
||
RefreshLiveTrackerView();
|
||
}
|
||
|
||
private void RequestLiveTrackerRefresh(bool immediate = false)
|
||
{
|
||
_isLiveTrackerDirty = true;
|
||
if (immediate)
|
||
{
|
||
_nextLiveTrackerRefreshTime = EditorApplication.timeSinceStartup;
|
||
if (_liveTrackerTab != null && _liveTrackerTab.style.display == DisplayStyle.Flex)
|
||
RefreshLiveTrackerView();
|
||
}
|
||
}
|
||
|
||
private void RefreshSubscribersView()
|
||
{
|
||
_subscribersScrollView.Clear();
|
||
if (!Application.isPlaying)
|
||
{
|
||
_subscribersScrollView.Add(new HelpBox("请先运行游戏,系统初始化后即可查看内存中的事件树。", HelpBoxMessageType.Info));
|
||
return;
|
||
}
|
||
|
||
var snapshots = EventBus.GetAllSubscribersSnapshot();
|
||
foreach (var entry in snapshots)
|
||
{
|
||
var eventType = entry.Key;
|
||
if (!string.IsNullOrEmpty(_searchString) &&
|
||
!eventType.Name.ToLower().Contains(_searchString.ToLower())) continue;
|
||
|
||
var handlers = entry.Value;
|
||
|
||
var foldout = new Foldout
|
||
{
|
||
text = $"{eventType.Name} (挂载数: {handlers.Length})",
|
||
style =
|
||
{
|
||
backgroundColor = new StyleColor(new Color(0.2f, 0.2f, 0.2f, 0.5f)),
|
||
marginTop = 2,
|
||
paddingBottom = 2,
|
||
borderBottomWidth = 1,
|
||
borderBottomColor = new StyleColor(new Color(0.1f, 0.1f, 0.1f))
|
||
}
|
||
};
|
||
|
||
foreach (var h in handlers) foldout.Add(CreateHandlerRow(h));
|
||
_subscribersScrollView.Add(foldout);
|
||
}
|
||
}
|
||
|
||
private void RecordEventLog(EventBase evt, string eventName, string senderInfo, EventHandlerInfo[] handlers)
|
||
{
|
||
if (!EventBus.EnableDebugRecord) return;
|
||
_logRecords.Add(CreateEventLogRecord(evt, eventName, senderInfo, handlers));
|
||
if (_logRecords.Count > MaxLogCount)
|
||
_logRecords.RemoveAt(0);
|
||
RequestLiveTrackerRefresh();
|
||
}
|
||
|
||
private EventLogRecord CreateEventLogRecord(EventBase evt, string eventName, string senderInfo,
|
||
EventHandlerInfo[] handlers)
|
||
{
|
||
var timestamp = DateTime.Now;
|
||
var hasListeners = handlers.Length > 0;
|
||
var paramsList = DumpEventParams(evt);
|
||
var handlerRows = new List<HandlerDisplayData>(handlers.Length);
|
||
foreach (var handler in handlers)
|
||
handlerRows.Add(ExtractHandlerDisplayData(handler));
|
||
|
||
var isCancelable = (GetMemberValue(evt, "IsCancelable") as bool?) ?? false;
|
||
var isCanceled = (GetMemberValue(evt, "IsCanceled") as bool?) ?? false;
|
||
var resultStr = GetMemberValue(evt, "Result")?.ToString() ?? "DEFAULT";
|
||
var phaseStr = GetMemberValue(evt, "Phase")?.ToString() ?? "NORMAL";
|
||
|
||
return new EventLogRecord
|
||
{
|
||
Timestamp = timestamp,
|
||
TimestampText = timestamp.ToString("HH:mm:ss.fff"),
|
||
EventName = eventName,
|
||
SenderInfo = senderInfo,
|
||
HasListeners = hasListeners,
|
||
IsCancelable = isCancelable,
|
||
IsCanceled = isCanceled,
|
||
Result = resultStr,
|
||
Phase = phaseStr,
|
||
Parameters = paramsList,
|
||
Handlers = handlerRows,
|
||
MatchText = BuildLogMatchText(eventName, senderInfo, paramsList)
|
||
};
|
||
}
|
||
|
||
private string BuildLogMatchText(string eventName, string senderInfo, List<string> paramsList)
|
||
{
|
||
if (paramsList.Count == 0)
|
||
return $"{eventName}\n{senderInfo}";
|
||
return $"{eventName}\n{senderInfo}\n{string.Join("\n", paramsList)}";
|
||
}
|
||
|
||
private void RefreshLiveTrackerView()
|
||
{
|
||
if (_liveTrackerScrollView == null) return;
|
||
_isLiveTrackerDirty = false;
|
||
_nextLiveTrackerRefreshTime = EditorApplication.timeSinceStartup + LiveTrackerRefreshIntervalSeconds;
|
||
|
||
_liveTrackerScrollView.Clear();
|
||
|
||
var filteredRecords = new List<EventLogRecord>(_logRecords.Count);
|
||
for (var i = _logRecords.Count - 1; i >= 0; i--)
|
||
{
|
||
var record = _logRecords[i];
|
||
if (MatchesLiveLogFilter(record))
|
||
filteredRecords.Add(record);
|
||
}
|
||
|
||
if (_liveLogStatsLabel != null)
|
||
{
|
||
_liveLogStatsLabel.text = _collapseSameEventType
|
||
? $"显示 {filteredRecords.Count} / {_logRecords.Count} 条,按事件类型折叠"
|
||
: $"显示 {filteredRecords.Count} / {_logRecords.Count} 条";
|
||
}
|
||
|
||
if (filteredRecords.Count == 0)
|
||
{
|
||
_liveTrackerScrollView.Add(new HelpBox("当前没有符合条件的实时事件日志。", HelpBoxMessageType.Info));
|
||
return;
|
||
}
|
||
|
||
if (_collapseSameEventType)
|
||
{
|
||
foreach (var group in BuildCollapsedGroups(filteredRecords))
|
||
_liveTrackerScrollView.Add(CreateCollapsedGroupElement(group));
|
||
return;
|
||
}
|
||
|
||
foreach (var record in filteredRecords)
|
||
_liveTrackerScrollView.Add(CreateLogEntryElement(record));
|
||
}
|
||
|
||
private bool MatchesLiveLogFilter(EventLogRecord record)
|
||
{
|
||
switch (_liveLogFilterMode)
|
||
{
|
||
case LiveLogFilterMode.HasListeners:
|
||
if (!record.HasListeners) return false;
|
||
break;
|
||
case LiveLogFilterMode.NoListeners:
|
||
if (record.HasListeners) return false;
|
||
break;
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(_liveLogSearchString))
|
||
return true;
|
||
|
||
return record.MatchText.IndexOf(_liveLogSearchString.Trim(), StringComparison.OrdinalIgnoreCase) >= 0;
|
||
}
|
||
|
||
private List<EventLogGroup> BuildCollapsedGroups(List<EventLogRecord> records)
|
||
{
|
||
var groups = new List<EventLogGroup>();
|
||
var groupMap = new Dictionary<string, EventLogGroup>(StringComparer.Ordinal);
|
||
|
||
foreach (var record in records)
|
||
{
|
||
if (!groupMap.TryGetValue(record.EventName, out var group))
|
||
{
|
||
group = new EventLogGroup { EventName = record.EventName };
|
||
groupMap[record.EventName] = group;
|
||
groups.Add(group);
|
||
}
|
||
|
||
group.Records.Add(record);
|
||
}
|
||
|
||
return groups;
|
||
}
|
||
|
||
private VisualElement CreateCollapsedGroupElement(EventLogGroup group)
|
||
{
|
||
var latest = group.LatestRecord;
|
||
var foldout = new Foldout
|
||
{
|
||
text = $"{group.EventName} × {group.Records.Count} 最近: {latest.TimestampText}",
|
||
value = group.Records.Count == 1,
|
||
style =
|
||
{
|
||
backgroundColor = new StyleColor(new Color(0.2f, 0.2f, 0.2f, 0.4f)),
|
||
marginTop = 2,
|
||
paddingBottom = 2,
|
||
borderBottomWidth = 1,
|
||
borderBottomColor = new StyleColor(new Color(0.1f, 0.1f, 0.1f))
|
||
}
|
||
};
|
||
|
||
var summary = new Label(
|
||
$"最近来源: {latest.SenderInfo} | 监听者: {(latest.HasListeners ? latest.Handlers.Count : 0)} | 已取消: {(latest.IsCanceled ? "是" : "否")}")
|
||
{
|
||
style =
|
||
{
|
||
marginLeft = 18,
|
||
marginBottom = 4,
|
||
color = new StyleColor(Color.gray)
|
||
}
|
||
};
|
||
foldout.Add(summary);
|
||
if (foldout.value)
|
||
PopulateCollapsedGroupDetails(foldout, group, 1);
|
||
foldout.RegisterValueChangedCallback(evt =>
|
||
{
|
||
if (evt.newValue)
|
||
{
|
||
PopulateCollapsedGroupDetails(foldout, group, 1);
|
||
return;
|
||
}
|
||
ClearFoldoutChildren(foldout, 1);
|
||
});
|
||
|
||
return foldout;
|
||
}
|
||
|
||
private void PopulateCollapsedGroupDetails(Foldout foldout, EventLogGroup group, int keepChildCount)
|
||
{
|
||
if (foldout.childCount > keepChildCount) return;
|
||
foreach (var record in group.Records)
|
||
{
|
||
var row = CreateLogEntryElement(record);
|
||
row.style.marginLeft = 12;
|
||
foldout.Add(row);
|
||
}
|
||
}
|
||
|
||
private void ClearFoldoutChildren(Foldout foldout, int keepChildCount)
|
||
{
|
||
while (foldout.childCount > keepChildCount)
|
||
foldout.RemoveAt(foldout.childCount - 1);
|
||
}
|
||
|
||
private VisualElement CreateLogEntryElement(EventLogRecord record)
|
||
{
|
||
var logEntry = new VisualElement
|
||
{
|
||
style =
|
||
{
|
||
flexDirection = FlexDirection.Column,
|
||
paddingBottom = 4,
|
||
paddingTop = 4,
|
||
borderBottomWidth = 1,
|
||
borderBottomColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f))
|
||
}
|
||
};
|
||
|
||
var headerRow = new VisualElement
|
||
{ style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } };
|
||
headerRow.Add(new Label(record.TimestampText)
|
||
{ style = { width = 80, color = new StyleColor(Color.gray) } });
|
||
headerRow.Add(new Label(record.EventName)
|
||
{
|
||
style = { color = new StyleColor(new Color(1f, 0.84f, 0f)), unityFontStyleAndWeight = FontStyle.Bold }
|
||
});
|
||
|
||
var senderClassName = GetSenderClassName(record.SenderInfo);
|
||
var senderLabel = new Label($" [由 {record.SenderInfo} 触发]")
|
||
{
|
||
style =
|
||
{
|
||
color = new StyleColor(new Color(0.5f, 0.8f, 1f)),
|
||
marginLeft = 5,
|
||
unityFontStyleAndWeight = FontStyle.Italic
|
||
}
|
||
};
|
||
|
||
if (senderClassName != "未知来源" && senderClassName != "EventBus" && senderClassName != "Unknown")
|
||
{
|
||
senderLabel.RegisterCallback<MouseEnterEvent>(_ =>
|
||
senderLabel.style.color = new StyleColor(Color.cyan));
|
||
senderLabel.RegisterCallback<MouseLeaveEvent>(_ =>
|
||
senderLabel.style.color = new StyleColor(new Color(0.5f, 0.8f, 1f)));
|
||
senderLabel.RegisterCallback<ClickEvent>(_ => OpenScriptByClassName(senderClassName));
|
||
}
|
||
|
||
headerRow.Add(senderLabel);
|
||
logEntry.Add(headerRow);
|
||
|
||
var statusRow = new VisualElement
|
||
{ style = { flexDirection = FlexDirection.Row, marginLeft = 80, marginTop = 2 } };
|
||
statusRow.Add(new Label($"可取消: {(record.IsCancelable ? "是" : "否")}")
|
||
{ style = { color = new StyleColor(Color.gray), marginRight = 15 } });
|
||
|
||
var canceledLabel = new Label($"已取消: {(record.IsCanceled ? "是" : "否")}")
|
||
{ style = { marginRight = 15 } };
|
||
canceledLabel.style.color =
|
||
record.IsCanceled ? new StyleColor(new Color(1f, 0.3f, 0.3f)) : new StyleColor(Color.gray);
|
||
statusRow.Add(canceledLabel);
|
||
|
||
statusRow.Add(new Label($"Result: {record.Result}")
|
||
{ style = { color = new StyleColor(new Color(0.8f, 0.6f, 0.8f)), marginRight = 15 } });
|
||
statusRow.Add(new Label($"最后阶段: {record.Phase}")
|
||
{ style = { color = new StyleColor(new Color(0.6f, 0.8f, 1f)) } });
|
||
logEntry.Add(statusRow);
|
||
|
||
var paramsContainer = new VisualElement
|
||
{ style = { flexDirection = FlexDirection.Column, marginLeft = 80, marginTop = 2, marginBottom = 2 } };
|
||
if (record.Parameters.Count == 0)
|
||
{
|
||
paramsContainer.Add(new Label("参数: { 无 }")
|
||
{ style = { color = new StyleColor(new Color(0.5f, 0.5f, 0.5f)) } });
|
||
}
|
||
else
|
||
{
|
||
paramsContainer.Add(
|
||
new Label("携带参数:") { style = { color = new StyleColor(new Color(0.6f, 0.8f, 1f)) } });
|
||
foreach (var pStr in record.Parameters)
|
||
paramsContainer.Add(new Label($" • {pStr}")
|
||
{ style = { color = new StyleColor(new Color(0.6f, 0.9f, 0.6f)), marginLeft = 10 } });
|
||
}
|
||
|
||
logEntry.Add(paramsContainer);
|
||
|
||
if (record.HasListeners)
|
||
{
|
||
foreach (var handler in record.Handlers)
|
||
logEntry.Add(CreateHandlerRow(handler, 80));
|
||
}
|
||
else
|
||
{
|
||
var noHandlerRow = new VisualElement
|
||
{ style = { flexDirection = FlexDirection.Row, marginLeft = 80, marginTop = 2 } };
|
||
noHandlerRow.Add(new Label("(无监听者响应)") { style = { color = new StyleColor(Color.gray) } });
|
||
logEntry.Add(noHandlerRow);
|
||
}
|
||
|
||
return logEntry;
|
||
}
|
||
|
||
private string GetSenderClassName(string senderInfo)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(senderInfo))
|
||
return "未知来源";
|
||
return senderInfo.Contains(".") ? senderInfo.Split('.')[0] : senderInfo;
|
||
}
|
||
|
||
private VisualElement CreateHandlerRow(object h)
|
||
{
|
||
return CreateHandlerRow(ExtractHandlerDisplayData(h), 40);
|
||
}
|
||
|
||
private VisualElement CreateHandlerRow(HandlerDisplayData handlerData, float marginLeft)
|
||
{
|
||
var targetInstance = handlerData.TargetInstance;
|
||
var targetName = handlerData.TargetName;
|
||
var methodName = handlerData.MethodName;
|
||
var priority = handlerData.Priority;
|
||
var execType = handlerData.ExecType;
|
||
|
||
var row = new VisualElement
|
||
{
|
||
style =
|
||
{
|
||
flexDirection = FlexDirection.Row, marginTop = 2, marginBottom = 2, marginLeft = marginLeft,
|
||
alignItems = Align.Center
|
||
}
|
||
};
|
||
|
||
row.Add(new Label($"[{priority}]") { style = { width = 75, color = new StyleColor(Color.gray) } });
|
||
row.Add(new Label($"[{execType}]")
|
||
{ style = { width = 50, color = new StyleColor(new Color(0.8f, 0.6f, 0.2f)) } });
|
||
|
||
var linkLabel = new Label($"{targetName}.{methodName}()")
|
||
{ style = { color = new StyleColor(new Color(0.3f, 0.6f, 1f)) } };
|
||
linkLabel.RegisterCallback<MouseEnterEvent>(e => linkLabel.style.color = new StyleColor(Color.cyan));
|
||
linkLabel.RegisterCallback<MouseLeaveEvent>(e =>
|
||
linkLabel.style.color = new StyleColor(new Color(0.3f, 0.6f, 1f)));
|
||
linkLabel.RegisterCallback<ClickEvent>(e => OpenScriptByClassName(targetName));
|
||
linkLabel.style.flexGrow = 1;
|
||
row.Add(linkLabel);
|
||
|
||
if (handlerData.PingTarget)
|
||
{
|
||
var pingBtn = new Button(() =>
|
||
{
|
||
EditorGUIUtility.PingObject(handlerData.PingTarget.gameObject);
|
||
Selection.activeGameObject = handlerData.PingTarget.gameObject;
|
||
}) { text = "Ping", style = { width = 50, height = 18 } };
|
||
row.Add(pingBtn);
|
||
}
|
||
else if (handlerData.IsStaticHandler && methodName != "未知方法")
|
||
row.Add(new Label("(静态)")
|
||
{
|
||
style = { width = 50, color = new StyleColor(Color.gray), unityTextAlign = TextAnchor.MiddleRight }
|
||
});
|
||
else
|
||
row.Add(new Label("(无对象)")
|
||
{
|
||
style = { width = 50, color = new StyleColor(Color.gray), unityTextAlign = TextAnchor.MiddleRight }
|
||
});
|
||
|
||
return row;
|
||
}
|
||
|
||
private HandlerDisplayData ExtractHandlerDisplayData(object handlerObj)
|
||
{
|
||
var priority = GetMemberValue(handlerObj, "Priority") ?? "NORMAL";
|
||
var debugInfo = GetMemberValue(handlerObj, "DebugInfo") as string ?? "未知方法";
|
||
|
||
var execType = "Sync";
|
||
if (debugInfo.Contains("(UniTask)") || debugInfo.Contains("Async")) execType = "Async";
|
||
else if (debugInfo.Contains("(Task->UniTask)") || debugInfo.Contains("(Task)")) execType = "Task";
|
||
|
||
object targetInstance = null;
|
||
var targetName = "未知类";
|
||
var methodName = debugInfo;
|
||
|
||
var handlerDelegate = GetMemberValue(handlerObj, "Handler") as Delegate;
|
||
if (handlerDelegate != null)
|
||
{
|
||
targetInstance = handlerDelegate.Target;
|
||
var methodInfo = handlerDelegate.Method;
|
||
|
||
if (targetInstance != null && targetInstance.GetType().Name.Contains("Wrapper"))
|
||
{
|
||
var realDelegate = GetMemberValue(targetInstance, "_originalAction") as Delegate;
|
||
if (realDelegate != null)
|
||
{
|
||
targetInstance = realDelegate.Target;
|
||
methodInfo = realDelegate.Method;
|
||
}
|
||
}
|
||
|
||
if (methodInfo != null)
|
||
{
|
||
var rawClassName = methodInfo.DeclaringType?.Name ?? "未知类";
|
||
if (rawClassName.Contains("<>c")) rawClassName = "匿名闭包类";
|
||
targetName = rawClassName;
|
||
methodName = methodInfo.Name;
|
||
}
|
||
}
|
||
|
||
return new HandlerDisplayData
|
||
{
|
||
TargetInstance = targetInstance,
|
||
PingTarget = targetInstance as MonoBehaviour,
|
||
TargetName = targetName,
|
||
MethodName = methodName,
|
||
Priority = priority,
|
||
ExecType = execType,
|
||
IsStaticHandler = targetInstance == null
|
||
};
|
||
}
|
||
|
||
private List<string> DumpEventParams(EventBase evt)
|
||
{
|
||
var values = new List<string>();
|
||
if (evt == null) return values;
|
||
|
||
var currentType = evt.GetType();
|
||
|
||
while (currentType != null && currentType != typeof(EventBase) && currentType != typeof(object))
|
||
{
|
||
var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance |
|
||
BindingFlags.DeclaredOnly;
|
||
|
||
foreach (var f in currentType.GetFields(flags))
|
||
{
|
||
if (f.Name.Contains("<") || f.Name.Contains(">")) continue;
|
||
if (f.Name is "CurrentHandler" or "Phase") continue;
|
||
values.Add($"{f.Name} = {FormatValue(f.GetValue(evt))}");
|
||
}
|
||
|
||
foreach (var p in currentType.GetProperties(flags))
|
||
{
|
||
if (p.Name is "CurrentHandler" or "Phase" or "IsCanceled" or "IsCancelable" or "Result") continue;
|
||
if (!p.CanRead || p.GetIndexParameters().Length > 0) continue;
|
||
try
|
||
{
|
||
values.Add($"{p.Name} = {FormatValue(p.GetValue(evt))}");
|
||
}
|
||
catch
|
||
{
|
||
// ignored
|
||
}
|
||
}
|
||
|
||
currentType = currentType.BaseType;
|
||
}
|
||
|
||
return values;
|
||
}
|
||
|
||
private string FormatValue(object val)
|
||
{
|
||
if (val == null) return "null";
|
||
if (val is UnityEngine.Object uObj) return uObj ? uObj.name : "null(已销毁)";
|
||
if (val is string s) return $"\"{s}\"";
|
||
if (val is Array arr) return $"[数组 Length:{arr.Length}]";
|
||
return val.ToString();
|
||
}
|
||
|
||
private object GetMemberValue(object obj, string memberName)
|
||
{
|
||
if (obj == null) return null;
|
||
var type = obj.GetType();
|
||
var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
|
||
|
||
var prop = type.GetProperty(memberName, flags);
|
||
if (prop != null) return prop.GetValue(obj);
|
||
|
||
var field = type.GetField(memberName, flags);
|
||
if (field != null) return field.GetValue(obj);
|
||
|
||
return null;
|
||
}
|
||
|
||
private void ClearLogs()
|
||
{
|
||
_logRecords.Clear();
|
||
RequestLiveTrackerRefresh(true);
|
||
}
|
||
|
||
private void OpenScriptByClassName(string className)
|
||
{
|
||
if (className == "匿名闭包类") return;
|
||
var guids = AssetDatabase.FindAssets($"{className} t:MonoScript");
|
||
foreach (var guid in guids)
|
||
{
|
||
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||
if (path.EndsWith($"{className}.cs"))
|
||
{
|
||
var script = AssetDatabase.LoadAssetAtPath<MonoScript>(path);
|
||
if (script)
|
||
{
|
||
AssetDatabase.OpenAsset(script);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
#endif
|