diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 57419a17..6ca8697f 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -2632,7 +2632,11 @@ public sealed class GameWindow : IDisposable { // D.5.1: enrich a known inventory/equipped item (stubbed from PlayerDescription) // with the icon/name/type its CreateObject carries, so the toolbar can render it. - Items.EnrichItem(spawn.Guid, spawn.IconId, spawn.Name ?? string.Empty, (AcDream.Core.Items.ItemType)(spawn.ItemType ?? 0)); + // D.5.1 (2026-06-17): also pass overlay/underlay ids from the extended + // WeenieHeader tail so IconComposer composites all icon layers. + Items.EnrichItem(spawn.Guid, spawn.IconId, spawn.Name ?? string.Empty, + (AcDream.Core.Items.ItemType)(spawn.ItemType ?? 0), + spawn.IconOverlayId, spawn.IconUnderlayId); // Phase A.1 hotfix: live CreateObject handler reads dats extensively // (Setup, GfxObj, Surface, SurfaceTexture) to hydrate the spawned diff --git a/src/AcDream.Core.Net/Messages/CreateObject.cs b/src/AcDream.Core.Net/Messages/CreateObject.cs index db3c8a7b..f0d2b65d 100644 --- a/src/AcDream.Core.Net/Messages/CreateObject.cs +++ b/src/AcDream.Core.Net/Messages/CreateObject.cs @@ -145,7 +145,18 @@ public static class CreateObject // a sizing hint for selection indicators on entities that // publish it. uint? Useability = null, - float? UseRadius = null); + float? UseRadius = null, + // D.5.1 (2026-06-17): icon overlay/underlay dat ids from the + // WeenieHeader optional tail. IconOverlayId is gated by + // WeenieHeaderFlag.IconOverlay (0x40000000) in weenieFlags; + // IconUnderlayId is gated by WeenieHeaderFlag2.IconUnderlay (0x01) + // in weenieFlags2 (present when objDescFlags bit 0x04000000 is set). + // Sourced from ACE WorldObject_Networking.cs:202-206. Zero when + // the server did not send the field (most entities have neither). + // IconComposer.GetIcon already composites these layers in the correct + // retail order (underlay / base / overlay+tint / effect). + uint IconOverlayId = 0, + uint IconUnderlayId = 0); /// /// The relevant subset of the server-sent MovementData / @@ -539,31 +550,62 @@ public static class CreateObject catch { /* truncated name — partial result is still useful */ } } - // --- WeenieHeader optional tail (2026-05-15): walk the - // conditional fields up through Useability + UseRadius. + // --- WeenieHeader optional tail: walk every conditional field + // in EXACT ACE write order (WorldObject_Networking.cs:87-219) + // so the cursor reaches IconOverlay + IconUnderlay. // - // Wire order is fixed by ACE WorldObject_Networking.cs:87-114 - // and matches retail PWD::Pack order. We MUST skip every - // preceding optional field (even those we don't care about) - // because each one moves the parse cursor. + // We MUST skip every field that precedes IconOverlay even when + // we don't need its value — each one occupies bytes on the wire + // and a cursor error here would desync ALL downstream optional + // reads for the rest of this entity's packet. // - // Field bit width decoded? - // ------- ------ -------- -------- - // weenieFlags2 conditional on objDescFlags & 0x80000000 (BF_INCLUDES_SECOND_HEADER) - // u32 skipped - // PluralName 0x1 String16L (variable, padded to 4) skipped - // ItemCapacity 0x2 1 byte skipped - // ContainerCap 0x4 1 byte skipped - // AmmoType 0x100 u16 skipped - // Value 0x8 u32 skipped - // Useability 0x10 u32 KEPT - // UseRadius 0x20 f32 KEPT + // Wire order (verified against ACE WorldObject_Networking.cs): + // bit field width + // --------- ------------------ ----- + // 0x04000000 (objDescFlags) weenieFlags2 u32 (skip) + // 0x00000001 PluralName String16L (skip) + // 0x00000002 ItemsCapacity u8 (skip) + // 0x00000004 ContainersCapacity u8 (skip) + // 0x00000100 AmmoType u16 (skip) + // 0x00000008 Value u32 (skip) + // 0x00000010 Usable u32 KEPT + // 0x00000020 UseRadius f32 KEPT + // 0x00080000 TargetType u32 (skip) + // 0x00000080 UiEffects u32 (skip) + // 0x00000200 CombatUse sbyte/1 byte (skip) + // 0x00000400 Structure u16 (skip) + // 0x00000800 MaxStructure u16 (skip) + // 0x00001000 StackSize u16 (skip) + // 0x00002000 MaxStackSize u16 (skip) + // 0x00004000 Container u32 (skip) + // 0x00008000 Wielder u32 (skip) + // 0x00010000 ValidLocations u32 (skip) + // 0x00020000 CurrentlyWieldedLocation u32 (skip) + // 0x00040000 Priority u32 (skip) + // 0x00100000 RadarBlipColor u8 (skip) + // 0x00800000 RadarBehavior u8 (skip) + // 0x08000000 PScript u16 (skip) + // 0x01000000 Workmanship f32 (skip) + // 0x00200000 Burden u16 (skip) + // 0x00400000 Spell u16 (skip) + // 0x02000000 HouseOwner u32 (skip) + // 0x04000000 HouseRestrictions RestrictionDB (skip, variable-length) + // 0x20000000 HookItemTypes u32 (skip) + // 0x00000040 Monarch u32 (skip) + // 0x10000000 HookType u16 (skip) + // 0x40000000 IconOverlay PackedDwordKnownType(0x06000000) CAPTURE + // weenieFlags2 bit 0x01: + // IconUnderlay PackedDwordKnownType(0x06000000) CAPTURE // - // Wrapped in try/catch — if a malformed entity truncates the - // tail we still return the prefix fields. Most spawned entities - // either have all of these or none of them. + // The entire walk is inside try/catch. A truncated packet degrades + // gracefully: whatever was parsed before the throw is kept, and + // IconOverlayId/IconUnderlayId stay 0 (no overlay drawn). This is + // SAFE because IconComposer early-returns on id==0 per layer. uint? useability = null; float? useRadius = null; + uint iconOverlayId = 0; + uint iconUnderlayId = 0; + uint weenieFlags2 = 0; try { // BF_INCLUDES_SECOND_HEADER = 0x04000000 per acclient.h:6458 @@ -571,20 +613,25 @@ public static class CreateObject // Earlier code had this as 0x80000000 — wrong bit, so the // weenieFlags2 4-byte skip never fired for entities that // actually had it set, corrupting downstream optional-tail - // offsets. Now correct. + // offsets. Now correct. We CAPTURE weenieFlags2 now (instead + // of skipping) so we can gate IconUnderlay from bit 0x01. bool hasSecondHeader = objectDescriptionFlags.HasValue && (objectDescriptionFlags.Value & 0x04000000u) != 0; - if (hasSecondHeader && body.Length - pos >= 4) pos += 4; // weenieFlags2 + if (hasSecondHeader) + { + if (body.Length - pos < 4) throw new FormatException("trunc weenieFlags2"); + weenieFlags2 = ReadU32(body, ref pos); + } if ((weenieFlags & 0x00000001u) != 0) // PluralName _ = ReadString16L(body, ref pos); - if ((weenieFlags & 0x00000002u) != 0) // ItemCapacity + if ((weenieFlags & 0x00000002u) != 0) // ItemsCapacity u8 { if (body.Length - pos < 1) throw new FormatException("trunc ItemCap"); pos += 1; } - if ((weenieFlags & 0x00000004u) != 0) // ContainerCapacity + if ((weenieFlags & 0x00000004u) != 0) // ContainersCapacity u8 { if (body.Length - pos < 1) throw new FormatException("trunc ContCap"); pos += 1; @@ -599,7 +646,7 @@ public static class CreateObject if (body.Length - pos < 4) throw new FormatException("trunc Value"); pos += 4; } - if ((weenieFlags & 0x00000010u) != 0) // Useability u32 ← KEEP + if ((weenieFlags & 0x00000010u) != 0) // Usable u32 ← KEEP { if (body.Length - pos < 4) throw new FormatException("trunc Useability"); useability = ReadU32(body, ref pos); @@ -610,6 +657,147 @@ public static class CreateObject useRadius = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos)); pos += 4; } + + // ---- Extended walk: fields after UseRadius through IconOverlay ---- + // Source: ACE WorldObject_Networking.cs:108-206 (verified 2026-06-17). + + if ((weenieFlags & 0x00080000u) != 0) // TargetType u32 + { + if (body.Length - pos < 4) throw new FormatException("trunc TargetType"); + pos += 4; + } + if ((weenieFlags & 0x00000080u) != 0) // UiEffects u32 + { + if (body.Length - pos < 4) throw new FormatException("trunc UiEffects"); + pos += 4; + } + if ((weenieFlags & 0x00000200u) != 0) // CombatUse sbyte (1 byte) + { + if (body.Length - pos < 1) throw new FormatException("trunc CombatUse"); + pos += 1; + } + if ((weenieFlags & 0x00000400u) != 0) // Structure u16 + { + if (body.Length - pos < 2) throw new FormatException("trunc Structure"); + pos += 2; + } + if ((weenieFlags & 0x00000800u) != 0) // MaxStructure u16 + { + if (body.Length - pos < 2) throw new FormatException("trunc MaxStructure"); + pos += 2; + } + if ((weenieFlags & 0x00001000u) != 0) // StackSize u16 + { + if (body.Length - pos < 2) throw new FormatException("trunc StackSize"); + pos += 2; + } + if ((weenieFlags & 0x00002000u) != 0) // MaxStackSize u16 + { + if (body.Length - pos < 2) throw new FormatException("trunc MaxStackSize"); + pos += 2; + } + if ((weenieFlags & 0x00004000u) != 0) // Container u32 + { + if (body.Length - pos < 4) throw new FormatException("trunc Container"); + pos += 4; + } + if ((weenieFlags & 0x00008000u) != 0) // Wielder u32 + { + if (body.Length - pos < 4) throw new FormatException("trunc Wielder"); + pos += 4; + } + if ((weenieFlags & 0x00010000u) != 0) // ValidLocations u32 + { + if (body.Length - pos < 4) throw new FormatException("trunc ValidLocations"); + pos += 4; + } + if ((weenieFlags & 0x00020000u) != 0) // CurrentlyWieldedLocation u32 + { + if (body.Length - pos < 4) throw new FormatException("trunc CurrentlyWieldedLocation"); + pos += 4; + } + if ((weenieFlags & 0x00040000u) != 0) // Priority u32 + { + if (body.Length - pos < 4) throw new FormatException("trunc Priority"); + pos += 4; + } + if ((weenieFlags & 0x00100000u) != 0) // RadarBlipColor u8 + { + if (body.Length - pos < 1) throw new FormatException("trunc RadarBlipColor"); + pos += 1; + } + if ((weenieFlags & 0x00800000u) != 0) // RadarBehavior u8 + { + if (body.Length - pos < 1) throw new FormatException("trunc RadarBehavior"); + pos += 1; + } + if ((weenieFlags & 0x08000000u) != 0) // PScript u16 + { + if (body.Length - pos < 2) throw new FormatException("trunc PScript"); + pos += 2; + } + if ((weenieFlags & 0x01000000u) != 0) // Workmanship f32 + { + if (body.Length - pos < 4) throw new FormatException("trunc Workmanship"); + pos += 4; + } + if ((weenieFlags & 0x00200000u) != 0) // Burden u16 + { + if (body.Length - pos < 2) throw new FormatException("trunc Burden"); + pos += 2; + } + if ((weenieFlags & 0x00400000u) != 0) // Spell u16 + { + if (body.Length - pos < 2) throw new FormatException("trunc Spell"); + pos += 2; + } + if ((weenieFlags & 0x02000000u) != 0) // HouseOwner u32 + { + if (body.Length - pos < 4) throw new FormatException("trunc HouseOwner"); + pos += 4; + } + if ((weenieFlags & 0x04000000u) != 0) // HouseRestrictions (RestrictionDB) + { + // Wire layout per ACE RestrictionDB + RestrictionDBExtensions.Write: + // u32 Version, u32 OpenStatus, u32 MonarchId, + // u16 count, u16 numBuckets, then count × (u32 guid + u32 value). + // Fixed header = 12 bytes; PackableHashTable header = 4 bytes. + // Total = 16 + count * 8. + if (body.Length - pos < 16) throw new FormatException("trunc RestrictionDB header"); + // Version(4) + OpenStatus(4) + MonarchId(4) = 12 bytes + pos += 12; + ushort tableCount = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos)); + pos += 2; // count u16 + pos += 2; // numBuckets u16 + int entryBytes = tableCount * 8; // each entry: u32 guid + u32 value + if (body.Length - pos < entryBytes) throw new FormatException("trunc RestrictionDB entries"); + pos += entryBytes; + } + if ((weenieFlags & 0x20000000u) != 0) // HookItemTypes u32 + { + if (body.Length - pos < 4) throw new FormatException("trunc HookItemTypes"); + pos += 4; + } + if ((weenieFlags & 0x00000040u) != 0) // Monarch u32 + { + if (body.Length - pos < 4) throw new FormatException("trunc Monarch"); + pos += 4; + } + if ((weenieFlags & 0x10000000u) != 0) // HookType u16 + { + if (body.Length - pos < 2) throw new FormatException("trunc HookType"); + pos += 2; + } + if ((weenieFlags & 0x40000000u) != 0) // IconOverlay PackedDwordOfKnownType(0x06000000) ← CAPTURE + { + iconOverlayId = ReadPackedDwordOfKnownType(body, ref pos, IconTypePrefix); + } + // IconUnderlay is gated by weenieFlags2 bit 0x01, not weenieFlags. + // weenieFlags2 is only present when hasSecondHeader (captured above). + if ((weenieFlags2 & 0x00000001u) != 0) // IconUnderlay PackedDwordOfKnownType(0x06000000) ← CAPTURE + { + iconUnderlayId = ReadPackedDwordOfKnownType(body, ref pos, IconTypePrefix); + } } catch { /* truncated weenie tail — keep whatever we got. */ } @@ -619,7 +807,8 @@ public static class CreateObject physicsState, objectDescriptionFlags, friction, elasticity, IconId: iconId, - Useability: useability, UseRadius: useRadius); + Useability: useability, UseRadius: useRadius, + IconOverlayId: iconOverlayId, IconUnderlayId: iconUnderlayId); // 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/src/AcDream.Core.Net/WorldSession.cs b/src/AcDream.Core.Net/WorldSession.cs index 2f07ede3..d263f0e7 100644 --- a/src/AcDream.Core.Net/WorldSession.cs +++ b/src/AcDream.Core.Net/WorldSession.cs @@ -82,7 +82,13 @@ public sealed class WorldSession : IDisposable uint? Useability = null, float? UseRadius = null, // D.5.1: icon datId from CreateObject WeenieHeader, for toolbar rendering. - uint IconId = 0); + uint IconId = 0, + // D.5.1 (2026-06-17): icon overlay/underlay dat ids from the extended + // WeenieHeader optional tail. Gated by WeenieHeaderFlag.IconOverlay + // (0x40000000) and WeenieHeaderFlag2.IconUnderlay (0x01) respectively. + // Zero when the server did not send the field (common for most entities). + uint IconOverlayId = 0, + uint IconUnderlayId = 0); /// Fires when the session finishes parsing a CreateObject. public event Action? EntitySpawned; @@ -719,7 +725,9 @@ public sealed class WorldSession : IDisposable parsed.Value.Elasticity, parsed.Value.Useability, parsed.Value.UseRadius, - parsed.Value.IconId)); + parsed.Value.IconId, + parsed.Value.IconOverlayId, + parsed.Value.IconUnderlayId)); } } else if (op == DeleteObject.Opcode) diff --git a/src/AcDream.Core/Items/ItemRepository.cs b/src/AcDream.Core/Items/ItemRepository.cs index a993f336..b92dd1ce 100644 --- a/src/AcDream.Core/Items/ItemRepository.cs +++ b/src/AcDream.Core/Items/ItemRepository.cs @@ -144,13 +144,22 @@ public sealed class ItemRepository /// Raises ItemPropertiesUpdated whenever the item is found (matching the /// UpdateProperties convention — it fires on found regardless of whether a field /// actually changed) so bound widgets (the toolbar) re-render. + /// + /// D.5.1 (2026-06-17): also accepts and + /// from the extended WeenieHeader tail. Both + /// default to 0 (not sent by server). IconComposer.GetIcon already composites + /// underlay/base/overlay in the correct retail layer order and early-returns on 0. + /// /// - public bool EnrichItem(uint objectId, uint iconId, string name, ItemType type) + public bool EnrichItem(uint objectId, uint iconId, string name, ItemType type, + uint iconOverlayId = 0, uint iconUnderlayId = 0) { if (!_items.TryGetValue(objectId, out var item)) return false; if (iconId != 0) item.IconId = iconId; if (!string.IsNullOrEmpty(name)) item.Name = name; if (type != default) item.Type = type; + if (iconOverlayId != 0) item.IconOverlayId = iconOverlayId; + if (iconUnderlayId != 0) item.IconUnderlayId = iconUnderlayId; ItemPropertiesUpdated?.Invoke(item); return true; } diff --git a/tests/AcDream.Core.Net.Tests/Messages/CreateObjectTests.cs b/tests/AcDream.Core.Net.Tests/Messages/CreateObjectTests.cs index 8a98d62f..b58c6fe3 100644 --- a/tests/AcDream.Core.Net.Tests/Messages/CreateObjectTests.cs +++ b/tests/AcDream.Core.Net.Tests/Messages/CreateObjectTests.cs @@ -179,6 +179,122 @@ public sealed class CreateObjectTests Assert.Equal(0x06001234u, parsed!.Value.IconId); } + // ----------------------------------------------------------------------- + // D.5.1 (2026-06-17): extended WeenieHeader optional-tail walk — the parser + // now continues past UseRadius through ALL intervening fields to reach + // IconOverlay (weenieFlags bit 0x40000000) and IconUnderlay (weenieFlags2 + // bit 0x01, present when objDescFlags bit 0x04000000 is set). + // + // Two tests: + // 1. WithIconOverlay — sets only the IconOverlay bit + the minimum + // intervening fields (none in this minimal body, so weenieFlags only has + // 0x40000000). Verifies the parse walks to IconOverlay and captures it. + // 2. WithIconOverlayAndUnderlay — sets IconOverlay + the IncludesSecondHeader + // objDescFlag + weenieFlags2 bit 0x01, writes both ids, asserts both are + // captured. + // 3. NoOverlayBits_CommonCase — weenieFlags=0, verifies the extended walk + // produces no overlay (regression guard for the common spawn path). + // ----------------------------------------------------------------------- + + [Fact] + public void TryParse_IconOverlay_CapturedFromExtendedTail() + { + // Only IconOverlay (0x40000000) bit set in weenieFlags. No intervening + // optional fields, so the extended tail immediately reads the overlay id. + // ACE WritePackedDwordOfKnownType strips the 0x06000000 prefix before + // packing; the reader ORs it back in. + byte[] body = BuildMinimalCreateObjectWithWeenieHeader( + guid: 0x5000000Au, + name: "EnchantedSword", + itemType: (uint)ItemType.MeleeWeapon, + weenieFlags: 0x40000000u, // IconOverlay + iconOverlayId: 0x1ABCu); // will be read back as 0x06001ABC + + var parsed = CreateObject.TryParse(body); + + Assert.NotNull(parsed); + Assert.Equal(0x06001ABCu, parsed!.Value.IconOverlayId); + Assert.Equal(0u, parsed.Value.IconUnderlayId); + } + + [Fact] + public void TryParse_IconOverlayAndUnderlay_BothCaptured() + { + // IncludesSecondHeader in objDescFlags (0x04000000) makes the parser read + // weenieFlags2. weenieFlags2 bit 0x01 (IconUnderlay) triggers the underlay + // read. Both overlay + underlay are captured. + // objectDescriptionFlags: 0x04000000 = IncludesSecondHeader + // weenieFlags: 0x40000000 = IconOverlay + // weenieFlags2: 0x00000001 = IconUnderlay + byte[] body = BuildMinimalCreateObjectWithWeenieHeader( + guid: 0x5000000Bu, + name: "MagicRing", + itemType: (uint)ItemType.Jewelry, + objectDescriptionFlags: 0x04000000u, + weenieFlags: 0x40000000u, + weenieFlags2: 0x00000001u, + iconOverlayId: 0x5678u, // → 0x06005678 + iconUnderlayId: 0x9ABCu); // → 0x06009ABC + + var parsed = CreateObject.TryParse(body); + + Assert.NotNull(parsed); + Assert.Equal(0x06005678u, parsed!.Value.IconOverlayId); + Assert.Equal(0x06009ABCu, parsed.Value.IconUnderlayId); + } + + [Fact] + public void TryParse_NoOverlayBits_CommonCase_OverlaysStayZero() + { + // Regression guard: most spawned entities (creatures, scenery, players) + // have weenieFlags=0 and no second-header. The extended walk must not + // corrupt existing parsed fields and must leave overlay ids at zero. + byte[] body = BuildMinimalCreateObjectWithWeenieHeader( + guid: 0x5000000Cu, + name: "CommonDrudge", + itemType: (uint)ItemType.Creature, + weenieFlags: 0u); + + var parsed = CreateObject.TryParse(body); + + Assert.NotNull(parsed); + Assert.Equal("CommonDrudge", parsed!.Value.Name); + Assert.Equal(0u, parsed.Value.IconOverlayId); + Assert.Equal(0u, parsed.Value.IconUnderlayId); + Assert.Null(parsed.Value.Useability); + } + + [Fact] + public void TryParse_IntermediateFieldsBeforeIconOverlay_SkippedCorrectly() + { + // Verifies the cursor arithmetic for fields between UseRadius and + // IconOverlay. This body sets several intermediate bits (Structure u16, + // MaxStructure u16, StackSize u16, Burden u16) plus IconOverlay. + // If any skip is wrong, the parser reads the wrong bytes as the + // overlay id or throws, both of which the assert would catch. + // 0x00000400 = Structure (u16) + // 0x00000800 = MaxStructure (u16) + // 0x00001000 = StackSize (u16) + // 0x00200000 = Burden (u16) + // 0x40000000 = IconOverlay + const uint flags = 0x40000000u | 0x00200000u | 0x00001000u | 0x00000800u | 0x00000400u; + byte[] body = BuildMinimalCreateObjectWithWeenieHeader( + guid: 0x5000000Du, + name: "FancySword", + itemType: (uint)ItemType.MeleeWeapon, + weenieFlags: flags, + structure: 50, + maxStructure: 100, + stackSize: 1, + burden: 300, + iconOverlayId: 0x2222u); // → 0x06002222 + + var parsed = CreateObject.TryParse(body); + + Assert.NotNull(parsed); + Assert.Equal(0x06002222u, parsed!.Value.IconOverlayId); + } + private static byte[] BuildMinimalCreateObjectWithWeenieHeader( uint guid, string name, @@ -186,10 +302,18 @@ public sealed class CreateObjectTests uint physicsState = 0, uint objectDescriptionFlags = 0, uint weenieFlags = 0, + uint weenieFlags2 = 0, uint iconId = 0, uint? value = null, uint? useability = null, - float? useRadius = null) + float? useRadius = null, + uint iconOverlayId = 0, + uint iconUnderlayId = 0, + // intermediate fields for cursor-arithmetic test + ushort? structure = null, + ushort? maxStructure = null, + ushort? stackSize = null, + ushort? burden = null) { var bytes = new List(); WriteU32(bytes, CreateObject.Opcode); @@ -217,19 +341,67 @@ public sealed class CreateObjectTests WriteU32(bytes, objectDescriptionFlags); Align4(bytes); - // Optional WeenieHeader tail (2026-05-15) — same order as ACE - // WorldObject_Networking.cs:87-114. Each field is written only when + // IncludesSecondHeader → weenieFlags2 written immediately after the align, + // before any other optional tail field (ACE WorldObject_Networking.cs:84-85). + if ((objectDescriptionFlags & 0x04000000u) != 0) + WriteU32(bytes, weenieFlags2); + + // Optional WeenieHeader tail — same order as ACE + // WorldObject_Networking.cs:87-206. Each field is written only when // its weenieFlags bit is set, matching the parser's walker exactly. - if ((weenieFlags & 0x00000008u) != 0) // Value u32 - WriteU32(bytes, value ?? 0u); - if ((weenieFlags & 0x00000010u) != 0) // Useability u32 - WriteU32(bytes, useability ?? 0u); - if ((weenieFlags & 0x00000020u) != 0) // UseRadius f32 + // 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 & 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 + if ((weenieFlags & 0x00000020u) != 0) // UseRadius f32 { Span tmp = stackalloc byte[4]; BinaryPrimitives.WriteSingleLittleEndian(tmp, useRadius ?? 0f); bytes.AddRange(tmp.ToArray()); } + if ((weenieFlags & 0x00080000u) != 0) WriteU32(bytes, 0); // TargetType u32 + if ((weenieFlags & 0x00000080u) != 0) WriteU32(bytes, 0); // UiEffects u32 + if ((weenieFlags & 0x00000200u) != 0) bytes.Add(0); // CombatUse sbyte/1 byte + 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 & 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); + bytes.AddRange(tmp.ToArray()); + } + if ((weenieFlags & 0x00200000u) != 0) WriteU16(bytes, burden ?? 0); // Burden u16 + if ((weenieFlags & 0x00400000u) != 0) WriteU16(bytes, 0); // Spell u16 + if ((weenieFlags & 0x02000000u) != 0) WriteU32(bytes, 0); // HouseOwner u32 + // HouseRestrictions (0x04000000): not parameterized (zero entries). + // Wire: Version(u32) + OpenStatus(u32) + MonarchId(u32) + count(u16) + numBuckets(u16) + entries. + // Zero entries → 16 bytes total. + if ((weenieFlags & 0x04000000u) != 0) + { + WriteU32(bytes, 0x10000002u); // Version + WriteU32(bytes, 0u); // OpenStatus + WriteU32(bytes, 0u); // MonarchId + WriteU16(bytes, 0); // count + WriteU16(bytes, 768); // numBuckets (retail constant) + } + if ((weenieFlags & 0x20000000u) != 0) WriteU32(bytes, 0); // HookItemTypes u32 + if ((weenieFlags & 0x00000040u) != 0) WriteU32(bytes, 0); // Monarch u32 + if ((weenieFlags & 0x10000000u) != 0) WriteU16(bytes, 0); // HookType u16 + if ((weenieFlags & 0x40000000u) != 0) WritePackedDword(bytes, iconOverlayId); // IconOverlay + if ((weenieFlags2 & 0x00000001u) != 0) WritePackedDword(bytes, iconUnderlayId); // IconUnderlay return bytes.ToArray(); }