Decode headers, optional fields, fragments, and single-fragment messages directly over pooled datagrams. Copy only fragment state that crosses a datagram lifetime, preserve synchronous dispatch and ACK ordering, and lock the path to the owned decoder with differential and zero-allocation tests.
152 lines
5.7 KiB
C#
152 lines
5.7 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
|
|
namespace AcDream.Core.Net.Messages;
|
|
|
|
/// <summary>
|
|
/// Central router for inbound <c>0xF7B0</c> GameEvent envelopes.
|
|
///
|
|
/// <para>
|
|
/// Each handled <see cref="GameEventType"/> 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.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// Handlers are invoked synchronously on the thread that called
|
|
/// <see cref="Dispatch"/> — normally the render thread since
|
|
/// <see cref="WorldSession"/>'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.
|
|
/// </para>
|
|
/// </summary>
|
|
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<GameEventType, RegistrationNode> _handlers = new();
|
|
private readonly Dictionary<GameEventType, int> _unhandledCounts = new();
|
|
|
|
/// <summary>
|
|
/// Register a handler for a GameEvent sub-opcode. Replaces any
|
|
/// existing handler for that opcode.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove the registered handler for a sub-opcode.
|
|
/// </summary>
|
|
public void Unregister(GameEventType type)
|
|
{
|
|
if (_handlers.Remove(type, out RegistrationNode? current))
|
|
current.Retired = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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 <paramref name="envelope"/> or its payload.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>Number of events of the given type we've seen with no handler.</summary>
|
|
public int GetUnhandledCount(GameEventType type) =>
|
|
_unhandledCounts.TryGetValue(type, out var n) ? n : 0;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public IReadOnlyDictionary<GameEventType, int> UnhandledCounts => _unhandledCounts;
|
|
|
|
/// <summary>Reset the unhandled-counts bag (e.g. after a log-off).</summary>
|
|
public void ResetUnhandledCounts() => _unhandledCounts.Clear();
|
|
|
|
/// <summary>How many distinct sub-opcodes have a handler registered.</summary>
|
|
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;
|
|
}
|
|
}
|