#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 _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(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 }