From 87e354f583c2b3677d29d9339fb270c19f6552f2 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 21 Jun 2026 18:51:42 +0200 Subject: [PATCH] =?UTF-8?q?feat(net):=20D.2b-B=20B-Wire=20=E2=80=94=20SetS?= =?UTF-8?q?tackSize=20(0x0197)=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 after a stack merge / split to update count + value. Layout: opcode(4) + seq(1) + guid(4) + stackSize(4) + value(4) = 17 bytes. 3 tests: happy path, wrong opcode, truncated. Client consumer: ACCWeenieObject::ServerSaysSetStackSize. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AcDream.Core.Net/Messages/SetStackSize.cs | 38 +++++++++++++++++++ .../Messages/SetStackSizeTests.cs | 37 ++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 src/AcDream.Core.Net/Messages/SetStackSize.cs create mode 100644 tests/AcDream.Core.Net.Tests/Messages/SetStackSizeTests.cs diff --git a/src/AcDream.Core.Net/Messages/SetStackSize.cs b/src/AcDream.Core.Net/Messages/SetStackSize.cs new file mode 100644 index 00000000..5d7a8313 --- /dev/null +++ b/src/AcDream.Core.Net/Messages/SetStackSize.cs @@ -0,0 +1,38 @@ +using System; +using System.Buffers.Binary; + +namespace AcDream.Core.Net.Messages; + +/// +/// Inbound SetStackSize (0x0197) — a top-level GameMessage (UIQueue group), +/// NOT a GameEvent. The server updates a stack's count + value after a merge / split. +/// Client consumer: ACCWeenieObject::ServerSaysSetStackSize. +/// +/// Wire layout (ACE GameMessageSetStackSize.cs, size hint 17): +/// +/// u32 opcode = 0x0197 +/// u8 sequence // ByteSequence (UpdatePropertyInt) — not honored +/// u32 guid +/// i32 stackSize +/// i32 value +/// +/// +public static class SetStackSize +{ + public const uint Opcode = 0x0197u; + + public readonly record struct Parsed(uint Guid, int StackSize, int Value); + + /// Parse a raw 0x0197 body. Returns null on opcode mismatch / truncation. + public static Parsed? TryParse(ReadOnlySpan body) + { + if (body.Length < 17) return null; // 4 + 1 + 4 + 4 + 4 + if (BinaryPrimitives.ReadUInt32LittleEndian(body) != Opcode) return null; + int pos = 4; + pos += 1; // sequence byte (not honored) + uint guid = BinaryPrimitives.ReadUInt32LittleEndian(body[pos..]); pos += 4; + int stack = BinaryPrimitives.ReadInt32LittleEndian(body[pos..]); pos += 4; + int value = BinaryPrimitives.ReadInt32LittleEndian(body[pos..]); + return new Parsed(guid, stack, value); + } +} diff --git a/tests/AcDream.Core.Net.Tests/Messages/SetStackSizeTests.cs b/tests/AcDream.Core.Net.Tests/Messages/SetStackSizeTests.cs new file mode 100644 index 00000000..7f09c4f1 --- /dev/null +++ b/tests/AcDream.Core.Net.Tests/Messages/SetStackSizeTests.cs @@ -0,0 +1,37 @@ +using System.Buffers.Binary; +using AcDream.Core.Net.Messages; +using Xunit; + +namespace AcDream.Core.Net.Tests.Messages; + +public sealed class SetStackSizeTests +{ + private static byte[] Build(uint guid, int stackSize, int value, byte seq = 1, uint opcode = 0x0197u) + { + var b = new byte[17]; + BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(0), opcode); + b[4] = seq; + BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(5), guid); + BinaryPrimitives.WriteInt32LittleEndian(b.AsSpan(9), stackSize); + BinaryPrimitives.WriteInt32LittleEndian(b.AsSpan(13), value); + return b; + } + + [Fact] + public void TryParse_validStack_returnsGuidStackValue() + { + var p = SetStackSize.TryParse(Build(0x50000A01u, stackSize: 25, value: 125)); + Assert.NotNull(p); + Assert.Equal(0x50000A01u, p!.Value.Guid); + Assert.Equal(25, p.Value.StackSize); + Assert.Equal(125, p.Value.Value); + } + + [Fact] + public void TryParse_wrongOpcode_returnsNull() + => Assert.Null(SetStackSize.TryParse(Build(1, 1, 1, opcode: 0x0196u))); + + [Fact] + public void TryParse_truncated_returnsNull() + => Assert.Null(SetStackSize.TryParse(new byte[16])); +}