feat: integrate composable Godot runtime and EventBus debugger
This commit is contained in:
55 files changed
+1481
-114
No files matched your search
@@ -0,0 +1,41 @@
|
||||
# ShrinkSDK.EventBus.Godot
|
||||
|
||||
Godot 4.7 编辑器调试窗口,用于查看 ShrinkSDK.EventBus 的订阅快照和实时分发结果。
|
||||
|
||||
## 安装
|
||||
|
||||
```powershell
|
||||
dotnet add package ShrinkSDK.EventBus.Godot --version 0.1.1
|
||||
```
|
||||
|
||||
取得包后,将:
|
||||
|
||||
```text
|
||||
contentFiles/any/any/addons/shrink_event_bus
|
||||
```
|
||||
|
||||
复制到 Godot 项目的:
|
||||
|
||||
```text
|
||||
addons/shrink_event_bus
|
||||
```
|
||||
|
||||
然后在 `Project > Project Settings > Plugins` 启用 **ShrinkEventBus**。包依赖 `ShrinkSDK.EventBus 2.3.1` 与 `GodotSharp 4.7.2`;在 Debug 构建中把 `ShrinkEventBusDebugger` 挂到常驻节点(例如主场景根节点)。
|
||||
|
||||
## 使用
|
||||
|
||||
运行项目后打开底部调试器的 `ShrinkEventBus` 标签页。点击“刷新快照”查看订阅,勾选“启用实时记录”采集后续分发。每个调试会话独立显示数据。
|
||||
|
||||
插件通过 `shrink_event_bus:state` 调试消息接收数据。每 250 ms 最多发送一次快照,保留最近 200 条,不保留事件对象。关闭记录会解除诊断订阅,停止游戏保留最后一份可查看的数据。Release 构建不包含采集实现。
|
||||
|
||||
诊断数据来自公开的 `EventBus.ObserveDiagnostics` 与 `ShrinkEventDiagnostic`,不依赖反射或内部事件。
|
||||
|
||||
## 可复用能力
|
||||
|
||||
<!-- shrink:capabilities -->
|
||||
Capability: Godot 编辑器中查看事件总线订阅快照与实时分发日志,按事件或 Bus 搜索并筛选处理状态
|
||||
Aliases: 事件 事件总线 可视化 调试 窗口 订阅 EventBus debugger
|
||||
Limits: 需要 Debug 构建和 Godot 调试连接;订阅快照仅列出静态 EventBus 管理的 Bus;诊断记录可包含独立 Host 的分发;无事件参数和发送者对象;最多保留 200 条记录
|
||||
Extension: editor/event_bus_debugger_panel.gd 的界面;ShrinkEventBusDebugger.cs 的诊断协议适配
|
||||
Evidence: [采集桥接](addons/shrink_event_bus/ShrinkEventBusDebugger.cs); [会话插件](addons/shrink_event_bus/editor/event_bus_debugger_plugin.gd); [窗口](addons/shrink_event_bus/editor/event_bus_debugger_panel.gd)
|
||||
<!-- /shrink:capabilities -->
|
||||
@@ -0,0 +1,35 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||
<AssemblyName>ShrinkSDK.EventBus.Godot</AssemblyName>
|
||||
<RootNamespace>ShrinkSDK.EventBus.Godot</RootNamespace>
|
||||
<PackageId>ShrinkSDK.EventBus.Godot</PackageId>
|
||||
<Version>0.1.1</Version>
|
||||
<Description>Godot editor debugger for ShrinkSDK.EventBus.</Description>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<IncludeSymbols>false</IncludeSymbols>
|
||||
<NoWarn>$(NoWarn);NU5128</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="addons\shrink_event_bus\ShrinkEventBusDebugger.cs" />
|
||||
<PackageReference Include="GodotSharp" Version="4.7.2" />
|
||||
<PackageReference Include="ShrinkSDK.EventBus" Version="2.3.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="README.md" Pack="true" PackagePath="\" />
|
||||
<None Include="addons\shrink_event_bus\README.md" Pack="true"
|
||||
PackagePath="contentFiles\any\any\addons\shrink_event_bus\README.md" BuildAction="None" />
|
||||
<None Include="addons\shrink_event_bus\plugin.cfg" Pack="true"
|
||||
PackagePath="contentFiles\any\any\addons\shrink_event_bus\plugin.cfg" BuildAction="None" />
|
||||
<None Include="addons\shrink_event_bus\plugin.gd" Pack="true"
|
||||
PackagePath="contentFiles\any\any\addons\shrink_event_bus\plugin.gd" BuildAction="None" />
|
||||
<None Include="addons\shrink_event_bus\*.uid" Pack="true"
|
||||
PackagePath="contentFiles\any\any\addons\shrink_event_bus\" BuildAction="None" />
|
||||
<None Include="addons\shrink_event_bus\editor\*" Pack="true"
|
||||
PackagePath="contentFiles\any\any\addons\shrink_event_bus\editor\" BuildAction="None" />
|
||||
<None Include="addons\shrink_event_bus\ShrinkEventBusDebugger.cs" Pack="true"
|
||||
PackagePath="contentFiles\any\any\addons\shrink_event_bus\ShrinkEventBusDebugger.cs" BuildAction="None" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,15 @@
|
||||
# Godot EventBus 调试器
|
||||
|
||||
运行项目后打开底部调试器的 `ShrinkEventBus` 标签页。点击“刷新快照”查看订阅,勾选“启用实时记录”采集后续分发。每个调试会话独立显示数据。
|
||||
|
||||
插件 UI 通过 Godot 调试连接接收 `shrink_event_bus:state`;运行时节点在常驻 App 下加载。每 250 ms 最多发送一次记录快照,保留最近 200 条,不保留事件对象。关闭记录解除观察者,停止游戏保留最后一份可查看的数据。
|
||||
|
||||
依赖 `ShrinkSDK.EventBus 2.3.0`:公开 `CaptureNamedBuses`、`CaptureBindings` 用于快照,`ObserveDiagnostics` 与 `ShrinkEventDiagnostic` 用于分发结果。Release 构建不包含采集实现。
|
||||
|
||||
<!-- shrink:capabilities -->
|
||||
Capability: Godot 编辑器中查看事件总线订阅快照与实时分发日志,按事件或 Bus 搜索并筛选处理状态
|
||||
Aliases: 事件 事件总线 可视化 调试 窗口 订阅 EventBus debugger
|
||||
Limits: 需要 Debug 构建和 Godot 调试连接;只观察静态 EventBus 管理的 Bus;无事件参数和发送者对象;最多保留 200 条记录
|
||||
Extension: editor/event_bus_debugger_panel.gd 的界面;ShrinkEventBusDebugger.cs 的诊断协议适配
|
||||
Evidence: [采集桥接](ShrinkEventBusDebugger.cs); [会话插件](editor/event_bus_debugger_plugin.gd); [窗口](editor/event_bus_debugger_panel.gd)
|
||||
<!-- /shrink:capabilities -->
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
#nullable enable
|
||||
using Godot;
|
||||
#if DEBUG
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ShrinkEventBus;
|
||||
using GArray = Godot.Collections.Array;
|
||||
using GDictionary = Godot.Collections.Dictionary;
|
||||
#endif
|
||||
|
||||
namespace ShrinkSDK.EventBus.Godot;
|
||||
|
||||
public partial class ShrinkEventBusDebugger : Node
|
||||
{
|
||||
#if DEBUG
|
||||
private const string Prefix = "shrink_event_bus";
|
||||
private const int Capacity = 200;
|
||||
private readonly object _gate = new();
|
||||
private readonly Queue<TraceRecord> _history = new();
|
||||
private IDisposable? _subscription;
|
||||
private bool _recording;
|
||||
private bool _registered;
|
||||
private long _discarded;
|
||||
private double _elapsed;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
if (!EngineDebugger.IsActive())
|
||||
return;
|
||||
ProcessMode = ProcessModeEnum.Always;
|
||||
EngineDebugger.RegisterMessageCapture(Prefix, Callable.From<string, GArray, bool>(HandleMessage));
|
||||
_registered = true;
|
||||
SendSnapshot();
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
if (!_registered)
|
||||
return;
|
||||
if (!EngineDebugger.IsActive())
|
||||
{
|
||||
SetRecording(false);
|
||||
return;
|
||||
}
|
||||
_elapsed += delta;
|
||||
if (_elapsed < 0.25)
|
||||
return;
|
||||
_elapsed = 0;
|
||||
if (_recording)
|
||||
SendSnapshot();
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
if (!_registered)
|
||||
return;
|
||||
SetRecording(false);
|
||||
EngineDebugger.UnregisterMessageCapture(Prefix);
|
||||
_registered = false;
|
||||
}
|
||||
|
||||
private bool HandleMessage(string message, GArray data)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case "set_debug_record":
|
||||
if (data is [{ VariantType: Variant.Type.Bool } _])
|
||||
SetRecording(data[0].AsBool());
|
||||
break;
|
||||
case "clear_history":
|
||||
lock (_gate)
|
||||
{
|
||||
_history.Clear();
|
||||
_discarded = 0;
|
||||
}
|
||||
break;
|
||||
case "request_snapshot":
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
SendSnapshot();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void SetRecording(bool enabled)
|
||||
{
|
||||
if (_recording == enabled)
|
||||
return;
|
||||
lock (_gate)
|
||||
_recording = enabled;
|
||||
if (enabled)
|
||||
{
|
||||
_subscription = global::ShrinkEventBus.EventBus.ObserveDiagnostics(Capture);
|
||||
}
|
||||
else
|
||||
{
|
||||
_subscription?.Dispose();
|
||||
_subscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void Capture(ShrinkEventDiagnostic diagnostic)
|
||||
{
|
||||
// 回调可能发生在工作线程:只留托管元数据,不触碰 Godot 对象或保留事件实例。
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_recording)
|
||||
return;
|
||||
var record = new TraceRecord(
|
||||
diagnostic.TimestampUtc,
|
||||
diagnostic.EventType,
|
||||
diagnostic.BusKey.ToString(),
|
||||
diagnostic.Result,
|
||||
diagnostic.ThreadId,
|
||||
diagnostic.ElapsedMilliseconds,
|
||||
diagnostic.IsAsync);
|
||||
if (_history.Count == Capacity)
|
||||
{
|
||||
_history.Dequeue();
|
||||
_discarded++;
|
||||
}
|
||||
_history.Enqueue(record);
|
||||
}
|
||||
}
|
||||
|
||||
private void SendSnapshot()
|
||||
{
|
||||
var groups = new GArray();
|
||||
var bindingCount = 0;
|
||||
var buses = global::ShrinkEventBus.EventBus.CaptureNamedBuses();
|
||||
foreach (var bus in buses)
|
||||
{
|
||||
foreach (var group in global::ShrinkEventBus.EventBus.CaptureBindings(bus).GroupBy(binding => binding.EventType))
|
||||
{
|
||||
var handlers = new GArray();
|
||||
foreach (var binding in group)
|
||||
{
|
||||
handlers.Add(new GDictionary
|
||||
{
|
||||
["declaring_type"] = binding.Handler.DeclaringType?.FullName ?? "",
|
||||
["method_name"] = binding.Handler.Name,
|
||||
["is_static"] = binding.Handler.IsStatic
|
||||
});
|
||||
}
|
||||
bindingCount += handlers.Count;
|
||||
groups.Add(new GDictionary
|
||||
{
|
||||
["event_name"] = $"[{bus.Key}] {group.Key.FullName}",
|
||||
["handlers"] = handlers
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
TraceRecord[] history;
|
||||
long discarded;
|
||||
lock (_gate)
|
||||
{
|
||||
history = [.. _history];
|
||||
discarded = _discarded;
|
||||
}
|
||||
var records = new GArray();
|
||||
foreach (var record in history)
|
||||
{
|
||||
records.Add(new GDictionary
|
||||
{
|
||||
["event_name"] = record.EventType.FullName ?? record.EventType.Name,
|
||||
["timestamp_utc"] = record.Timestamp.ToString("O"),
|
||||
["bus"] = record.Bus,
|
||||
["handled"] = record.Result.Handled,
|
||||
["accepted"] = record.Result.Accepted,
|
||||
["is_canceled"] = record.Result.Canceled,
|
||||
["failure"] = record.Result.Failure.ToString(),
|
||||
["thread_id"] = record.ThreadId,
|
||||
["elapsed_ms"] = record.ElapsedMs,
|
||||
["is_async"] = record.IsAsync
|
||||
});
|
||||
}
|
||||
EngineDebugger.SendMessage(Prefix + ":state", new GArray { new GDictionary
|
||||
{
|
||||
["debug_enabled"] = _recording,
|
||||
["bus_count"] = buses.Count,
|
||||
["binding_count"] = bindingCount,
|
||||
["discarded_count"] = discarded,
|
||||
["subscriber_groups"] = groups,
|
||||
["recent_logs"] = records
|
||||
} });
|
||||
}
|
||||
|
||||
private sealed record TraceRecord(DateTime Timestamp, Type EventType, string Bus,
|
||||
ShrinkPostResult Result, int ThreadId, double ElapsedMs, bool IsAsync);
|
||||
#endif
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
uid://dcvus446brme5
|
||||
+409
@@ -0,0 +1,409 @@
|
||||
@tool
|
||||
extends VBoxContainer
|
||||
|
||||
signal request_snapshot
|
||||
signal toggle_debug(enabled: bool)
|
||||
signal clear_requested
|
||||
|
||||
const MAX_VISIBLE_LOGS := 200
|
||||
|
||||
var _event_groups: Array = []
|
||||
var _live_logs: Array = []
|
||||
|
||||
var _status_label: Label
|
||||
var _debug_toggle: CheckButton
|
||||
var _search_input: LineEdit
|
||||
var _listener_filter: OptionButton
|
||||
var _collapse_check: CheckButton
|
||||
var _subscriber_list: VBoxContainer
|
||||
var _live_list: VBoxContainer
|
||||
var _session_active := false
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
name = "ShrinkEventBus"
|
||||
size_flags_horizontal = SIZE_EXPAND_FILL
|
||||
size_flags_vertical = SIZE_EXPAND_FILL
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_build_ui()
|
||||
set_session_active(_session_active)
|
||||
|
||||
|
||||
func set_session_active(active: bool) -> void:
|
||||
_session_active = active
|
||||
if not is_instance_valid(_status_label):
|
||||
return
|
||||
_debug_toggle.disabled = not active
|
||||
_debug_toggle.set_pressed_no_signal(false)
|
||||
_status_label.text = "等待运行时连接,点击刷新快照。" if active else "运行项目后连接事件总线。"
|
||||
|
||||
|
||||
func apply_snapshot(snapshot: Dictionary) -> void:
|
||||
if not is_instance_valid(_status_label):
|
||||
return
|
||||
|
||||
_debug_toggle.set_pressed_no_signal(bool(snapshot.get("debug_enabled", false)))
|
||||
_event_groups = _normalize_dictionary_array(snapshot.get("subscriber_groups", []))
|
||||
_live_logs = _normalize_dictionary_array(snapshot.get("recent_logs", []))
|
||||
if _live_logs.size() > MAX_VISIBLE_LOGS:
|
||||
_live_logs = _live_logs.slice(_live_logs.size() - MAX_VISIBLE_LOGS, _live_logs.size())
|
||||
|
||||
_status_label.text = "Bus: %d | 订阅: %d | 日志: %d / 200 | 已淘汰: %d" % [
|
||||
int(snapshot.get("bus_count", 0)),
|
||||
int(snapshot.get("binding_count", 0)),
|
||||
_live_logs.size(),
|
||||
int(snapshot.get("discarded_count", 0))
|
||||
]
|
||||
|
||||
_refresh_subscribers()
|
||||
_refresh_live_logs()
|
||||
|
||||
|
||||
func append_live_event(record: Dictionary) -> void:
|
||||
_live_logs.append(record)
|
||||
if _live_logs.size() > MAX_VISIBLE_LOGS:
|
||||
_live_logs.pop_front()
|
||||
_refresh_live_logs()
|
||||
|
||||
|
||||
func _build_ui() -> void:
|
||||
if _status_label != null:
|
||||
return
|
||||
|
||||
var toolbar := HBoxContainer.new()
|
||||
toolbar.size_flags_horizontal = SIZE_EXPAND_FILL
|
||||
add_child(toolbar)
|
||||
|
||||
_debug_toggle = CheckButton.new()
|
||||
_debug_toggle.text = "启用实时记录"
|
||||
_debug_toggle.toggled.connect(_on_debug_toggled)
|
||||
toolbar.add_child(_debug_toggle)
|
||||
|
||||
var refresh_button := Button.new()
|
||||
refresh_button.text = "刷新快照"
|
||||
refresh_button.pressed.connect(func() -> void: request_snapshot.emit())
|
||||
toolbar.add_child(refresh_button)
|
||||
|
||||
var clear_button := Button.new()
|
||||
clear_button.text = "清空日志"
|
||||
clear_button.pressed.connect(func() -> void: clear_requested.emit())
|
||||
toolbar.add_child(clear_button)
|
||||
|
||||
var spacer := Control.new()
|
||||
spacer.size_flags_horizontal = SIZE_EXPAND_FILL
|
||||
toolbar.add_child(spacer)
|
||||
|
||||
_status_label = Label.new()
|
||||
_status_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
toolbar.add_child(_status_label)
|
||||
|
||||
var filter_row := HBoxContainer.new()
|
||||
filter_row.size_flags_horizontal = SIZE_EXPAND_FILL
|
||||
add_child(filter_row)
|
||||
|
||||
_search_input = LineEdit.new()
|
||||
_search_input.placeholder_text = "按事件名 / Bus / 方法名过滤"
|
||||
_search_input.size_flags_horizontal = SIZE_EXPAND_FILL
|
||||
_search_input.text_changed.connect(_on_filter_changed)
|
||||
filter_row.add_child(_search_input)
|
||||
|
||||
_listener_filter = OptionButton.new()
|
||||
_listener_filter.add_item("全部事件", 0)
|
||||
_listener_filter.add_item("已处理 / 有订阅", 1)
|
||||
_listener_filter.add_item("未处理 / 无订阅", 2)
|
||||
_listener_filter.item_selected.connect(_on_filter_changed)
|
||||
filter_row.add_child(_listener_filter)
|
||||
|
||||
_collapse_check = CheckButton.new()
|
||||
_collapse_check.text = "同类事件折叠"
|
||||
_collapse_check.button_pressed = true
|
||||
_collapse_check.toggled.connect(_on_filter_changed)
|
||||
filter_row.add_child(_collapse_check)
|
||||
|
||||
var tab_container := TabContainer.new()
|
||||
tab_container.size_flags_horizontal = SIZE_EXPAND_FILL
|
||||
tab_container.size_flags_vertical = SIZE_EXPAND_FILL
|
||||
add_child(tab_container)
|
||||
|
||||
var subscriber_scroll := ScrollContainer.new()
|
||||
subscriber_scroll.name = "订阅者全览"
|
||||
subscriber_scroll.size_flags_horizontal = SIZE_EXPAND_FILL
|
||||
subscriber_scroll.size_flags_vertical = SIZE_EXPAND_FILL
|
||||
tab_container.add_child(subscriber_scroll)
|
||||
|
||||
_subscriber_list = VBoxContainer.new()
|
||||
_subscriber_list.size_flags_horizontal = SIZE_EXPAND_FILL
|
||||
_subscriber_list.size_flags_vertical = SIZE_EXPAND_FILL
|
||||
subscriber_scroll.add_child(_subscriber_list)
|
||||
|
||||
var live_scroll := ScrollContainer.new()
|
||||
live_scroll.name = "实时触发日志"
|
||||
live_scroll.size_flags_horizontal = SIZE_EXPAND_FILL
|
||||
live_scroll.size_flags_vertical = SIZE_EXPAND_FILL
|
||||
tab_container.add_child(live_scroll)
|
||||
|
||||
_live_list = VBoxContainer.new()
|
||||
_live_list.size_flags_horizontal = SIZE_EXPAND_FILL
|
||||
_live_list.size_flags_vertical = SIZE_EXPAND_FILL
|
||||
live_scroll.add_child(_live_list)
|
||||
|
||||
|
||||
func _refresh_subscribers() -> void:
|
||||
for child in _subscriber_list.get_children():
|
||||
_subscriber_list.remove_child(child)
|
||||
child.queue_free()
|
||||
|
||||
var groups := _event_groups.duplicate(true)
|
||||
groups.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
return String(a.get("event_name", "")) < String(b.get("event_name", ""))
|
||||
)
|
||||
|
||||
for group in groups:
|
||||
var event_name := String(group.get("event_name", ""))
|
||||
var handlers := _normalize_dictionary_array(group.get("handlers", []))
|
||||
if not _passes_text_filter(event_name, handlers):
|
||||
continue
|
||||
if not _passes_listener_filter(handlers.size()):
|
||||
continue
|
||||
|
||||
var foldout := EventBusFoldout.new()
|
||||
foldout.title = "%s (%d)" % [event_name, handlers.size()]
|
||||
foldout.collapsed = false
|
||||
_subscriber_list.add_child(foldout)
|
||||
|
||||
if handlers.is_empty():
|
||||
foldout.add_content(_make_label("当前没有订阅者。"))
|
||||
continue
|
||||
|
||||
for handler in handlers:
|
||||
foldout.add_content(_make_handler_row(handler))
|
||||
|
||||
if _subscriber_list.get_child_count() == 0:
|
||||
_subscriber_list.add_child(_make_label("暂无匹配订阅信息。"))
|
||||
|
||||
|
||||
func _refresh_live_logs() -> void:
|
||||
for child in _live_list.get_children():
|
||||
_live_list.remove_child(child)
|
||||
child.queue_free()
|
||||
|
||||
var filtered := _get_filtered_logs()
|
||||
if filtered.is_empty():
|
||||
_live_list.add_child(_make_label("暂无匹配日志。"))
|
||||
return
|
||||
|
||||
if _collapse_check.button_pressed:
|
||||
for group in _collapse_records(filtered):
|
||||
_live_list.add_child(_make_live_group(group))
|
||||
return
|
||||
|
||||
for record in filtered:
|
||||
_live_list.add_child(_make_live_record(record))
|
||||
|
||||
|
||||
func _get_filtered_logs() -> Array:
|
||||
var records: Array = []
|
||||
for record in _live_logs:
|
||||
if record is not Dictionary:
|
||||
continue
|
||||
if not _passes_listener_filter(1 if bool(record.get("handled", false)) else 0):
|
||||
continue
|
||||
if not _passes_text_filter(
|
||||
String(record.get("event_name", "")) + " " + String(record.get("bus", "")),
|
||||
_normalize_dictionary_array(record.get("handlers", [])),
|
||||
_normalize_string_array(record.get("payload_lines", []))
|
||||
):
|
||||
continue
|
||||
records.append(record)
|
||||
return records
|
||||
|
||||
|
||||
func _passes_text_filter(base_text: String, handlers: Array, payload_lines: Array = []) -> bool:
|
||||
var keyword := _search_input.text.strip_edges().to_lower()
|
||||
if keyword == "":
|
||||
return true
|
||||
|
||||
var haystack := base_text.to_lower()
|
||||
for payload in payload_lines:
|
||||
haystack += " " + String(payload).to_lower()
|
||||
for handler in handlers:
|
||||
if handler is Dictionary:
|
||||
haystack += " " + String(handler.get("declaring_type", "")).to_lower()
|
||||
haystack += " " + String(handler.get("method_name", "")).to_lower()
|
||||
return haystack.find(keyword) != -1
|
||||
|
||||
|
||||
func _passes_listener_filter(listener_count: int) -> bool:
|
||||
match _listener_filter.selected:
|
||||
1:
|
||||
return listener_count > 0
|
||||
2:
|
||||
return listener_count == 0
|
||||
_:
|
||||
return true
|
||||
|
||||
|
||||
func _collapse_records(records: Array) -> Array:
|
||||
var groups: Array = []
|
||||
var current_event_name := ""
|
||||
var current_records: Array = []
|
||||
|
||||
for record in records:
|
||||
var event_name := "[%s] %s" % [String(record.get("bus", "")), String(record.get("event_name", ""))]
|
||||
if current_records.is_empty() or event_name == current_event_name:
|
||||
current_event_name = event_name
|
||||
current_records.append(record)
|
||||
continue
|
||||
|
||||
groups.append({
|
||||
"event_name": current_event_name,
|
||||
"records": current_records.duplicate(true)
|
||||
})
|
||||
current_event_name = event_name
|
||||
current_records.clear()
|
||||
current_records.append(record)
|
||||
|
||||
if not current_records.is_empty():
|
||||
groups.append({
|
||||
"event_name": current_event_name,
|
||||
"records": current_records.duplicate(true)
|
||||
})
|
||||
|
||||
return groups
|
||||
|
||||
|
||||
func _make_live_group(group: Dictionary) -> Control:
|
||||
var records: Array = group.get("records", [])
|
||||
var latest: Dictionary = records[-1] if not records.is_empty() else {}
|
||||
var foldout := EventBusFoldout.new()
|
||||
foldout.title = "%s x%d" % [String(group.get("event_name", "未知事件")), records.size()]
|
||||
foldout.collapsed = false
|
||||
foldout.add_content(_make_live_record(latest))
|
||||
|
||||
if records.size() > 1:
|
||||
var details := EventBusFoldout.new()
|
||||
details.title = "展开全部 %d 条" % records.size()
|
||||
details.collapsed = true
|
||||
for record in records:
|
||||
details.add_content(_make_live_record(record))
|
||||
foldout.add_content(details)
|
||||
|
||||
return foldout
|
||||
|
||||
|
||||
func _make_live_record(record: Dictionary) -> Control:
|
||||
var root := VBoxContainer.new()
|
||||
root.add_theme_constant_override("separation", 4)
|
||||
root.add_child(_make_label("[%s] %s %s UTC" % [
|
||||
String(record.get("bus", "")), String(record.get("event_name", "")),
|
||||
_format_timestamp(String(record.get("timestamp_utc", "")))
|
||||
]))
|
||||
root.add_child(_make_label("%s | 接受: %s | 处理: %s | 取消: %s | %s | %.3f ms | 线程 %d" % [
|
||||
String(record.get("failure", "None")),
|
||||
"是" if bool(record.get("accepted", false)) else "否",
|
||||
"是" if bool(record.get("handled", false)) else "否",
|
||||
"是" if bool(record.get("is_canceled", false)) else "否",
|
||||
"异步" if bool(record.get("is_async", false)) else "同步",
|
||||
float(record.get("elapsed_ms", 0.0)), int(record.get("thread_id", 0))
|
||||
]))
|
||||
return _wrap_card(root)
|
||||
|
||||
|
||||
func _make_handler_row(handler: Dictionary) -> Control:
|
||||
return _make_label("%s.%s (%s)" % [
|
||||
String(handler.get("declaring_type", "")), String(handler.get("method_name", "")),
|
||||
"静态绑定方法" if bool(handler.get("is_static", false)) else "实例绑定方法"
|
||||
])
|
||||
|
||||
func _make_label(text: String) -> Label:
|
||||
var label := Label.new()
|
||||
label.text = text
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
return label
|
||||
|
||||
|
||||
func _wrap_card(content: Control) -> PanelContainer:
|
||||
var panel := PanelContainer.new()
|
||||
panel.size_flags_horizontal = SIZE_EXPAND_FILL
|
||||
panel.add_child(content)
|
||||
return panel
|
||||
|
||||
|
||||
func _normalize_dictionary_array(value) -> Array:
|
||||
var result: Array = []
|
||||
if value is Array:
|
||||
for item in value:
|
||||
if item is Dictionary:
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
|
||||
func _normalize_string_array(value) -> Array:
|
||||
var result: Array = []
|
||||
if value is Array:
|
||||
for item in value:
|
||||
result.append(String(item))
|
||||
return result
|
||||
|
||||
|
||||
func _format_timestamp(timestamp: String) -> String:
|
||||
if timestamp == "":
|
||||
return "--"
|
||||
var parts := timestamp.split("T")
|
||||
if parts.size() < 2:
|
||||
return timestamp
|
||||
return parts[1].trim_suffix("Z")
|
||||
|
||||
|
||||
func _on_debug_toggled(enabled: bool) -> void:
|
||||
toggle_debug.emit(enabled)
|
||||
|
||||
|
||||
func _on_filter_changed(_value = null) -> void:
|
||||
_refresh_subscribers()
|
||||
_refresh_live_logs()
|
||||
|
||||
|
||||
class EventBusFoldout:
|
||||
extends VBoxContainer
|
||||
|
||||
var title := "":
|
||||
set(value):
|
||||
title = value
|
||||
if _button != null:
|
||||
_button.text = value
|
||||
|
||||
var collapsed := false:
|
||||
set(value):
|
||||
collapsed = value
|
||||
if _button != null:
|
||||
_button.button_pressed = not value
|
||||
if _body != null:
|
||||
_body.visible = not value
|
||||
|
||||
var _button: CheckButton
|
||||
var _body: VBoxContainer
|
||||
|
||||
func _ready() -> void:
|
||||
if _button != null:
|
||||
return
|
||||
|
||||
_button = CheckButton.new()
|
||||
_button.text = title
|
||||
_button.button_pressed = not collapsed
|
||||
_button.toggled.connect(func(pressed: bool) -> void:
|
||||
collapsed = not pressed
|
||||
)
|
||||
add_child(_button)
|
||||
|
||||
_body = VBoxContainer.new()
|
||||
_body.visible = not collapsed
|
||||
_body.size_flags_horizontal = SIZE_EXPAND_FILL
|
||||
add_child(_body)
|
||||
|
||||
func add_content(control: Control) -> void:
|
||||
if _body == null:
|
||||
_ready()
|
||||
_body.add_child(control)
|
||||
+1
@@ -0,0 +1 @@
|
||||
uid://c8jc4n74owcks
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
@tool
|
||||
extends EditorDebuggerPlugin
|
||||
|
||||
const PREFIX := "shrink_event_bus"
|
||||
const PANEL_SCRIPT := preload("res://addons/shrink_event_bus/editor/event_bus_debugger_panel.gd")
|
||||
var _panels: Dictionary = {}
|
||||
var _connections: Array = []
|
||||
|
||||
|
||||
func _has_capture(prefix: String) -> bool:
|
||||
return prefix == PREFIX
|
||||
|
||||
|
||||
func _setup_session(session_id: int) -> void:
|
||||
var session := get_session(session_id)
|
||||
var panel = PANEL_SCRIPT.new()
|
||||
_panels[session_id] = panel
|
||||
panel.request_snapshot.connect(func():
|
||||
if session.is_active():
|
||||
session.send_message(PREFIX + ":request_snapshot", [])
|
||||
)
|
||||
panel.toggle_debug.connect(func(enabled: bool):
|
||||
if session.is_active():
|
||||
session.send_message(PREFIX + ":set_debug_record", [enabled])
|
||||
)
|
||||
panel.clear_requested.connect(func():
|
||||
if session.is_active():
|
||||
session.send_message(PREFIX + ":clear_history", [])
|
||||
)
|
||||
var started := func():
|
||||
panel.set_session_active(true)
|
||||
session.send_message(PREFIX + ":request_snapshot", [])
|
||||
var stopped := func(): panel.set_session_active(false)
|
||||
session.started.connect(started)
|
||||
session.stopped.connect(stopped)
|
||||
_connections.append([session, started, stopped])
|
||||
session.add_session_tab(panel)
|
||||
panel.set_session_active(session.is_active())
|
||||
|
||||
|
||||
func dispose_panels() -> void:
|
||||
for connection in _connections:
|
||||
var session = connection[0]
|
||||
if session.started.is_connected(connection[1]):
|
||||
session.started.disconnect(connection[1])
|
||||
if session.stopped.is_connected(connection[2]):
|
||||
session.stopped.disconnect(connection[2])
|
||||
_connections.clear()
|
||||
for session_id in _panels:
|
||||
var panel = _panels[session_id]
|
||||
if is_instance_valid(panel):
|
||||
get_session(session_id).remove_session_tab(panel)
|
||||
panel.queue_free()
|
||||
_panels.clear()
|
||||
|
||||
|
||||
func _capture(message: String, data: Array, session_id: int) -> bool:
|
||||
if message != PREFIX + ":state":
|
||||
return false
|
||||
var panel = _panels.get(session_id)
|
||||
if is_instance_valid(panel) and not data.is_empty() and data[0] is Dictionary:
|
||||
panel.apply_snapshot(data[0])
|
||||
return true
|
||||
+1
@@ -0,0 +1 @@
|
||||
uid://dlkjwusf3memk
|
||||
@@ -0,0 +1,6 @@
|
||||
[plugin]
|
||||
name="ShrinkEventBus"
|
||||
description="ShrinkSDK.EventBus 2.3.1 的订阅快照与实时事件调试窗口。"
|
||||
author="cneicy"
|
||||
version="0.1.1"
|
||||
script="plugin.gd"
|
||||
@@ -0,0 +1,18 @@
|
||||
@tool
|
||||
extends EditorPlugin
|
||||
|
||||
const DEBUGGER_PLUGIN_SCRIPT := preload("res://addons/shrink_event_bus/editor/event_bus_debugger_plugin.gd")
|
||||
|
||||
var _debugger_plugin: EditorDebuggerPlugin
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
_debugger_plugin = DEBUGGER_PLUGIN_SCRIPT.new()
|
||||
add_debugger_plugin(_debugger_plugin)
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if _debugger_plugin != null:
|
||||
_debugger_plugin.dispose_panels()
|
||||
remove_debugger_plugin(_debugger_plugin)
|
||||
_debugger_plugin = null
|
||||
@@ -0,0 +1 @@
|
||||
uid://2r868eolxj3m
|
||||
Reference in new issue
Block a user