From 79c0374d532430d563d2dbea47e6ae82395f4cd7 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 21 Jun 2026 18:52:21 +0200 Subject: [PATCH] =?UTF-8?q?feat(net):=20D.2b-B=20B-Wire=20=E2=80=94=20Inve?= =?UTF-8?q?ntoryRemoveObject=20(0x0024)=20parser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Top-level GameMessage (UIQueue), not a GameEvent. Server sends when an object leaves the client's inventory view (sold / dropped / destroyed). Layout: opcode(4) + guid(4) = 8 bytes — the smallest inventory wire. 3 tests: happy path, wrong opcode, truncated. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Messages/InventoryRemoveObject.cs | 31 ++++++++++++++++++ .../Messages/InventoryRemoveObjectTests.cs | 32 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 src/AcDream.Core.Net/Messages/InventoryRemoveObject.cs create mode 100644 tests/AcDream.Core.Net.Tests/Messages/InventoryRemoveObjectTests.cs diff --git a/src/AcDream.Core.Net/Messages/InventoryRemoveObject.cs b/src/AcDream.Core.Net/Messages/InventoryRemoveObject.cs new file mode 100644 index 00000000..b89d9cd4 --- /dev/null +++ b/src/AcDream.Core.Net/Messages/InventoryRemoveObject.cs @@ -0,0 +1,31 @@ +using System; +using System.Buffers.Binary; + +namespace AcDream.Core.Net.Messages; + +/// +/// Inbound InventoryRemoveObject (0x0024) — a top-level GameMessage (UIQueue), +/// NOT a GameEvent. The server tells the client an object left its inventory view +/// (given away / sold / destroyed). The client drops it from object maintenance. +/// +/// Wire layout (ACE GameMessageInventoryRemoveObject.cs, size hint 8): +/// +/// u32 opcode = 0x0024 +/// u32 guid +/// +/// +public static class InventoryRemoveObject +{ + public const uint Opcode = 0x0024u; + + public readonly record struct Parsed(uint Guid); + + /// Parse a raw 0x0024 body. Returns null on opcode mismatch / truncation. + public static Parsed? TryParse(ReadOnlySpan body) + { + if (body.Length < 8) return null; // 4 + 4 + if (BinaryPrimitives.ReadUInt32LittleEndian(body) != Opcode) return null; + uint guid = BinaryPrimitives.ReadUInt32LittleEndian(body[4..]); + return new Parsed(guid); + } +} diff --git a/tests/AcDream.Core.Net.Tests/Messages/InventoryRemoveObjectTests.cs b/tests/AcDream.Core.Net.Tests/Messages/InventoryRemoveObjectTests.cs new file mode 100644 index 00000000..b4588fe9 --- /dev/null +++ b/tests/AcDream.Core.Net.Tests/Messages/InventoryRemoveObjectTests.cs @@ -0,0 +1,32 @@ +using System.Buffers.Binary; +using AcDream.Core.Net.Messages; +using Xunit; + +namespace AcDream.Core.Net.Tests.Messages; + +public sealed class InventoryRemoveObjectTests +{ + private static byte[] Build(uint guid, uint opcode = 0x0024u) + { + var b = new byte[8]; + BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(0), opcode); + BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(4), guid); + return b; + } + + [Fact] + public void TryParse_valid_returnsGuid() + { + var p = InventoryRemoveObject.TryParse(Build(0x50000A07u)); + Assert.NotNull(p); + Assert.Equal(0x50000A07u, p!.Value.Guid); + } + + [Fact] + public void TryParse_wrongOpcode_returnsNull() + => Assert.Null(InventoryRemoveObject.TryParse(Build(1, opcode: 0x0023u))); + + [Fact] + public void TryParse_truncated_returnsNull() + => Assert.Null(InventoryRemoveObject.TryParse(new byte[7])); +}