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
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:
parent
763b127ad9
commit
7bd4388b6b
2 changed files with 560 additions and 406 deletions
|
|
@ -535,7 +535,9 @@ public static class CreateObject
|
|||
// _bitfield (acclient.h:6431-6463) was previously discarded as
|
||||
// "ObjectDescriptionFlags" at the WeenieHeader trailer.
|
||||
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
|
||||
// when their PhysicsDescriptionFlag bits are set. Default values
|
||||
// come from PhysicsBody constructors; these overrides drive the
|
||||
|
|
@ -778,383 +780,20 @@ public static class CreateObject
|
|||
DefaultScriptIntensity: defaultScriptIntensity,
|
||||
Timestamps: timestamps);
|
||||
|
||||
// --- 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;
|
||||
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. */ }
|
||||
// --- WeenieHeader / PublicWeenieDesc body ---
|
||||
// Extracted (Slice 5.0, world-interaction-completion plan
|
||||
// decision 2) into PublicWeenieDescParser.Parse
|
||||
// (PublicWeenieDescBody.cs) so the Slice 5.1 vendor item-list
|
||||
// parser (ApproachVendor, GameEvent 0x0062) can share this exact
|
||||
// conditional field cascade instead of duplicating it.
|
||||
// Extraction only — same fields, same order, same truncation
|
||||
// handling as the original inline code (both inner try/catches
|
||||
// there swallowed and returned a partial result; this call does
|
||||
// the same).
|
||||
var desc = PublicWeenieDescParser.Parse(body, ref pos);
|
||||
|
||||
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,
|
||||
movementSeq,
|
||||
PositionSequence: positionSeq,
|
||||
|
|
@ -1162,34 +801,34 @@ public static class CreateObject
|
|||
ParentLocation: parentLocation,
|
||||
PlacementId: placementId,
|
||||
PhysicsState: physicsState,
|
||||
ObjectDescriptionFlags: objectDescriptionFlags,
|
||||
ObjectDescriptionFlags: desc.ObjectDescriptionFlags,
|
||||
Friction: friction,
|
||||
Elasticity: elasticity,
|
||||
IconId: iconId,
|
||||
Useability: useability, UseRadius: useRadius, TargetType: targetType,
|
||||
IconOverlayId: iconOverlayId, IconUnderlayId: iconUnderlayId,
|
||||
UiEffects: uiEffects,
|
||||
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,
|
||||
RadarBlipColor: radarBlipColor, RadarBehavior: radarBehavior,
|
||||
CombatUse: combatUse,
|
||||
PluralName: pluralName,
|
||||
PetOwnerId: petOwnerId,
|
||||
AmmoType: ammoType,
|
||||
SpellId: spellId,
|
||||
CooldownId: cooldownId,
|
||||
CooldownDuration: cooldownDuration,
|
||||
IconId: desc.IconId,
|
||||
Useability: desc.Useability, UseRadius: desc.UseRadius, TargetType: desc.TargetType,
|
||||
IconOverlayId: desc.IconOverlayId, IconUnderlayId: desc.IconUnderlayId,
|
||||
UiEffects: desc.UiEffects,
|
||||
WeenieClassId: desc.WeenieClassId,
|
||||
Value: desc.Value, StackSize: desc.StackSize, StackSizeMax: desc.StackSizeMax,
|
||||
Burden: desc.Burden, ItemsCapacity: desc.ItemsCapacity, ContainersCapacity: desc.ContainersCapacity,
|
||||
HookItemTypes: desc.HookItemTypes, HookType: desc.HookType,
|
||||
ContainerId: desc.ContainerId, WielderId: desc.WielderId,
|
||||
ValidLocations: desc.ValidLocations, CurrentWieldedLocation: desc.CurrentWieldedLocation,
|
||||
Priority: desc.Priority, Structure: desc.Structure, MaxStructure: desc.MaxStructure,
|
||||
Workmanship: desc.Workmanship,
|
||||
RadarBlipColor: desc.RadarBlipColor, RadarBehavior: desc.RadarBehavior,
|
||||
CombatUse: desc.CombatUse,
|
||||
PluralName: desc.PluralName,
|
||||
PetOwnerId: desc.PetOwnerId,
|
||||
AmmoType: desc.AmmoType,
|
||||
SpellId: desc.SpellId,
|
||||
CooldownId: desc.CooldownId,
|
||||
CooldownDuration: desc.CooldownDuration,
|
||||
Physics: physics,
|
||||
MaterialType: materialType,
|
||||
HouseOwnerId: houseOwnerId,
|
||||
MonarchId: monarchId,
|
||||
Restrictions: restrictions);
|
||||
MaterialType: desc.MaterialType,
|
||||
HouseOwnerId: desc.HouseOwnerId,
|
||||
MonarchId: desc.MonarchId,
|
||||
Restrictions: desc.Restrictions);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -1271,7 +910,12 @@ public static class CreateObject
|
|||
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");
|
||||
uint v = BinaryPrimitives.ReadUInt32LittleEndian(source.Slice(pos));
|
||||
|
|
@ -1279,7 +923,7 @@ public static class CreateObject
|
|||
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");
|
||||
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-
|
||||
/// bit marker in the first u16, then reads the second u16.
|
||||
/// </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");
|
||||
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
|
||||
/// (a 0 means "no value" and must not be turned into <c>knownType</c>).
|
||||
/// </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);
|
||||
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;
|
||||
pos += padding;
|
||||
|
|
|
|||
510
src/AcDream.Core.Net/Messages/PublicWeenieDescBody.cs
Normal file
510
src/AcDream.Core.Net/Messages/PublicWeenieDescBody.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue