feat(net): D.2b-B B-Wire — SetStackSize (0x0197) parser

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) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-21 18:51:42 +02:00
parent 7b25d78506
commit 87e354f583
2 changed files with 75 additions and 0 deletions

View file

@ -0,0 +1,38 @@
using System;
using System.Buffers.Binary;
namespace AcDream.Core.Net.Messages;
/// <summary>
/// Inbound <c>SetStackSize (0x0197)</c> — 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.
///
/// <para>Wire layout (ACE <c>GameMessageSetStackSize.cs</c>, size hint 17):</para>
/// <code>
/// u32 opcode = 0x0197
/// u8 sequence // ByteSequence (UpdatePropertyInt) — not honored
/// u32 guid
/// i32 stackSize
/// i32 value
/// </code>
/// </summary>
public static class SetStackSize
{
public const uint Opcode = 0x0197u;
public readonly record struct Parsed(uint Guid, int StackSize, int Value);
/// <summary>Parse a raw 0x0197 body. Returns null on opcode mismatch / truncation.</summary>
public static Parsed? TryParse(ReadOnlySpan<byte> 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);
}
}

View file

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