Implements the inbound GameEvent routing layer — the single biggest
network-protocol gap per r08 (94 sub-opcodes, zero handled before).
WorldSession now detects 0xF7B0, parses the 16-byte header (guid +
gameEventSequence + eventType), and forwards to a pluggable
GameEventDispatcher.
Added:
- GameEventEnvelope record + TryParse with layout from
ACE GameEventMessage.cs.
- GameEventType enum: all 94 S→C sub-opcodes from
ACE.Server.Network.GameEvent.GameEventType, named per ACE conventions.
- GameEventDispatcher: handler registry + unhandled-counts bag for
diagnostics ("which server events are firing that we don't parse?").
Handlers invoked synchronously on the decode thread; thrown exceptions
are swallowed + logged to stderr so one bad handler can't take down
the packet loop.
- GameEvents parsers: ChannelBroadcast, Tell, TransientMessage,
PopupString, WeenieError (+ WithString), UpdateHealth, PingResponse,
MagicUpdateSpell. Each returns a typed record or null on malformed
payload. String16L helper matches the existing CharacterList pattern
(u16 length + ASCII bytes + 4-byte pad).
- WorldSession.GameEvents property exposing the dispatcher so
GameWindow / UI / chat can register handlers at startup.
Wired into WorldSession.ProcessDatagram: new `else if (op ==
GameEventEnvelope.Opcode)` branch with TryParse + Dispatch.
Tests (13 new):
- Envelope: valid round-trip, wrong outer opcode, too-short body.
- Dispatcher: handler invoked, unhandled count, exception isolation,
unregister + rollover to unhandled.
- Event parsers: ChannelBroadcast, Tell, UpdateHealth, WeenieError,
Transient, MagicUpdateSpell.
Total: 521 tests pass (up from 508).
With this dispatcher in place, Phase F.2 (items + appraise), F.3 (combat
+ damage), F.4 (spell cast state machine), chat UI, allegiance, quest
tracker — all of which depend on GameEvent handling — are unblocked.
Ref: r08 §4 (GameEvent sub-opcode table), §2 (envelope wire shape).
Ref: ACE GameEventMessage.cs / GameEventType.cs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
57 lines
2 KiB
C#
57 lines
2 KiB
C#
using System;
|
||
using System.Buffers.Binary;
|
||
|
||
namespace AcDream.Core.Net.Messages;
|
||
|
||
/// <summary>
|
||
/// Parsed header of a retail AC <c>0xF7B0</c> GameEvent envelope (S→C).
|
||
///
|
||
/// <para>
|
||
/// Wire layout
|
||
/// (references/ACE/Source/ACE.Server/Network/GameEvent/GameEventMessage.cs):
|
||
/// <code>
|
||
/// u32 0xF7B0 // GameMessage opcode (GameEvent envelope)
|
||
/// u32 guid // session's player guid (0 if none yet)
|
||
/// u32 gameEventSequence // per-session, incremented by server
|
||
/// u32 eventType // GameEventType sub-opcode (94 values)
|
||
/// <payload> // variable; type-dispatched
|
||
/// </code>
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// Payload is returned as a <see cref="ReadOnlyMemory{T}"/> view into the
|
||
/// original message body — no copy. Per-event parsers slice from this.
|
||
/// </para>
|
||
/// </summary>
|
||
public readonly record struct GameEventEnvelope(
|
||
uint PlayerGuid,
|
||
uint Sequence,
|
||
GameEventType EventType,
|
||
ReadOnlyMemory<byte> Payload)
|
||
{
|
||
/// <summary>GameMessage opcode of the outer envelope.</summary>
|
||
public const uint Opcode = 0xF7B0u;
|
||
|
||
/// <summary>Header size (4×u32 = 16 bytes before payload).</summary>
|
||
public const int HeaderSize = 16;
|
||
|
||
/// <summary>
|
||
/// Parse a raw 0xF7B0 GameMessage body into the header + payload view.
|
||
/// Returns null if the body is shorter than the header or the outer
|
||
/// opcode doesn't match.
|
||
/// </summary>
|
||
public static GameEventEnvelope? TryParse(byte[] body)
|
||
{
|
||
if (body is null || body.Length < HeaderSize) return null;
|
||
|
||
uint outer = BinaryPrimitives.ReadUInt32LittleEndian(body);
|
||
if (outer != Opcode) return null;
|
||
|
||
uint guid = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(4));
|
||
uint sequence = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8));
|
||
uint eventType = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12));
|
||
|
||
var payload = body.AsMemory(HeaderSize);
|
||
return new GameEventEnvelope(guid, sequence, (GameEventType)eventType, payload);
|
||
}
|
||
}
|