refactor(net): Slice 5.0 — extract the PublicWeenieDesc body parser for shared use (behavior-preserving)
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>
This commit is contained in:
Erik 2026-08-07 14:41:09 +02:00
parent 763b127ad9
commit 7bd4388b6b
2 changed files with 560 additions and 406 deletions

View file

@ -535,7 +535,9 @@ public static class CreateObject
// _bitfield (acclient.h:6431-6463) was previously discarded as // _bitfield (acclient.h:6431-6463) was previously discarded as
// "ObjectDescriptionFlags" at the WeenieHeader trailer. // "ObjectDescriptionFlags" at the WeenieHeader trailer.
uint? physicsState = null; uint? physicsState = null;
uint? objectDescriptionFlags = null; // ObjectDescriptionFlags (retail PWD._bitfield) is captured inside
// PublicWeenieDescParser.Parse below (Slice 5.0 extraction) — no
// longer a top-level accumulator here.
// L.3b (2026-04-30): per-object friction + elasticity. Wire-encoded // L.3b (2026-04-30): per-object friction + elasticity. Wire-encoded
// when their PhysicsDescriptionFlag bits are set. Default values // when their PhysicsDescriptionFlag bits are set. Default values
// come from PhysicsBody constructors; these overrides drive the // come from PhysicsBody constructors; these overrides drive the
@ -778,383 +780,20 @@ public static class CreateObject
DefaultScriptIntensity: defaultScriptIntensity, DefaultScriptIntensity: defaultScriptIntensity,
Timestamps: timestamps); Timestamps: timestamps);
// --- WeenieHeader: read the fixed prefix fields we need. --- // --- WeenieHeader / PublicWeenieDesc body ---
// ACE WorldObject_Networking.SerializeCreateObject writes: // Extracted (Slice 5.0, world-interaction-completion plan
// weenieFlags, Name, WeenieClassId(PackedDword), // decision 2) into PublicWeenieDescParser.Parse
// IconId(PackedDwordOfKnownType 0x06000000), ItemType, // (PublicWeenieDescBody.cs) so the Slice 5.1 vendor item-list
// ObjectDescriptionFlags, align. // parser (ApproachVendor, GameEvent 0x0062) can share this exact
string? name = null; // conditional field cascade instead of duplicating it.
uint? itemType = null; // Extraction only — same fields, same order, same truncation
uint weenieFlags = 0; // handling as the original inline code (both inner try/catches
string? pluralName = null; // there swallowed and returned a partial result; this call does
uint iconId = 0; // the same).
uint weenieClassId = 0; var desc = PublicWeenieDescParser.Parse(body, ref pos);
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;
if (body.Length - pos >= 4)
{
weenieFlags = ReadU32(body, ref pos);
try
{
name = ReadString16L(body, ref pos);
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);
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 = ReadU32(body, ref pos);
}
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 = ReadU32(body, ref pos);
}
if ((weenieFlags & 0x00000001u) != 0) // PluralName
pluralName = 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)ReadU32(body, ref pos);
}
if ((weenieFlags & 0x00000010u) != 0) // Usable u32 ← KEEP
{
if (body.Length - pos < 4) throw new FormatException("trunc Useability");
useability = 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 = ReadU32(body, ref pos);
}
if ((weenieFlags & 0x00000080u) != 0) // UiEffects u32 ← CAPTURE
{
if (body.Length - pos < 4) throw new FormatException("trunc UiEffects");
uiEffects = 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 = ReadU32(body, ref pos);
}
if ((weenieFlags & 0x00008000u) != 0) // Wielder u32
{
if (body.Length - pos < 4) throw new FormatException("trunc Wielder");
wWielderId = ReadU32(body, ref pos);
}
if ((weenieFlags & 0x00010000u) != 0) // ValidLocations u32
{
if (body.Length - pos < 4) throw new FormatException("trunc ValidLocations");
wValidLocations = ReadU32(body, ref pos);
}
if ((weenieFlags & 0x00020000u) != 0) // CurrentlyWieldedLocation u32
{
if (body.Length - pos < 4) throw new FormatException("trunc CurrentlyWieldedLocation");
wCurrentWieldedLocation = ReadU32(body, ref pos);
}
if ((weenieFlags & 0x00040000u) != 0) // Priority u32
{
if (body.Length - pos < 4) throw new FormatException("trunc Priority");
wPriority = 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 = 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 = ReadU32(body, ref pos); // 0 = private, 1 = open
uint restrictionMonarchId = ReadU32(body, ref pos);
if (body.Length - pos < 4) throw new FormatException("trunc RestrictionDB PHashTable header");
uint packedSize = 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 = ReadU32(body, ref pos);
uint permission = 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 = ReadU32(body, ref pos);
}
if ((weenieFlags & 0x00000040u) != 0) // Monarch u32 ← CAPTURE (AP-129)
{
if (body.Length - pos < 4) throw new FormatException("trunc Monarch");
monarchId = 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 = 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);
}
// 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 = ReadU32(body, ref pos);
}
if ((weenieFlags2 & 0x00000002u) != 0) // CooldownId u32
{
if (body.Length - pos < 4) throw new FormatException("trunc CooldownId");
cooldownId = 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 = ReadU32(body, ref pos);
}
}
catch { /* truncated weenie tail — keep whatever we got. */ }
return new Parsed(guid, position, setupTableId, animParts, return new Parsed(guid, position, setupTableId, animParts,
textureChanges, subPalettes, basePaletteId, objScale, name, itemType, motionState, motionTableId, textureChanges, subPalettes, basePaletteId, objScale, desc.Name, desc.ItemType, motionState, motionTableId,
instanceSeq, teleportSeq, serverControlSeq, forcePositionSeq, instanceSeq, teleportSeq, serverControlSeq, forcePositionSeq,
movementSeq, movementSeq,
PositionSequence: positionSeq, PositionSequence: positionSeq,
@ -1162,34 +801,34 @@ public static class CreateObject
ParentLocation: parentLocation, ParentLocation: parentLocation,
PlacementId: placementId, PlacementId: placementId,
PhysicsState: physicsState, PhysicsState: physicsState,
ObjectDescriptionFlags: objectDescriptionFlags, ObjectDescriptionFlags: desc.ObjectDescriptionFlags,
Friction: friction, Friction: friction,
Elasticity: elasticity, Elasticity: elasticity,
IconId: iconId, IconId: desc.IconId,
Useability: useability, UseRadius: useRadius, TargetType: targetType, Useability: desc.Useability, UseRadius: desc.UseRadius, TargetType: desc.TargetType,
IconOverlayId: iconOverlayId, IconUnderlayId: iconUnderlayId, IconOverlayId: desc.IconOverlayId, IconUnderlayId: desc.IconUnderlayId,
UiEffects: uiEffects, UiEffects: desc.UiEffects,
WeenieClassId: weenieClassId, WeenieClassId: desc.WeenieClassId,
Value: wValue, StackSize: wStackSize, StackSizeMax: wMaxStackSize, Value: desc.Value, StackSize: desc.StackSize, StackSizeMax: desc.StackSizeMax,
Burden: wBurden, ItemsCapacity: wItemsCapacity, ContainersCapacity: wContainersCapacity, Burden: desc.Burden, ItemsCapacity: desc.ItemsCapacity, ContainersCapacity: desc.ContainersCapacity,
HookItemTypes: wHookItemTypes, HookType: wHookType, HookItemTypes: desc.HookItemTypes, HookType: desc.HookType,
ContainerId: wContainerId, WielderId: wWielderId, ContainerId: desc.ContainerId, WielderId: desc.WielderId,
ValidLocations: wValidLocations, CurrentWieldedLocation: wCurrentWieldedLocation, ValidLocations: desc.ValidLocations, CurrentWieldedLocation: desc.CurrentWieldedLocation,
Priority: wPriority, Structure: wStructure, MaxStructure: wMaxStructure, Priority: desc.Priority, Structure: desc.Structure, MaxStructure: desc.MaxStructure,
Workmanship: wWorkmanship, Workmanship: desc.Workmanship,
RadarBlipColor: radarBlipColor, RadarBehavior: radarBehavior, RadarBlipColor: desc.RadarBlipColor, RadarBehavior: desc.RadarBehavior,
CombatUse: combatUse, CombatUse: desc.CombatUse,
PluralName: pluralName, PluralName: desc.PluralName,
PetOwnerId: petOwnerId, PetOwnerId: desc.PetOwnerId,
AmmoType: ammoType, AmmoType: desc.AmmoType,
SpellId: spellId, SpellId: desc.SpellId,
CooldownId: cooldownId, CooldownId: desc.CooldownId,
CooldownDuration: cooldownDuration, CooldownDuration: desc.CooldownDuration,
Physics: physics, Physics: physics,
MaterialType: materialType, MaterialType: desc.MaterialType,
HouseOwnerId: houseOwnerId, HouseOwnerId: desc.HouseOwnerId,
MonarchId: monarchId, MonarchId: desc.MonarchId,
Restrictions: restrictions); Restrictions: desc.Restrictions);
} }
catch catch
{ {
@ -1271,7 +910,12 @@ public static class CreateObject
return new ModelData(basePaletteId, subPalettes, textureChanges, animParts); return new ModelData(basePaletteId, subPalettes, textureChanges, animParts);
} }
private static uint ReadU32(ReadOnlySpan<byte> source, ref int pos) // Widened from `private` to `internal` (Slice 5.0, extraction only) so
// PublicWeenieDescParser.Parse (PublicWeenieDescBody.cs) — the shared
// desc-body parser used by both CreateObject and the upcoming vendor
// item-list parser — can reuse these primitive readers instead of
// duplicating them. No behavior change.
internal static uint ReadU32(ReadOnlySpan<byte> source, ref int pos)
{ {
if (source.Length - pos < 4) throw new FormatException("truncated u32"); if (source.Length - pos < 4) throw new FormatException("truncated u32");
uint v = BinaryPrimitives.ReadUInt32LittleEndian(source.Slice(pos)); uint v = BinaryPrimitives.ReadUInt32LittleEndian(source.Slice(pos));
@ -1279,7 +923,7 @@ public static class CreateObject
return v; return v;
} }
private static string ReadString16L(ReadOnlySpan<byte> source, ref int pos) internal static string ReadString16L(ReadOnlySpan<byte> source, ref int pos)
{ {
if (source.Length - pos < 2) throw new FormatException("truncated String16L length"); if (source.Length - pos < 2) throw new FormatException("truncated String16L length");
ushort length = BinaryPrimitives.ReadUInt16LittleEndian(source.Slice(pos)); ushort length = BinaryPrimitives.ReadUInt16LittleEndian(source.Slice(pos));
@ -1307,7 +951,7 @@ public static class CreateObject
/// written as a little-endian u32. The reader is the inverse — sees the high- /// written as a little-endian u32. The reader is the inverse — sees the high-
/// bit marker in the first u16, then reads the second u16. /// bit marker in the first u16, then reads the second u16.
/// </summary> /// </summary>
private static uint ReadPackedDword(ReadOnlySpan<byte> source, ref int pos) internal static uint ReadPackedDword(ReadOnlySpan<byte> source, ref int pos)
{ {
if (source.Length - pos < 2) throw new FormatException("truncated PackedDword"); if (source.Length - pos < 2) throw new FormatException("truncated PackedDword");
ushort first = BinaryPrimitives.ReadUInt16LittleEndian(source.Slice(pos)); ushort first = BinaryPrimitives.ReadUInt16LittleEndian(source.Slice(pos));
@ -1331,13 +975,13 @@ public static class CreateObject
/// recover the original dat id. The zero sentinel is preserved as-is /// recover the original dat id. The zero sentinel is preserved as-is
/// (a 0 means "no value" and must not be turned into <c>knownType</c>). /// (a 0 means "no value" and must not be turned into <c>knownType</c>).
/// </summary> /// </summary>
private static uint ReadPackedDwordOfKnownType(ReadOnlySpan<byte> source, ref int pos, uint knownType) internal static uint ReadPackedDwordOfKnownType(ReadOnlySpan<byte> source, ref int pos, uint knownType)
{ {
uint packed = ReadPackedDword(source, ref pos); uint packed = ReadPackedDword(source, ref pos);
return packed == 0 ? 0 : (packed | knownType); return packed == 0 ? 0 : (packed | knownType);
} }
private static void AlignTo4(ref int pos) internal static void AlignTo4(ref int pos)
{ {
int padding = (4 - (pos & 3)) & 3; int padding = (4 - (pos & 3)) & 3;
pos += padding; pos += padding;

View file

@ -0,0 +1,510 @@
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);
}
}