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]));
+}