From 7b25d78506a368a2972ce0fd241aba9adac35f4e Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 21 Jun 2026 18:50:56 +0200 Subject: [PATCH] =?UTF-8?q?feat(net):=20D.2b-B=20B-Wire=20=E2=80=94=20Priv?= =?UTF-8?q?ateUpdatePropertyInt=20(0x02CD)=20parser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror of PublicUpdatePropertyInt (0x02CE) but with no guid field — targets the local player's own object. Burden (EncumbranceVal, PropertyInt 5) changes ride this opcode on every pick-up / drop. Layout: opcode(4) + seq(1) + property(4) + value(4) = 13 bytes. 3 tests: happy path, wrong opcode, truncated. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Messages/PrivateUpdatePropertyInt.cs | 38 +++++++++++++++++++ .../Messages/PrivateUpdatePropertyIntTests.cs | 36 ++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 src/AcDream.Core.Net/Messages/PrivateUpdatePropertyInt.cs create mode 100644 tests/AcDream.Core.Net.Tests/Messages/PrivateUpdatePropertyIntTests.cs diff --git a/src/AcDream.Core.Net/Messages/PrivateUpdatePropertyInt.cs b/src/AcDream.Core.Net/Messages/PrivateUpdatePropertyInt.cs new file mode 100644 index 00000000..5d60af03 --- /dev/null +++ b/src/AcDream.Core.Net/Messages/PrivateUpdatePropertyInt.cs @@ -0,0 +1,38 @@ +using System; +using System.Buffers.Binary; + +namespace AcDream.Core.Net.Messages; + +/// +/// Inbound PrivateUpdatePropertyInt (0x02CD) — the server updates one +/// PropertyInt on the player's OWN object. Unlike the sibling +/// (0x02CE), this carries NO guid (it +/// implicitly targets the local player). Burden (EncumbranceVal, PropertyInt 5) +/// changes ride this opcode on every pick-up / drop. +/// +/// Wire layout (ACE GameMessagePrivateUpdatePropertyInt): +/// +/// u32 opcode = 0x02CD +/// u8 sequence // single byte (ByteSequence) — not honored, latest-wins (DR-4) +/// u32 property // PropertyInt enum +/// i32 value +/// +/// +public static class PrivateUpdatePropertyInt +{ + public const uint Opcode = 0x02CDu; + + public readonly record struct Parsed(uint Property, int Value); + + /// Parse a raw 0x02CD body. Returns null on opcode mismatch / truncation. + public static Parsed? TryParse(ReadOnlySpan body) + { + if (body.Length < 13) return null; // 4 + 1 + 4 + 4 + if (BinaryPrimitives.ReadUInt32LittleEndian(body) != Opcode) return null; + int pos = 4; + pos += 1; // sequence byte (not honored) + uint prop = BinaryPrimitives.ReadUInt32LittleEndian(body[pos..]); pos += 4; + int value = BinaryPrimitives.ReadInt32LittleEndian(body[pos..]); + return new Parsed(prop, value); + } +} diff --git a/tests/AcDream.Core.Net.Tests/Messages/PrivateUpdatePropertyIntTests.cs b/tests/AcDream.Core.Net.Tests/Messages/PrivateUpdatePropertyIntTests.cs new file mode 100644 index 00000000..f696c010 --- /dev/null +++ b/tests/AcDream.Core.Net.Tests/Messages/PrivateUpdatePropertyIntTests.cs @@ -0,0 +1,36 @@ +using System.Buffers.Binary; +using AcDream.Core.Net.Messages; +using Xunit; + +namespace AcDream.Core.Net.Tests.Messages; + +public sealed class PrivateUpdatePropertyIntTests +{ + // 0x02CD body: opcode(4) + seq(1) + property(4) + value(4) = 13 bytes. NO guid. + private static byte[] Build(uint property, int value, byte seq = 1, uint opcode = 0x02CDu) + { + var b = new byte[13]; + BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(0), opcode); + b[4] = seq; + BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(5), property); + BinaryPrimitives.WriteInt32LittleEndian(b.AsSpan(9), value); + return b; + } + + [Fact] + public void TryParse_encumbranceVal_returnsPropValue() + { + var p = PrivateUpdatePropertyInt.TryParse(Build(property: 5u, value: 1500)); + Assert.NotNull(p); + Assert.Equal(5u, p!.Value.Property); + Assert.Equal(1500, p.Value.Value); + } + + [Fact] + public void TryParse_wrongOpcode_returnsNull() + => Assert.Null(PrivateUpdatePropertyInt.TryParse(Build(5, 1, opcode: 0x02CEu))); + + [Fact] + public void TryParse_truncated_returnsNull() + => Assert.Null(PrivateUpdatePropertyInt.TryParse(new byte[12])); +}