using System; using System.Buffers.Binary; using System.Text; using AcDream.Core.Net.Messages; using Xunit; namespace AcDream.Core.Net.Tests.Messages; public sealed class GameEventDispatcherTests { // Helper: build a 0xF7B0 envelope with given sub-opcode + payload bytes. private static byte[] MakeEnvelope(GameEventType type, ReadOnlySpan payload, uint playerGuid = 0x12345678u, uint sequence = 0) { byte[] body = new byte[GameEventEnvelope.HeaderSize + payload.Length]; var span = body.AsSpan(); BinaryPrimitives.WriteUInt32LittleEndian(span, GameEventEnvelope.Opcode); BinaryPrimitives.WriteUInt32LittleEndian(span.Slice(4), playerGuid); BinaryPrimitives.WriteUInt32LittleEndian(span.Slice(8), sequence); BinaryPrimitives.WriteUInt32LittleEndian(span.Slice(12), (uint)type); payload.CopyTo(span.Slice(GameEventEnvelope.HeaderSize)); return body; } [Fact] public void TryParse_ValidEnvelope_ReturnsFields() { byte[] body = MakeEnvelope(GameEventType.PlayerDescription, payload: new byte[8], playerGuid: 0xAABBCCDD, sequence: 42); var env = GameEventEnvelope.TryParse(body); Assert.NotNull(env); Assert.Equal(0xAABBCCDDu, env!.Value.PlayerGuid); Assert.Equal(42u, env.Value.Sequence); Assert.Equal(GameEventType.PlayerDescription, env.Value.EventType); Assert.Equal(8, env.Value.Payload.Length); } [Fact] public void TryParse_WrongOuterOpcode_ReturnsNull() { byte[] body = new byte[16]; BinaryPrimitives.WriteUInt32LittleEndian(body, 0xDEADBEEFu); Assert.Null(GameEventEnvelope.TryParse(body)); } [Fact] public void TryParse_TooShort_ReturnsNull() { Assert.Null(GameEventEnvelope.TryParse(new byte[10])); } [Fact] public void Dispatch_CallsHandler_ForRegisteredType() { var dispatcher = new GameEventDispatcher(); GameEventEnvelope received = default; int calls = 0; dispatcher.Register(GameEventType.Tell, env => { received = env; calls++; }); var body = MakeEnvelope(GameEventType.Tell, new byte[4]); var env = GameEventEnvelope.TryParse(body); dispatcher.Dispatch(env!.Value); Assert.Equal(1, calls); Assert.Equal(GameEventType.Tell, received.EventType); } [Fact] public void Dispatch_Unhandled_CountedButNoThrow() { var dispatcher = new GameEventDispatcher(); var body = MakeEnvelope(GameEventType.HouseProfile, Array.Empty()); var env = GameEventEnvelope.TryParse(body); dispatcher.Dispatch(env!.Value); dispatcher.Dispatch(env.Value); Assert.Equal(2, dispatcher.GetUnhandledCount(GameEventType.HouseProfile)); } [Fact] public void Dispatch_HandlerThrows_DoesNotPropagate() { var dispatcher = new GameEventDispatcher(); dispatcher.Register(GameEventType.Tell, _ => throw new InvalidOperationException("bad handler")); var body = MakeEnvelope(GameEventType.Tell, Array.Empty()); // Should not throw; error is logged instead. dispatcher.Dispatch(GameEventEnvelope.TryParse(body)!.Value); } [Fact] public void Unregister_RemovesHandler_AndMovesToUnhandled() { var dispatcher = new GameEventDispatcher(); int calls = 0; dispatcher.Register(GameEventType.Tell, _ => calls++); dispatcher.Unregister(GameEventType.Tell); var body = MakeEnvelope(GameEventType.Tell, Array.Empty()); dispatcher.Dispatch(GameEventEnvelope.TryParse(body)!.Value); Assert.Equal(0, calls); Assert.Equal(1, dispatcher.GetUnhandledCount(GameEventType.Tell)); } // ── Per-event parser tests ─────────────────────────────────────────────── private static byte[] MakeString16L(string s) { byte[] data = Encoding.ASCII.GetBytes(s); int recordSize = 2 + data.Length; int padding = (4 - (recordSize & 3)) & 3; byte[] result = new byte[recordSize + padding]; BinaryPrimitives.WriteUInt16LittleEndian(result, (ushort)data.Length); Array.Copy(data, 0, result, 2, data.Length); return result; } [Fact] public void ParseChannelBroadcast_RoundTrip() { // u32 channelId + string16L sender + string16L message byte[] chan = new byte[4]; BinaryPrimitives.WriteUInt32LittleEndian(chan, 42); byte[] sender = MakeString16L("Hero"); byte[] msg = MakeString16L("hello allegiance"); byte[] payload = new byte[chan.Length + sender.Length + msg.Length]; Buffer.BlockCopy(chan, 0, payload, 0, chan.Length); Buffer.BlockCopy(sender, 0, payload, chan.Length, sender.Length); Buffer.BlockCopy(msg, 0, payload, chan.Length + sender.Length, msg.Length); var parsed = GameEvents.ParseChannelBroadcast(payload); Assert.NotNull(parsed); Assert.Equal(42u, parsed!.Value.ChannelId); Assert.Equal("Hero", parsed.Value.SenderName); Assert.Equal("hello allegiance", parsed.Value.Message); } [Fact] public void ParseTell_RoundTrip() { byte[] msg = MakeString16L("hi"); byte[] sender = MakeString16L("Alice"); byte[] tail = new byte[12]; // u32 senderGuid + u32 targetGuid + u32 chatType BinaryPrimitives.WriteUInt32LittleEndian(tail, 0xAAu); BinaryPrimitives.WriteUInt32LittleEndian(tail.AsSpan(4), 0xBBu); BinaryPrimitives.WriteUInt32LittleEndian(tail.AsSpan(8), 1u); byte[] payload = new byte[msg.Length + sender.Length + tail.Length]; Buffer.BlockCopy(msg, 0, payload, 0, msg.Length); Buffer.BlockCopy(sender, 0, payload, msg.Length, sender.Length); Buffer.BlockCopy(tail, 0, payload, msg.Length + sender.Length, tail.Length); var parsed = GameEvents.ParseTell(payload); Assert.NotNull(parsed); Assert.Equal("hi", parsed!.Value.Message); Assert.Equal("Alice", parsed.Value.SenderName); Assert.Equal(0xAAu, parsed.Value.SenderGuid); Assert.Equal(0xBBu, parsed.Value.TargetGuid); Assert.Equal(1u, parsed.Value.ChatType); } [Fact] public void ParseUpdateHealth_RoundTrip() { byte[] payload = new byte[8]; BinaryPrimitives.WriteUInt32LittleEndian(payload, 0xC0DEu); BinaryPrimitives.WriteSingleLittleEndian(payload.AsSpan(4), 0.42f); var parsed = GameEvents.ParseUpdateHealth(payload); Assert.NotNull(parsed); Assert.Equal(0xC0DEu, parsed!.Value.TargetGuid); Assert.Equal(0.42f, parsed.Value.HealthPercent, 4); } [Fact] public void ParseWeenieError_RoundTrip() { byte[] payload = new byte[4]; BinaryPrimitives.WriteUInt32LittleEndian(payload, 42u); Assert.Equal(42u, GameEvents.ParseWeenieError(payload)); } [Fact] public void ParseTransient_RoundTrip() { byte[] msg = MakeString16L("Your spell fizzled!"); byte[] chatType = new byte[4]; BinaryPrimitives.WriteUInt32LittleEndian(chatType, 5u); byte[] payload = new byte[msg.Length + 4]; Buffer.BlockCopy(msg, 0, payload, 0, msg.Length); Buffer.BlockCopy(chatType, 0, payload, msg.Length, 4); var parsed = GameEvents.ParseTransient(payload); Assert.NotNull(parsed); Assert.Equal("Your spell fizzled!", parsed!.Value.Message); Assert.Equal(5u, parsed.Value.ChatType); } [Fact] public void ParseMagicUpdateSpell_RoundTrip() { byte[] payload = new byte[4]; BinaryPrimitives.WriteUInt32LittleEndian(payload, 0x3E1u); // Flame Bolt I Assert.Equal(0x3E1u, GameEvents.ParseMagicUpdateSpell(payload)); } }