using System; using System.Collections.Generic; namespace AcDream.Core.Net.Messages; /// /// Central router for inbound 0xF7B0 GameEvent envelopes. /// /// /// Each handled gets a registered delegate. /// Unhandled types are counted for diagnostics so it's easy to see which /// events the server is actually emitting vs which ones we theoretically /// support — a useful number when iterating on chat / inventory / combat. /// /// /// /// Handlers are invoked synchronously on the thread that called /// — normally the render thread since /// 's decode path runs there in the current /// architecture. Handlers must not block or retain the envelope/payload: /// production payload memory borrows a pooled inbound datagram and expires /// when the synchronous dispatch call returns. Handlers must parse or copy /// any state they need to keep. /// /// public sealed class GameEventDispatcher { public delegate void EventHandler(GameEventEnvelope envelope); private sealed class RegistrationNode( GameEventType type, EventHandler handler, RegistrationNode? previous) { public GameEventType Type { get; } = type; public EventHandler Handler { get; } = handler; public RegistrationNode? Previous { get; } = previous; public bool Retired { get; set; } } private sealed class OwnedRegistration( GameEventDispatcher owner, RegistrationNode node) : IDisposable { private Action? _retire = () => owner.Retire(node); public void Dispose() => Interlocked.Exchange(ref _retire, null)?.Invoke(); } private readonly Dictionary _handlers = new(); private readonly Dictionary _unhandledCounts = new(); /// /// Register a handler for a GameEvent sub-opcode. Replaces any /// existing handler for that opcode. /// public void Register(GameEventType type, EventHandler handler) { ArgumentNullException.ThrowIfNull(handler); // Legacy replacement intentionally severs the predecessor chain. An // exact owned token installed earlier must never remove or expose a // later unowned replacement when that token is disposed. _handlers[type] = new RegistrationNode(type, handler, previous: null); } /// /// Installs one exact owned handler. Disposal restores the nearest live /// predecessor only when this registration is still current. Nested A/B /// ownership is safe in either disposal order. /// public IDisposable RegisterOwned(GameEventType type, EventHandler handler) { ArgumentNullException.ThrowIfNull(handler); _handlers.TryGetValue(type, out RegistrationNode? previous); var node = new RegistrationNode(type, handler, previous); _handlers[type] = node; return new OwnedRegistration(this, node); } /// /// Remove the registered handler for a sub-opcode. /// public void Unregister(GameEventType type) { if (_handlers.Remove(type, out RegistrationNode? current)) current.Retired = true; } /// /// Route an envelope to its handler, or log as unhandled. Exceptions /// inside handlers are swallowed to keep the decode loop alive — a /// malformed event from the server should not crash the client. The /// handler must not retain or its payload. /// public void Dispatch(GameEventEnvelope envelope) { if (_handlers.TryGetValue(envelope.EventType, out RegistrationNode? registration)) { try { registration.Handler(envelope); } catch (Exception ex) { // The decode thread must survive handler failures. Log via // Console so it surfaces in live-play logs without needing // a Serilog sink here. Console.Error.WriteLine( $"[GameEvent] handler for 0x{(uint)envelope.EventType:X4} threw: {ex.Message}"); } } else { _unhandledCounts.TryGetValue(envelope.EventType, out int n); _unhandledCounts[envelope.EventType] = n + 1; } } /// Number of events of the given type we've seen with no handler. public int GetUnhandledCount(GameEventType type) => _unhandledCounts.TryGetValue(type, out var n) ? n : 0; /// /// Snapshot of every event type we've seen without a handler, keyed /// by type → count. Useful for "which server events are firing that /// we don't parse?" diagnostic overlays. /// public IReadOnlyDictionary UnhandledCounts => _unhandledCounts; /// Reset the unhandled-counts bag (e.g. after a log-off). public void ResetUnhandledCounts() => _unhandledCounts.Clear(); /// How many distinct sub-opcodes have a handler registered. public int RegisteredHandlerCount => _handlers.Count; private void Retire(RegistrationNode node) { node.Retired = true; if (!_handlers.TryGetValue(node.Type, out RegistrationNode? current) || !ReferenceEquals(current, node)) return; RegistrationNode? predecessor = node.Previous; while (predecessor?.Retired == true) predecessor = predecessor.Previous; if (predecessor is null) _handlers.Remove(node.Type); else _handlers[node.Type] = predecessor; } }