diff --git a/src/AcDream.Core.Net/Messages/PublicUpdatePropertyInt.cs b/src/AcDream.Core.Net/Messages/PublicUpdatePropertyInt.cs new file mode 100644 index 00000000..35d466a6 --- /dev/null +++ b/src/AcDream.Core.Net/Messages/PublicUpdatePropertyInt.cs @@ -0,0 +1,44 @@ +using System; +using System.Buffers.Binary; + +namespace AcDream.Core.Net.Messages; + +/// +/// Inbound PublicUpdatePropertyInt (0x02CE) — the server updates one +/// PropertyInt on a visible object (carries the object guid). Standalone +/// GameMessage, dispatched like / CreateObject. +/// +/// +/// The companion PrivateUpdatePropertyInt (0x02CD) targets the player's OWN +/// object (no guid) and is not parsed here — it has no item-icon impact. +/// +/// +/// Wire layout (ACE GameMessagePublicUpdatePropertyInt, size hint 17): +/// +/// u32 opcode = 0x02CE +/// u8 sequence // single byte (ByteSequence.NextBytes) — see PrivateUpdateVital +/// u32 guid +/// u32 property // PropertyInt enum; UiEffects = 18 +/// i32 value +/// +/// The sequence is parsed-past but not honored (latest-wins; divergence DR-4). +/// +public static class PublicUpdatePropertyInt +{ + public const uint Opcode = 0x02CEu; + + public readonly record struct Parsed(uint Guid, uint Property, int Value); + + /// Parse a raw 0x02CE 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; + uint prop = BinaryPrimitives.ReadUInt32LittleEndian(body[pos..]); pos += 4; + int value = BinaryPrimitives.ReadInt32LittleEndian(body[pos..]); + return new Parsed(guid, prop, value); + } +} diff --git a/tests/AcDream.Core.Net.Tests/Messages/PublicUpdatePropertyIntTests.cs b/tests/AcDream.Core.Net.Tests/Messages/PublicUpdatePropertyIntTests.cs new file mode 100644 index 00000000..bda5555a --- /dev/null +++ b/tests/AcDream.Core.Net.Tests/Messages/PublicUpdatePropertyIntTests.cs @@ -0,0 +1,36 @@ +using System.Buffers.Binary; +using AcDream.Core.Net.Messages; + +namespace AcDream.Core.Net.Tests.Messages; + +public sealed class PublicUpdatePropertyIntTests +{ + private static byte[] Build(uint guid, uint property, int value, byte seq = 1, uint opcode = 0x02CEu) + { + var b = new byte[17]; + BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(0), opcode); + b[4] = seq; + BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(5), guid); + BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(9), property); + BinaryPrimitives.WriteInt32LittleEndian(b.AsSpan(13), value); + return b; + } + + [Fact] + public void TryParse_uiEffectsUpdate_returnsGuidPropValue() + { + var p = PublicUpdatePropertyInt.TryParse(Build(0x50000001u, property: 18u, value: 0x9)); + Assert.NotNull(p); + Assert.Equal(0x50000001u, p!.Value.Guid); + Assert.Equal(18u, p.Value.Property); + Assert.Equal(0x9, p.Value.Value); + } + + [Fact] + public void TryParse_wrongOpcode_returnsNull() + => Assert.Null(PublicUpdatePropertyInt.TryParse(Build(1, 18, 1, opcode: 0x02CDu))); + + [Fact] + public void TryParse_truncated_returnsNull() + => Assert.Null(PublicUpdatePropertyInt.TryParse(new byte[16])); +}