Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
The WeenieHeader fixed prefix plus the ~300-line conditional optional-tail cascade moves verbatim from CreateObject.TryParse into PublicWeenieDescParser.Parse (PublicWeenieDescBody.cs), so Slice 5.1's vendor-item parser can share it instead of duplicating it — each shop item on the wire is a full CreateObject-style PublicWeenieDesc (research doc §A.2). Same field order, same nested try/catch swallow shape, same truncation messages; CreateObject composes its Parsed from the returned record. Zero test files changed; Core.Net tests 764/0/0 unchanged; clean-room complete solution 11,271 passed / 4 skipped / 0 failed. Per the Slice 5 contract (decision 2): extraction FIRST, as its own bisectable commit, before any vendor code exists to call it. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
510 lines
25 KiB
C#
510 lines
25 KiB
C#
using System.Buffers.Binary;
|
|
using System.Collections.Generic;
|
|
using AcDream.Core.Items;
|
|
|
|
namespace AcDream.Core.Net.Messages;
|
|
|
|
/// <summary>
|
|
/// The result of parsing a retail <c>PublicWeenieDesc</c> body: the fixed
|
|
/// weenieFlags/name/wcid/icon/itemType/objDescFlags prefix, plus every
|
|
/// conditional optional-tail field, in exact wire order.
|
|
///
|
|
/// <para>
|
|
/// This is the SAME parse retail's <c>PublicWeenieDesc::UnPack</c> performs
|
|
/// (and ACE's <c>WorldObject_Networking.SerializeCreateObject</c> writes) at
|
|
/// two distinct wire sites: the <c>CreateObject</c> GameMessage (0xF745,
|
|
/// where this body follows Guid + ModelData + PhysicsData), and the vendor
|
|
/// <c>ApproachVendor</c> item list (GameEvent 0x0062, where — per ACE's
|
|
/// <c>gamedataonly</c> serialization — this body follows only the packed
|
|
/// stack-size dword and the item's Guid; there is no ModelData/PhysicsData
|
|
/// at all for a vendor's listed items). See
|
|
/// <c>docs/research/2026-08-08-slice5-vendor-browse-research.md</c> §A.2.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// Extracted from <see cref="CreateObject.TryParse"/> (Slice 5.0,
|
|
/// <c>docs/plans/2026-07-23-world-interaction-completion.md</c> Slice 5
|
|
/// decision 2) so the Slice 5.1 vendor item-list parser can reuse this exact
|
|
/// ~300-line conditional field cascade instead of duplicating it.
|
|
/// Extraction only — the field set, wire order, and truncation/failure
|
|
/// behavior are unchanged from the original inline code.
|
|
/// </para>
|
|
/// </summary>
|
|
public readonly record struct PublicWeenieDescBody(
|
|
string? Name = null,
|
|
uint? ItemType = null,
|
|
uint? ObjectDescriptionFlags = null,
|
|
uint IconId = 0,
|
|
uint WeenieClassId = 0,
|
|
int? Value = null,
|
|
int? StackSize = null,
|
|
int? StackSizeMax = null,
|
|
int? Burden = null,
|
|
int? ItemsCapacity = null,
|
|
int? ContainersCapacity = null,
|
|
uint? HookItemTypes = null,
|
|
uint? HookType = 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,
|
|
uint? Useability = null,
|
|
float? UseRadius = null,
|
|
uint? TargetType = null,
|
|
uint IconOverlayId = 0,
|
|
uint IconUnderlayId = 0,
|
|
uint UiEffects = 0,
|
|
byte? RadarBlipColor = null,
|
|
byte? RadarBehavior = null,
|
|
byte? CombatUse = null,
|
|
string? PluralName = null,
|
|
uint? PetOwnerId = null,
|
|
ushort? AmmoType = null,
|
|
uint? SpellId = null,
|
|
uint? CooldownId = null,
|
|
double? CooldownDuration = null,
|
|
uint? MaterialType = null,
|
|
uint? HouseOwnerId = null,
|
|
uint? MonarchId = null,
|
|
HouseRestrictionRecord? Restrictions = null);
|
|
|
|
/// <summary>
|
|
/// Parses the shared <see cref="PublicWeenieDescBody"/> wire shape. See the
|
|
/// type doc above for the two call sites and the extraction rationale.
|
|
/// </summary>
|
|
public static class PublicWeenieDescParser
|
|
{
|
|
/// <summary>
|
|
/// Parse a PublicWeenieDesc body starting at <paramref name="pos"/>.
|
|
/// Never throws: both the fixed-prefix read and the optional-tail walk
|
|
/// are wrapped in their own try/catch (matching the pre-extraction
|
|
/// inline code exactly), so a truncated body degrades gracefully —
|
|
/// whatever fields parsed before the truncation are kept, and the
|
|
/// cursor position reflects exactly how far the original inline
|
|
/// parser would have advanced.
|
|
/// </summary>
|
|
public static PublicWeenieDescBody Parse(ReadOnlySpan<byte> body, ref int pos)
|
|
{
|
|
// --- WeenieHeader: read the fixed prefix fields we need. ---
|
|
// ACE WorldObject_Networking.SerializeCreateObject writes:
|
|
// weenieFlags, Name, WeenieClassId(PackedDword),
|
|
// IconId(PackedDwordOfKnownType 0x06000000), ItemType,
|
|
// ObjectDescriptionFlags, align.
|
|
string? name = null;
|
|
uint? itemType = null;
|
|
uint weenieFlags = 0;
|
|
string? pluralName = null;
|
|
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? wHookItemTypes = null;
|
|
uint? wHookType = 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;
|
|
uint? objectDescriptionFlags = null;
|
|
if (body.Length - pos >= 4)
|
|
{
|
|
weenieFlags = CreateObject.ReadU32(body, ref pos);
|
|
try
|
|
{
|
|
name = CreateObject.ReadString16L(body, ref pos);
|
|
weenieClassId = CreateObject.ReadPackedDword(body, ref pos); // WeenieClassId (D.5.4: was discarded)
|
|
iconId = CreateObject.ReadPackedDwordOfKnownType(body, ref pos, CreateObject.IconTypePrefix);
|
|
if (body.Length - pos >= 4)
|
|
itemType = CreateObject.ReadU32(body, ref pos);
|
|
if (body.Length - pos >= 4)
|
|
{
|
|
// ObjectDescriptionFlags = retail PWD._bitfield
|
|
// (acclient.h:6431-6463). Carries BF_PLAYER (0x8),
|
|
// BF_PLAYER_KILLER (0x20), BF_FREE_PKSTATUS (0x200000),
|
|
// BF_PKLITE_PKSTATUS (0x2000000) — the bits that
|
|
// acclient_2013_pseudo_c.txt:406898-406918 read for
|
|
// IsPK() / IsPKLite() / IsImpenetrable(). Previously
|
|
// discarded; now surfaced for the PvP collision rule.
|
|
objectDescriptionFlags = CreateObject.ReadU32(body, ref pos);
|
|
}
|
|
CreateObject.AlignTo4(ref pos);
|
|
}
|
|
catch { /* truncated name — partial result is still useful */ }
|
|
}
|
|
|
|
// --- WeenieHeader optional tail: walk every conditional field
|
|
// in EXACT ACE write order (WorldObject_Networking.cs:87-219)
|
|
// so the cursor reaches IconOverlay + IconUnderlay.
|
|
//
|
|
// 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.
|
|
//
|
|
// Wire order (verified against ACE WorldObject_Networking.cs):
|
|
// bit field width
|
|
// --------- ------------------ -----
|
|
// 0x04000000 (objDescFlags) weenieFlags2 u32 (skip)
|
|
// 0x00000001 PluralName String16L CAPTURE
|
|
// 0x00000002 ItemsCapacity s8 -> int CAPTURE
|
|
// 0x00000004 ContainersCapacity s8 -> int CAPTURE
|
|
// 0x00000100 AmmoType u16 (skip)
|
|
// 0x00000008 Value u32 (skip)
|
|
// 0x00000010 Usable u32 KEPT
|
|
// 0x00000020 UseRadius f32 KEPT
|
|
// 0x00080000 TargetType u32 (skip)
|
|
// 0x00000080 UiEffects u32 CAPTURE (D.5.2)
|
|
// 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 CAPTURE
|
|
// 0x00800000 RadarBehavior u8 CAPTURE
|
|
// 0x08000000 PScript u16 (skip)
|
|
// 0x01000000 Workmanship f32 (skip)
|
|
// 0x00200000 Burden u16 (skip)
|
|
// 0x00400000 Spell u16 (skip)
|
|
// 0x02000000 HouseOwner u32 CAPTURE (AP-129)
|
|
// 0x04000000 HouseRestrictions RestrictionDB CAPTURE (AP-129, variable-length)
|
|
// 0x20000000 HookItemTypes u32 CAPTURE
|
|
// 0x00000040 Monarch u32 CAPTURE (AP-129)
|
|
// 0x10000000 HookType u16 CAPTURE
|
|
// 0x40000000 IconOverlay PackedDwordKnownType(0x06000000) CAPTURE
|
|
// weenieFlags2 bit 0x01:
|
|
// IconUnderlay PackedDwordKnownType(0x06000000) CAPTURE
|
|
// 0x80000000 MaterialType u32 CAPTURE
|
|
// weenieFlags2 bit 0x02:
|
|
// CooldownId u32 CAPTURE
|
|
// weenieFlags2 bit 0x04:
|
|
// CooldownDuration f64 CAPTURE
|
|
// weenieFlags2 bit 0x08:
|
|
// PetOwner u32 CAPTURE
|
|
//
|
|
// 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? targetType = null;
|
|
byte? radarBlipColor = null;
|
|
byte? radarBehavior = null;
|
|
byte? combatUse = null;
|
|
ushort? ammoType = null;
|
|
uint? spellId = null;
|
|
uint iconOverlayId = 0;
|
|
uint iconUnderlayId = 0;
|
|
uint uiEffects = 0;
|
|
uint weenieFlags2 = 0;
|
|
uint? petOwnerId = null;
|
|
uint? cooldownId = null;
|
|
double? cooldownDuration = null;
|
|
uint? materialType = null;
|
|
// AP-129 (Campaign P Slice P4 review fix, 2026-07-30): house-restriction
|
|
// PWD tail fields, previously skipped. HouseOwner/Monarch are plain u32
|
|
// ids; HouseRestrictions is the full RestrictionDB (open flag + allegiance
|
|
// monarch + guest table) — see AcDream.Core.Items.HouseRestrictionRecord.
|
|
uint? houseOwnerId = null;
|
|
uint? monarchId = null;
|
|
HouseRestrictionRecord? restrictions = null;
|
|
try
|
|
{
|
|
// BF_INCLUDES_SECOND_HEADER = 0x04000000 per acclient.h:6458
|
|
// (ACE ObjectDescriptionFlag.IncludesSecondHeader matches).
|
|
// 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. 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)
|
|
{
|
|
if (body.Length - pos < 4) throw new FormatException("trunc weenieFlags2");
|
|
weenieFlags2 = CreateObject.ReadU32(body, ref pos);
|
|
}
|
|
|
|
if ((weenieFlags & 0x00000001u) != 0) // PluralName
|
|
pluralName = CreateObject.ReadString16L(body, ref pos);
|
|
|
|
if ((weenieFlags & 0x00000002u) != 0) // ItemsCapacity s8 -> int
|
|
{
|
|
if (body.Length - pos < 1) throw new FormatException("trunc ItemCap");
|
|
// PublicWeenieDesc::UnPack @ 0x005AD52C uses MOVSX.
|
|
// ACE stores the creature/non-container sentinel -1 as
|
|
// 0xFF; retail retains -1 rather than turning it into 255.
|
|
wItemsCapacity = unchecked((sbyte)body[pos]);
|
|
pos += 1;
|
|
}
|
|
if ((weenieFlags & 0x00000004u) != 0) // ContainersCapacity s8 -> int
|
|
{
|
|
if (body.Length - pos < 1) throw new FormatException("trunc ContCap");
|
|
// PublicWeenieDesc::UnPack @ 0x005AD53E likewise uses
|
|
// MOVSX before assigning the 32-bit public field.
|
|
wContainersCapacity = unchecked((sbyte)body[pos]);
|
|
pos += 1;
|
|
}
|
|
if ((weenieFlags & 0x00000100u) != 0) // AmmoType u16
|
|
{
|
|
if (body.Length - pos < 2) throw new FormatException("trunc AmmoType");
|
|
ammoType = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos));
|
|
pos += 2;
|
|
}
|
|
if ((weenieFlags & 0x00000008u) != 0) // Value u32
|
|
{
|
|
if (body.Length - pos < 4) throw new FormatException("trunc Value");
|
|
wValue = (int)CreateObject.ReadU32(body, ref pos);
|
|
}
|
|
if ((weenieFlags & 0x00000010u) != 0) // Usable u32 ← KEEP
|
|
{
|
|
if (body.Length - pos < 4) throw new FormatException("trunc Useability");
|
|
useability = CreateObject.ReadU32(body, ref pos);
|
|
}
|
|
if ((weenieFlags & 0x00000020u) != 0) // UseRadius f32 ← KEEP
|
|
{
|
|
if (body.Length - pos < 4) throw new FormatException("trunc UseRadius");
|
|
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");
|
|
targetType = CreateObject.ReadU32(body, ref pos);
|
|
}
|
|
if ((weenieFlags & 0x00000080u) != 0) // UiEffects u32 ← CAPTURE
|
|
{
|
|
if (body.Length - pos < 4) throw new FormatException("trunc UiEffects");
|
|
uiEffects = CreateObject.ReadU32(body, ref pos);
|
|
}
|
|
if ((weenieFlags & 0x00000200u) != 0) // CombatUse sbyte (1 byte)
|
|
{
|
|
if (body.Length - pos < 1) throw new FormatException("trunc CombatUse");
|
|
combatUse = body[pos]; pos += 1;
|
|
}
|
|
if ((weenieFlags & 0x00000400u) != 0) // Structure u16
|
|
{
|
|
if (body.Length - pos < 2) throw new FormatException("trunc Structure");
|
|
wStructure = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos)); pos += 2;
|
|
}
|
|
if ((weenieFlags & 0x00000800u) != 0) // MaxStructure u16
|
|
{
|
|
if (body.Length - pos < 2) throw new FormatException("trunc MaxStructure");
|
|
wMaxStructure = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos)); pos += 2;
|
|
}
|
|
if ((weenieFlags & 0x00001000u) != 0) // StackSize u16
|
|
{
|
|
if (body.Length - pos < 2) throw new FormatException("trunc StackSize");
|
|
wStackSize = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos)); pos += 2;
|
|
}
|
|
if ((weenieFlags & 0x00002000u) != 0) // MaxStackSize u16
|
|
{
|
|
if (body.Length - pos < 2) throw new FormatException("trunc MaxStackSize");
|
|
wMaxStackSize = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos)); pos += 2;
|
|
}
|
|
if ((weenieFlags & 0x00004000u) != 0) // Container u32
|
|
{
|
|
if (body.Length - pos < 4) throw new FormatException("trunc Container");
|
|
wContainerId = CreateObject.ReadU32(body, ref pos);
|
|
}
|
|
if ((weenieFlags & 0x00008000u) != 0) // Wielder u32
|
|
{
|
|
if (body.Length - pos < 4) throw new FormatException("trunc Wielder");
|
|
wWielderId = CreateObject.ReadU32(body, ref pos);
|
|
}
|
|
if ((weenieFlags & 0x00010000u) != 0) // ValidLocations u32
|
|
{
|
|
if (body.Length - pos < 4) throw new FormatException("trunc ValidLocations");
|
|
wValidLocations = CreateObject.ReadU32(body, ref pos);
|
|
}
|
|
if ((weenieFlags & 0x00020000u) != 0) // CurrentlyWieldedLocation u32
|
|
{
|
|
if (body.Length - pos < 4) throw new FormatException("trunc CurrentlyWieldedLocation");
|
|
wCurrentWieldedLocation = CreateObject.ReadU32(body, ref pos);
|
|
}
|
|
if ((weenieFlags & 0x00040000u) != 0) // Priority u32
|
|
{
|
|
if (body.Length - pos < 4) throw new FormatException("trunc Priority");
|
|
wPriority = CreateObject.ReadU32(body, ref pos);
|
|
}
|
|
if ((weenieFlags & 0x00100000u) != 0) // RadarBlipColor u8
|
|
{
|
|
if (body.Length - pos < 1) throw new FormatException("trunc RadarBlipColor");
|
|
radarBlipColor = body[pos]; pos += 1;
|
|
}
|
|
if ((weenieFlags & 0x00800000u) != 0) // RadarBehavior u8
|
|
{
|
|
if (body.Length - pos < 1) throw new FormatException("trunc RadarBehavior");
|
|
radarBehavior = body[pos]; 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");
|
|
wWorkmanship = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos)); pos += 4;
|
|
}
|
|
if ((weenieFlags & 0x00200000u) != 0) // Burden u16
|
|
{
|
|
if (body.Length - pos < 2) throw new FormatException("trunc Burden");
|
|
wBurden = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos)); pos += 2;
|
|
}
|
|
if ((weenieFlags & 0x00400000u) != 0) // Spell u16
|
|
{
|
|
if (body.Length - pos < 2) throw new FormatException("trunc Spell");
|
|
spellId = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos));
|
|
pos += 2;
|
|
}
|
|
if ((weenieFlags & 0x02000000u) != 0) // HouseOwner u32 ← CAPTURE (AP-129)
|
|
{
|
|
if (body.Length - pos < 4) throw new FormatException("trunc HouseOwner");
|
|
houseOwnerId = CreateObject.ReadU32(body, ref pos);
|
|
}
|
|
if ((weenieFlags & 0x04000000u) != 0) // HouseRestrictions (RestrictionDB) ← CAPTURE (AP-129)
|
|
{
|
|
// Wire layout per Chorizite RestrictionDB.generated.cs (verified against
|
|
// protocol.xml:6270-6275): u32 Version, u32 Flags, u32 MonarchId, then a
|
|
// PHashTable<uint,uint> (NOT the u16-count/u16-size PackableHashTable shape
|
|
// this skip previously assumed — the byte total happened to match for any
|
|
// realistic guest-list size, but the count extraction was reading the wrong
|
|
// half of the packed dword). PHashTable's own header is ONE packed u32 whose
|
|
// low 24 bits are the entry count.
|
|
if (body.Length - pos < 12) throw new FormatException("trunc RestrictionDB header");
|
|
pos += 4; // Version — not consulted
|
|
uint flags = CreateObject.ReadU32(body, ref pos); // 0 = private, 1 = open
|
|
uint restrictionMonarchId = CreateObject.ReadU32(body, ref pos);
|
|
if (body.Length - pos < 4) throw new FormatException("trunc RestrictionDB PHashTable header");
|
|
uint packedSize = CreateObject.ReadU32(body, ref pos);
|
|
uint entryCount = packedSize & 0xFFFFFFu;
|
|
long entryBytes = (long)entryCount * 8; // each entry: u32 guid + u32 value
|
|
if (body.Length - pos < entryBytes) throw new FormatException("trunc RestrictionDB entries");
|
|
var guests = new Dictionary<uint, uint>((int)entryCount);
|
|
for (uint i = 0; i < entryCount; i++)
|
|
{
|
|
uint guestId = CreateObject.ReadU32(body, ref pos);
|
|
uint permission = CreateObject.ReadU32(body, ref pos);
|
|
guests[guestId] = permission;
|
|
}
|
|
restrictions = new HouseRestrictionRecord(
|
|
OpenToPublic: flags != 0,
|
|
AllegianceMonarchId: restrictionMonarchId,
|
|
Guests: guests);
|
|
}
|
|
if ((weenieFlags & 0x20000000u) != 0) // HookItemTypes u32
|
|
{
|
|
if (body.Length - pos < 4) throw new FormatException("trunc HookItemTypes");
|
|
wHookItemTypes = CreateObject.ReadU32(body, ref pos);
|
|
}
|
|
if ((weenieFlags & 0x00000040u) != 0) // Monarch u32 ← CAPTURE (AP-129)
|
|
{
|
|
if (body.Length - pos < 4) throw new FormatException("trunc Monarch");
|
|
monarchId = CreateObject.ReadU32(body, ref pos);
|
|
}
|
|
if ((weenieFlags & 0x10000000u) != 0) // HookType u16
|
|
{
|
|
if (body.Length - pos < 2) throw new FormatException("trunc HookType");
|
|
wHookType = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos));
|
|
pos += 2;
|
|
}
|
|
if ((weenieFlags & 0x40000000u) != 0) // IconOverlay PackedDwordOfKnownType(0x06000000) ← CAPTURE
|
|
{
|
|
iconOverlayId = CreateObject.ReadPackedDwordOfKnownType(body, ref pos, CreateObject.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 = CreateObject.ReadPackedDwordOfKnownType(body, ref pos, CreateObject.IconTypePrefix);
|
|
}
|
|
// PublicWeenieDesc::UnPack @ 0x005AD7AC..0x005AD7F8 and ACE
|
|
// SerializeCreateObject both place these fields after IconUnderlay.
|
|
if ((weenieFlags & 0x80000000u) != 0) // MaterialType u32
|
|
{
|
|
if (body.Length - pos < 4) throw new FormatException("trunc MaterialType");
|
|
materialType = CreateObject.ReadU32(body, ref pos);
|
|
}
|
|
if ((weenieFlags2 & 0x00000002u) != 0) // CooldownId u32
|
|
{
|
|
if (body.Length - pos < 4) throw new FormatException("trunc CooldownId");
|
|
cooldownId = CreateObject.ReadU32(body, ref pos);
|
|
}
|
|
if ((weenieFlags2 & 0x00000004u) != 0) // CooldownDuration f64
|
|
{
|
|
if (body.Length - pos < 8) throw new FormatException("trunc CooldownDuration");
|
|
cooldownDuration = BinaryPrimitives.ReadDoubleLittleEndian(body.Slice(pos));
|
|
pos += 8;
|
|
}
|
|
if ((weenieFlags2 & 0x00000008u) != 0) // PetOwner u32
|
|
{
|
|
if (body.Length - pos < 4) throw new FormatException("trunc PetOwner");
|
|
petOwnerId = CreateObject.ReadU32(body, ref pos);
|
|
}
|
|
}
|
|
catch { /* truncated weenie tail — keep whatever we got. */ }
|
|
|
|
return new PublicWeenieDescBody(
|
|
Name: name,
|
|
ItemType: itemType,
|
|
ObjectDescriptionFlags: objectDescriptionFlags,
|
|
IconId: iconId,
|
|
WeenieClassId: weenieClassId,
|
|
Value: wValue,
|
|
StackSize: wStackSize,
|
|
StackSizeMax: wMaxStackSize,
|
|
Burden: wBurden,
|
|
ItemsCapacity: wItemsCapacity,
|
|
ContainersCapacity: wContainersCapacity,
|
|
HookItemTypes: wHookItemTypes,
|
|
HookType: wHookType,
|
|
ContainerId: wContainerId,
|
|
WielderId: wWielderId,
|
|
ValidLocations: wValidLocations,
|
|
CurrentWieldedLocation: wCurrentWieldedLocation,
|
|
Priority: wPriority,
|
|
Structure: wStructure,
|
|
MaxStructure: wMaxStructure,
|
|
Workmanship: wWorkmanship,
|
|
Useability: useability,
|
|
UseRadius: useRadius,
|
|
TargetType: targetType,
|
|
IconOverlayId: iconOverlayId,
|
|
IconUnderlayId: iconUnderlayId,
|
|
UiEffects: uiEffects,
|
|
RadarBlipColor: radarBlipColor,
|
|
RadarBehavior: radarBehavior,
|
|
CombatUse: combatUse,
|
|
PluralName: pluralName,
|
|
PetOwnerId: petOwnerId,
|
|
AmmoType: ammoType,
|
|
SpellId: spellId,
|
|
CooldownId: cooldownId,
|
|
CooldownDuration: cooldownDuration,
|
|
MaterialType: materialType,
|
|
HouseOwnerId: houseOwnerId,
|
|
MonarchId: monarchId,
|
|
Restrictions: restrictions);
|
|
}
|
|
}
|