feat(net): D.2b-B B-Wire — PrivateUpdatePropertyInt (0x02CD) parser

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) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-21 18:50:56 +02:00
parent 3f190811be
commit 7b25d78506
2 changed files with 74 additions and 0 deletions

View file

@ -0,0 +1,38 @@
using System;
using System.Buffers.Binary;
namespace AcDream.Core.Net.Messages;
/// <summary>
/// Inbound <c>PrivateUpdatePropertyInt (0x02CD)</c> — the server updates one
/// <c>PropertyInt</c> on the player's OWN object. Unlike the sibling
/// <see cref="PublicUpdatePropertyInt"/> (0x02CE), this carries NO guid (it
/// implicitly targets the local player). Burden (<c>EncumbranceVal</c>, PropertyInt 5)
/// changes ride this opcode on every pick-up / drop.
///
/// <para>Wire layout (ACE <c>GameMessagePrivateUpdatePropertyInt</c>):</para>
/// <code>
/// u32 opcode = 0x02CD
/// u8 sequence // single byte (ByteSequence) — not honored, latest-wins (DR-4)
/// u32 property // PropertyInt enum
/// i32 value
/// </code>
/// </summary>
public static class PrivateUpdatePropertyInt
{
public const uint Opcode = 0x02CDu;
public readonly record struct Parsed(uint Property, int Value);
/// <summary>Parse a raw 0x02CD body. Returns null on opcode mismatch / truncation.</summary>
public static Parsed? TryParse(ReadOnlySpan<byte> 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);
}
}

View file

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