From 91970c4fe93c193e7e8fb20dd22b7b0b24164dcd Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 18 Jun 2026 15:41:15 +0200 Subject: [PATCH] feat(D.5.4): capture full item field set in CreateObject parser WeenieClassId + Value/StackSize/MaxStackSize/Burden/capacities/Container/Wielder/ ValidLocations/CurrentWieldedLocation/Priority/Structure/Workmanship. Nullable = flag absent (don't clobber on merge). Cursor walk unchanged; +cursor-integrity test. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AcDream.Core.Net/Messages/CreateObject.cs | 78 ++++++++++++---- .../Messages/CreateObjectTests.cs | 92 ++++++++++++++++--- 2 files changed, 142 insertions(+), 28 deletions(-) diff --git a/src/AcDream.Core.Net/Messages/CreateObject.cs b/src/AcDream.Core.Net/Messages/CreateObject.cs index 35122e79..b79546ed 100644 --- a/src/AcDream.Core.Net/Messages/CreateObject.cs +++ b/src/AcDream.Core.Net/Messages/CreateObject.cs @@ -161,7 +161,29 @@ public static class CreateObject // effect recolor (Magical=0x1 … Nether=0x1000). The ONLY wire path for the effect // state (PropertyInt.UiEffects=18 has no [AssessmentProperty] → not in appraise). // Previously read + discarded at the UiEffects skip. 0 = no effect. - uint UiEffects = 0); + uint UiEffects = 0, + // D.5.4 (2026-06-18): full item field set from the WeenieHeader tail — + // previously walked-past. Wire bits per r06 §4 / PublicWeenieDesc. + // Quantity fields are int? to match ClientObject storage (ACE PropertyInt + // convention; the wire ushort/byte values widen losslessly); id/mask + // fields are uint?. null = the gated flag was absent (don't clobber on + // merge). WeenieClassId is the fixed-prefix class id (was discarded at + // cs:538); it is non-nullable — 0 means the prefix was absent/zero. + uint WeenieClassId = 0, + int? Value = null, + int? StackSize = null, + int? StackSizeMax = null, + int? Burden = null, + int? ItemsCapacity = null, + int? ContainersCapacity = null, + uint? ContainerId = null, + uint? WielderId = null, + uint? ValidLocations = null, + uint? CurrentWieldedLocation = null, + uint? Priority = null, + int? Structure = null, + int? MaxStructure = null, + float? Workmanship = null); /// /// The relevant subset of the server-sent MovementData / @@ -529,13 +551,28 @@ public static class CreateObject uint? itemType = null; uint weenieFlags = 0; uint iconId = 0; + uint weenieClassId = 0; + int? wValue = null; + int? wStackSize = null; + int? wMaxStackSize = null; + int? wBurden = null; + int? wItemsCapacity = null; + int? wContainersCapacity = null; + uint? wContainerId = null; + uint? wWielderId = null; + uint? wValidLocations = null; + uint? wCurrentWieldedLocation = null; + uint? wPriority = null; + int? wStructure = null; + int? wMaxStructure = null; + float? wWorkmanship = null; if (body.Length - pos >= 4) { weenieFlags = ReadU32(body, ref pos); try { name = ReadString16L(body, ref pos); - _ = ReadPackedDword(body, ref pos); // WeenieClassId + weenieClassId = ReadPackedDword(body, ref pos); // WeenieClassId (D.5.4: was discarded) iconId = ReadPackedDwordOfKnownType(body, ref pos, IconTypePrefix); if (body.Length - pos >= 4) itemType = ReadU32(body, ref pos); @@ -635,12 +672,12 @@ public static class CreateObject if ((weenieFlags & 0x00000002u) != 0) // ItemsCapacity u8 { if (body.Length - pos < 1) throw new FormatException("trunc ItemCap"); - pos += 1; + wItemsCapacity = body[pos]; pos += 1; } if ((weenieFlags & 0x00000004u) != 0) // ContainersCapacity u8 { if (body.Length - pos < 1) throw new FormatException("trunc ContCap"); - pos += 1; + wContainersCapacity = body[pos]; pos += 1; } if ((weenieFlags & 0x00000100u) != 0) // AmmoType u16 { @@ -650,7 +687,7 @@ public static class CreateObject if ((weenieFlags & 0x00000008u) != 0) // Value u32 { if (body.Length - pos < 4) throw new FormatException("trunc Value"); - pos += 4; + wValue = (int)ReadU32(body, ref pos); } if ((weenieFlags & 0x00000010u) != 0) // Usable u32 ← KEEP { @@ -685,47 +722,47 @@ public static class CreateObject if ((weenieFlags & 0x00000400u) != 0) // Structure u16 { if (body.Length - pos < 2) throw new FormatException("trunc Structure"); - pos += 2; + wStructure = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos)); pos += 2; } if ((weenieFlags & 0x00000800u) != 0) // MaxStructure u16 { if (body.Length - pos < 2) throw new FormatException("trunc MaxStructure"); - pos += 2; + wMaxStructure = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos)); pos += 2; } if ((weenieFlags & 0x00001000u) != 0) // StackSize u16 { if (body.Length - pos < 2) throw new FormatException("trunc StackSize"); - pos += 2; + wStackSize = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos)); pos += 2; } if ((weenieFlags & 0x00002000u) != 0) // MaxStackSize u16 { if (body.Length - pos < 2) throw new FormatException("trunc MaxStackSize"); - pos += 2; + wMaxStackSize = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos)); pos += 2; } if ((weenieFlags & 0x00004000u) != 0) // Container u32 { if (body.Length - pos < 4) throw new FormatException("trunc Container"); - pos += 4; + wContainerId = ReadU32(body, ref pos); } if ((weenieFlags & 0x00008000u) != 0) // Wielder u32 { if (body.Length - pos < 4) throw new FormatException("trunc Wielder"); - pos += 4; + wWielderId = ReadU32(body, ref pos); } if ((weenieFlags & 0x00010000u) != 0) // ValidLocations u32 { if (body.Length - pos < 4) throw new FormatException("trunc ValidLocations"); - pos += 4; + wValidLocations = ReadU32(body, ref pos); } if ((weenieFlags & 0x00020000u) != 0) // CurrentlyWieldedLocation u32 { if (body.Length - pos < 4) throw new FormatException("trunc CurrentlyWieldedLocation"); - pos += 4; + wCurrentWieldedLocation = ReadU32(body, ref pos); } if ((weenieFlags & 0x00040000u) != 0) // Priority u32 { if (body.Length - pos < 4) throw new FormatException("trunc Priority"); - pos += 4; + wPriority = ReadU32(body, ref pos); } if ((weenieFlags & 0x00100000u) != 0) // RadarBlipColor u8 { @@ -745,12 +782,12 @@ public static class CreateObject if ((weenieFlags & 0x01000000u) != 0) // Workmanship f32 { if (body.Length - pos < 4) throw new FormatException("trunc Workmanship"); - pos += 4; + wWorkmanship = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos)); pos += 4; } if ((weenieFlags & 0x00200000u) != 0) // Burden u16 { if (body.Length - pos < 2) throw new FormatException("trunc Burden"); - pos += 2; + wBurden = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos)); pos += 2; } if ((weenieFlags & 0x00400000u) != 0) // Spell u16 { @@ -815,7 +852,14 @@ public static class CreateObject IconId: iconId, Useability: useability, UseRadius: useRadius, IconOverlayId: iconOverlayId, IconUnderlayId: iconUnderlayId, - UiEffects: uiEffects); + UiEffects: uiEffects, + WeenieClassId: weenieClassId, + Value: wValue, StackSize: wStackSize, StackSizeMax: wMaxStackSize, + Burden: wBurden, ItemsCapacity: wItemsCapacity, ContainersCapacity: wContainersCapacity, + ContainerId: wContainerId, WielderId: wWielderId, + ValidLocations: wValidLocations, CurrentWieldedLocation: wCurrentWieldedLocation, + Priority: wPriority, Structure: wStructure, MaxStructure: wMaxStructure, + Workmanship: wWorkmanship); // Local helper: if we ran out of fields past PhysicsData, still // return the useful prefix (guid/position/setup/animParts/textures/palettes/scale/motion). diff --git a/tests/AcDream.Core.Net.Tests/Messages/CreateObjectTests.cs b/tests/AcDream.Core.Net.Tests/Messages/CreateObjectTests.cs index f760f98b..58a5a017 100644 --- a/tests/AcDream.Core.Net.Tests/Messages/CreateObjectTests.cs +++ b/tests/AcDream.Core.Net.Tests/Messages/CreateObjectTests.cs @@ -378,6 +378,66 @@ public sealed class CreateObjectTests Assert.Equal(0u, parsed!.Value.UiEffects); } + [Fact] + public void TryParse_WeenieClassId_Surfaced() + { + byte[] body = BuildMinimalCreateObjectWithWeenieHeader( + guid: 0x50000020u, name: "Sword", itemType: (uint)ItemType.MeleeWeapon, + weenieClassId: 0xABCDu); + var parsed = CreateObject.TryParse(body); + Assert.NotNull(parsed); + Assert.Equal(0xABCDu, parsed!.Value.WeenieClassId); + } + + [Fact] + public void TryParse_FullItemFields_Captured() + { + uint flags = + 0x00000008u | 0x00001000u | 0x00002000u | 0x00200000u | + 0x00000002u | 0x00000004u | 0x00004000u | 0x00008000u | + 0x00010000u | 0x00020000u | 0x00040000u | 0x00000400u | + 0x00000800u | 0x01000000u; + byte[] body = BuildMinimalCreateObjectWithWeenieHeader( + guid: 0x50000021u, name: "Pack", itemType: (uint)ItemType.Container, + weenieFlags: flags, + value: 250u, stackSize: 7, maxStackSize: 100u, burden: 42, + itemsCapacity: 24, containersCapacity: 7, + container: 0x50000099u, wielder: 0x5000009Au, + validLocations: 0x02000000u, currentWieldedLocation: 0x02000000u, + priority: 8u, structure: 5, maxStructure: 10, workmanship: 7.5f); + var parsed = CreateObject.TryParse(body); + Assert.NotNull(parsed); + var p = parsed!.Value; + Assert.Equal(250, p.Value); + Assert.Equal(7, p.StackSize); + Assert.Equal(100, p.StackSizeMax); + Assert.Equal(42, p.Burden); + Assert.Equal(24, p.ItemsCapacity); + Assert.Equal(7, p.ContainersCapacity); + Assert.Equal(0x50000099u, p.ContainerId); + Assert.Equal(0x5000009Au, p.WielderId); + Assert.Equal(0x02000000u, p.ValidLocations); + Assert.Equal(0x02000000u, p.CurrentWieldedLocation); + Assert.Equal(8u, p.Priority); + Assert.Equal(5, p.Structure); + Assert.Equal(10, p.MaxStructure); + Assert.Equal(7.5f, p.Workmanship); + } + + [Fact] + public void TryParse_MidTailFieldsSet_StillReachesIconOverlay() + { + uint flags = 0x00001000u | 0x00004000u | 0x40000000u; + byte[] body = BuildMinimalCreateObjectWithWeenieHeader( + guid: 0x50000022u, name: "Ring", itemType: (uint)ItemType.Jewelry, + weenieFlags: flags, stackSize: 1, container: 0x500000F0u, + iconOverlayId: 0x4321u); + var parsed = CreateObject.TryParse(body); + Assert.NotNull(parsed); + Assert.Equal(0x06004321u, parsed!.Value.IconOverlayId); + Assert.Equal(0x500000F0u, parsed.Value.ContainerId); + } + private static byte[] BuildMinimalCreateObjectWithWeenieHeader( uint guid, string name, @@ -397,7 +457,17 @@ public sealed class CreateObjectTests ushort? structure = null, ushort? maxStructure = null, ushort? stackSize = null, - ushort? burden = null) + ushort? burden = null, + uint weenieClassId = 0x1234, + uint? maxStackSize = null, + byte? itemsCapacity = null, + byte? containersCapacity = null, + uint? container = null, + uint? wielder = null, + uint? validLocations = null, + uint? currentWieldedLocation = null, + uint? priority = null, + float? workmanship = null) { var bytes = new List(); WriteU32(bytes, CreateObject.Opcode); @@ -419,7 +489,7 @@ public sealed class CreateObjectTests // Fixed WeenieHeader prefix per ACE SerializeCreateObject. WriteU32(bytes, weenieFlags); // weenieFlags WriteString16L(bytes, name); - WritePackedDword(bytes, 0x1234); // WeenieClassId + WritePackedDword(bytes, weenieClassId); // WeenieClassId WritePackedDword(bytes, iconId); // IconId via known-type writer (prefix stripped by ACE writer) WriteU32(bytes, itemType); WriteU32(bytes, objectDescriptionFlags); @@ -435,8 +505,8 @@ public sealed class CreateObjectTests // its weenieFlags bit is set, matching the parser's walker exactly. // Fields not parameterized above default to 0. if ((weenieFlags & 0x00000001u) != 0) { /* PluralName — not parameterized */ } - if ((weenieFlags & 0x00000002u) != 0) bytes.Add(0); // ItemsCapacity u8 - if ((weenieFlags & 0x00000004u) != 0) bytes.Add(0); // ContainersCapacity u8 + if ((weenieFlags & 0x00000002u) != 0) bytes.Add(itemsCapacity ?? 0); // ItemsCapacity u8 + if ((weenieFlags & 0x00000004u) != 0) bytes.Add(containersCapacity ?? 0); // ContainersCapacity u8 if ((weenieFlags & 0x00000100u) != 0) WriteU16(bytes, 0); // AmmoType u16 if ((weenieFlags & 0x00000008u) != 0) WriteU32(bytes, value ?? 0u); // Value u32 if ((weenieFlags & 0x00000010u) != 0) WriteU32(bytes, useability ?? 0u); // Usable u32 @@ -452,19 +522,19 @@ public sealed class CreateObjectTests if ((weenieFlags & 0x00000400u) != 0) WriteU16(bytes, structure ?? 0); // Structure u16 if ((weenieFlags & 0x00000800u) != 0) WriteU16(bytes, maxStructure ?? 0); // MaxStructure u16 if ((weenieFlags & 0x00001000u) != 0) WriteU16(bytes, stackSize ?? 0); // StackSize u16 - if ((weenieFlags & 0x00002000u) != 0) WriteU16(bytes, 0); // MaxStackSize u16 - if ((weenieFlags & 0x00004000u) != 0) WriteU32(bytes, 0); // Container u32 - if ((weenieFlags & 0x00008000u) != 0) WriteU32(bytes, 0); // Wielder u32 - if ((weenieFlags & 0x00010000u) != 0) WriteU32(bytes, 0); // ValidLocations u32 - if ((weenieFlags & 0x00020000u) != 0) WriteU32(bytes, 0); // CurrentlyWieldedLocation u32 - if ((weenieFlags & 0x00040000u) != 0) WriteU32(bytes, 0); // Priority u32 + if ((weenieFlags & 0x00002000u) != 0) WriteU16(bytes, (ushort)(maxStackSize ?? 0)); // MaxStackSize u16 + if ((weenieFlags & 0x00004000u) != 0) WriteU32(bytes, container ?? 0); // Container u32 + if ((weenieFlags & 0x00008000u) != 0) WriteU32(bytes, wielder ?? 0); // Wielder u32 + if ((weenieFlags & 0x00010000u) != 0) WriteU32(bytes, validLocations ?? 0); // ValidLocations u32 + if ((weenieFlags & 0x00020000u) != 0) WriteU32(bytes, currentWieldedLocation ?? 0); // CurrentlyWieldedLocation u32 + if ((weenieFlags & 0x00040000u) != 0) WriteU32(bytes, priority ?? 0); // Priority u32 if ((weenieFlags & 0x00100000u) != 0) bytes.Add(0); // RadarBlipColor u8 if ((weenieFlags & 0x00800000u) != 0) bytes.Add(0); // RadarBehavior u8 if ((weenieFlags & 0x08000000u) != 0) WriteU16(bytes, 0); // PScript u16 if ((weenieFlags & 0x01000000u) != 0) // Workmanship f32 { Span tmp = stackalloc byte[4]; - BinaryPrimitives.WriteSingleLittleEndian(tmp, 0f); + BinaryPrimitives.WriteSingleLittleEndian(tmp, workmanship ?? 0f); bytes.AddRange(tmp.ToArray()); } if ((weenieFlags & 0x00200000u) != 0) WriteU16(bytes, burden ?? 0); // Burden u16