feat(D.5.1): parse item IconOverlay/IconUnderlay from CreateObject -> faithful icon overlay layer
The CreateObject optional-tail walker previously stopped at UseRadius (~20 fields before IconOverlay). This left ItemInstance.IconOverlayId/IconUnderlayId always 0, so IconComposer's underlay/overlay layers were never drawn on toolbar icons. Exact field order verified against ACE WorldObject_Networking.cs:87-219 (the serializer is the authority; acdream connects to a local ACE server): UseRadius → TargetType(u32) → UiEffects(u32) → CombatUse(sbyte) → Structure(u16) → MaxStructure(u16) → StackSize(u16) → MaxStackSize(u16) → Container(u32) → Wielder(u32) → ValidLocations(u32) → CurrentlyWieldedLocation(u32) → Priority(u32) → RadarBlipColor(u8) → RadarBehavior(u8) → PScript(u16) → Workmanship(f32) → Burden(u16) → Spell(u16) → HouseOwner(u32) → HouseRestrictions(variable RestrictionDB) → HookItemTypes(u32) → Monarch(u32) → HookType(u16) → IconOverlay(PackedDwordKnownType) ← CAPTURE → IconUnderlay from weenieFlags2 bit 0x01 ← CAPTURE RestrictionDB handled correctly: Version(u32) + OpenStatus(u32) + MonarchId(u32) + count(u16) + numBuckets(u16) + count×8 bytes entries. Length-aware skip, not a fixed constant. weenieFlags2 is now CAPTURED (not skipped) when IncludesSecondHeader (objDescFlags bit 0x04000000) is set, so the IconUnderlay bit can be tested. The entire extended walk is inside try/catch: truncated packets degrade to IconOverlayId=0 / IconUnderlayId=0 (no overlay drawn), never corrupting. Threading: CreateObject.Parsed → WorldSession.EntitySpawn → GameWindow OnLiveEntitySpawned → Items.EnrichItem — both ids thread through all three seams. EnrichItem extended with optional iconOverlayId + iconUnderlayId params (defaulted 0, backward-compatible). No change to IconComposer or ToolbarController (they already consume the ids). Tests: 4 new CreateObject tests (IconOverlay only, overlay+underlay, no-overlay regression, intermediate-fields cursor arithmetic). Full suite: 0 failures, 2636 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a7cad5566b
commit
8a42066192
5 changed files with 421 additions and 39 deletions
|
|
@ -2632,7 +2632,11 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
// D.5.1: enrich a known inventory/equipped item (stubbed from PlayerDescription)
|
||||
// with the icon/name/type its CreateObject carries, so the toolbar can render it.
|
||||
Items.EnrichItem(spawn.Guid, spawn.IconId, spawn.Name ?? string.Empty, (AcDream.Core.Items.ItemType)(spawn.ItemType ?? 0));
|
||||
// D.5.1 (2026-06-17): also pass overlay/underlay ids from the extended
|
||||
// WeenieHeader tail so IconComposer composites all icon layers.
|
||||
Items.EnrichItem(spawn.Guid, spawn.IconId, spawn.Name ?? string.Empty,
|
||||
(AcDream.Core.Items.ItemType)(spawn.ItemType ?? 0),
|
||||
spawn.IconOverlayId, spawn.IconUnderlayId);
|
||||
|
||||
// Phase A.1 hotfix: live CreateObject handler reads dats extensively
|
||||
// (Setup, GfxObj, Surface, SurfaceTexture) to hydrate the spawned
|
||||
|
|
|
|||
|
|
@ -145,7 +145,18 @@ public static class CreateObject
|
|||
// a sizing hint for selection indicators on entities that
|
||||
// publish it.
|
||||
uint? Useability = null,
|
||||
float? UseRadius = null);
|
||||
float? UseRadius = null,
|
||||
// D.5.1 (2026-06-17): icon overlay/underlay dat ids from the
|
||||
// WeenieHeader optional tail. IconOverlayId is gated by
|
||||
// WeenieHeaderFlag.IconOverlay (0x40000000) in weenieFlags;
|
||||
// IconUnderlayId is gated by WeenieHeaderFlag2.IconUnderlay (0x01)
|
||||
// in weenieFlags2 (present when objDescFlags bit 0x04000000 is set).
|
||||
// Sourced from ACE WorldObject_Networking.cs:202-206. Zero when
|
||||
// the server did not send the field (most entities have neither).
|
||||
// IconComposer.GetIcon already composites these layers in the correct
|
||||
// retail order (underlay / base / overlay+tint / effect).
|
||||
uint IconOverlayId = 0,
|
||||
uint IconUnderlayId = 0);
|
||||
|
||||
/// <summary>
|
||||
/// The relevant subset of the server-sent <c>MovementData</c> /
|
||||
|
|
@ -539,31 +550,62 @@ public static class CreateObject
|
|||
catch { /* truncated name — partial result is still useful */ }
|
||||
}
|
||||
|
||||
// --- WeenieHeader optional tail (2026-05-15): walk the
|
||||
// conditional fields up through Useability + UseRadius.
|
||||
// --- WeenieHeader optional tail: walk every conditional field
|
||||
// in EXACT ACE write order (WorldObject_Networking.cs:87-219)
|
||||
// so the cursor reaches IconOverlay + IconUnderlay.
|
||||
//
|
||||
// Wire order is fixed by ACE WorldObject_Networking.cs:87-114
|
||||
// and matches retail PWD::Pack order. We MUST skip every
|
||||
// preceding optional field (even those we don't care about)
|
||||
// because each one moves the parse cursor.
|
||||
// We MUST skip every field that precedes IconOverlay even when
|
||||
// we don't need its value — each one occupies bytes on the wire
|
||||
// and a cursor error here would desync ALL downstream optional
|
||||
// reads for the rest of this entity's packet.
|
||||
//
|
||||
// Field bit width decoded?
|
||||
// ------- ------ -------- --------
|
||||
// weenieFlags2 conditional on objDescFlags & 0x80000000 (BF_INCLUDES_SECOND_HEADER)
|
||||
// u32 skipped
|
||||
// PluralName 0x1 String16L (variable, padded to 4) skipped
|
||||
// ItemCapacity 0x2 1 byte skipped
|
||||
// ContainerCap 0x4 1 byte skipped
|
||||
// AmmoType 0x100 u16 skipped
|
||||
// Value 0x8 u32 skipped
|
||||
// Useability 0x10 u32 KEPT
|
||||
// UseRadius 0x20 f32 KEPT
|
||||
// Wire order (verified against ACE WorldObject_Networking.cs):
|
||||
// bit field width
|
||||
// --------- ------------------ -----
|
||||
// 0x04000000 (objDescFlags) weenieFlags2 u32 (skip)
|
||||
// 0x00000001 PluralName String16L (skip)
|
||||
// 0x00000002 ItemsCapacity u8 (skip)
|
||||
// 0x00000004 ContainersCapacity u8 (skip)
|
||||
// 0x00000100 AmmoType u16 (skip)
|
||||
// 0x00000008 Value u32 (skip)
|
||||
// 0x00000010 Usable u32 KEPT
|
||||
// 0x00000020 UseRadius f32 KEPT
|
||||
// 0x00080000 TargetType u32 (skip)
|
||||
// 0x00000080 UiEffects u32 (skip)
|
||||
// 0x00000200 CombatUse sbyte/1 byte (skip)
|
||||
// 0x00000400 Structure u16 (skip)
|
||||
// 0x00000800 MaxStructure u16 (skip)
|
||||
// 0x00001000 StackSize u16 (skip)
|
||||
// 0x00002000 MaxStackSize u16 (skip)
|
||||
// 0x00004000 Container u32 (skip)
|
||||
// 0x00008000 Wielder u32 (skip)
|
||||
// 0x00010000 ValidLocations u32 (skip)
|
||||
// 0x00020000 CurrentlyWieldedLocation u32 (skip)
|
||||
// 0x00040000 Priority u32 (skip)
|
||||
// 0x00100000 RadarBlipColor u8 (skip)
|
||||
// 0x00800000 RadarBehavior u8 (skip)
|
||||
// 0x08000000 PScript u16 (skip)
|
||||
// 0x01000000 Workmanship f32 (skip)
|
||||
// 0x00200000 Burden u16 (skip)
|
||||
// 0x00400000 Spell u16 (skip)
|
||||
// 0x02000000 HouseOwner u32 (skip)
|
||||
// 0x04000000 HouseRestrictions RestrictionDB (skip, variable-length)
|
||||
// 0x20000000 HookItemTypes u32 (skip)
|
||||
// 0x00000040 Monarch u32 (skip)
|
||||
// 0x10000000 HookType u16 (skip)
|
||||
// 0x40000000 IconOverlay PackedDwordKnownType(0x06000000) CAPTURE
|
||||
// weenieFlags2 bit 0x01:
|
||||
// IconUnderlay PackedDwordKnownType(0x06000000) CAPTURE
|
||||
//
|
||||
// Wrapped in try/catch — if a malformed entity truncates the
|
||||
// tail we still return the prefix fields. Most spawned entities
|
||||
// either have all of these or none of them.
|
||||
// The entire walk is inside try/catch. A truncated packet degrades
|
||||
// gracefully: whatever was parsed before the throw is kept, and
|
||||
// IconOverlayId/IconUnderlayId stay 0 (no overlay drawn). This is
|
||||
// SAFE because IconComposer early-returns on id==0 per layer.
|
||||
uint? useability = null;
|
||||
float? useRadius = null;
|
||||
uint iconOverlayId = 0;
|
||||
uint iconUnderlayId = 0;
|
||||
uint weenieFlags2 = 0;
|
||||
try
|
||||
{
|
||||
// BF_INCLUDES_SECOND_HEADER = 0x04000000 per acclient.h:6458
|
||||
|
|
@ -571,20 +613,25 @@ public static class CreateObject
|
|||
// Earlier code had this as 0x80000000 — wrong bit, so the
|
||||
// weenieFlags2 4-byte skip never fired for entities that
|
||||
// actually had it set, corrupting downstream optional-tail
|
||||
// offsets. Now correct.
|
||||
// offsets. Now correct. We CAPTURE weenieFlags2 now (instead
|
||||
// of skipping) so we can gate IconUnderlay from bit 0x01.
|
||||
bool hasSecondHeader = objectDescriptionFlags.HasValue
|
||||
&& (objectDescriptionFlags.Value & 0x04000000u) != 0;
|
||||
if (hasSecondHeader && body.Length - pos >= 4) pos += 4; // weenieFlags2
|
||||
if (hasSecondHeader)
|
||||
{
|
||||
if (body.Length - pos < 4) throw new FormatException("trunc weenieFlags2");
|
||||
weenieFlags2 = ReadU32(body, ref pos);
|
||||
}
|
||||
|
||||
if ((weenieFlags & 0x00000001u) != 0) // PluralName
|
||||
_ = ReadString16L(body, ref pos);
|
||||
|
||||
if ((weenieFlags & 0x00000002u) != 0) // ItemCapacity
|
||||
if ((weenieFlags & 0x00000002u) != 0) // ItemsCapacity u8
|
||||
{
|
||||
if (body.Length - pos < 1) throw new FormatException("trunc ItemCap");
|
||||
pos += 1;
|
||||
}
|
||||
if ((weenieFlags & 0x00000004u) != 0) // ContainerCapacity
|
||||
if ((weenieFlags & 0x00000004u) != 0) // ContainersCapacity u8
|
||||
{
|
||||
if (body.Length - pos < 1) throw new FormatException("trunc ContCap");
|
||||
pos += 1;
|
||||
|
|
@ -599,7 +646,7 @@ public static class CreateObject
|
|||
if (body.Length - pos < 4) throw new FormatException("trunc Value");
|
||||
pos += 4;
|
||||
}
|
||||
if ((weenieFlags & 0x00000010u) != 0) // Useability u32 ← KEEP
|
||||
if ((weenieFlags & 0x00000010u) != 0) // Usable u32 ← KEEP
|
||||
{
|
||||
if (body.Length - pos < 4) throw new FormatException("trunc Useability");
|
||||
useability = ReadU32(body, ref pos);
|
||||
|
|
@ -610,6 +657,147 @@ public static class CreateObject
|
|||
useRadius = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
// ---- Extended walk: fields after UseRadius through IconOverlay ----
|
||||
// Source: ACE WorldObject_Networking.cs:108-206 (verified 2026-06-17).
|
||||
|
||||
if ((weenieFlags & 0x00080000u) != 0) // TargetType u32
|
||||
{
|
||||
if (body.Length - pos < 4) throw new FormatException("trunc TargetType");
|
||||
pos += 4;
|
||||
}
|
||||
if ((weenieFlags & 0x00000080u) != 0) // UiEffects u32
|
||||
{
|
||||
if (body.Length - pos < 4) throw new FormatException("trunc UiEffects");
|
||||
pos += 4;
|
||||
}
|
||||
if ((weenieFlags & 0x00000200u) != 0) // CombatUse sbyte (1 byte)
|
||||
{
|
||||
if (body.Length - pos < 1) throw new FormatException("trunc CombatUse");
|
||||
pos += 1;
|
||||
}
|
||||
if ((weenieFlags & 0x00000400u) != 0) // Structure u16
|
||||
{
|
||||
if (body.Length - pos < 2) throw new FormatException("trunc Structure");
|
||||
pos += 2;
|
||||
}
|
||||
if ((weenieFlags & 0x00000800u) != 0) // MaxStructure u16
|
||||
{
|
||||
if (body.Length - pos < 2) throw new FormatException("trunc MaxStructure");
|
||||
pos += 2;
|
||||
}
|
||||
if ((weenieFlags & 0x00001000u) != 0) // StackSize u16
|
||||
{
|
||||
if (body.Length - pos < 2) throw new FormatException("trunc StackSize");
|
||||
pos += 2;
|
||||
}
|
||||
if ((weenieFlags & 0x00002000u) != 0) // MaxStackSize u16
|
||||
{
|
||||
if (body.Length - pos < 2) throw new FormatException("trunc MaxStackSize");
|
||||
pos += 2;
|
||||
}
|
||||
if ((weenieFlags & 0x00004000u) != 0) // Container u32
|
||||
{
|
||||
if (body.Length - pos < 4) throw new FormatException("trunc Container");
|
||||
pos += 4;
|
||||
}
|
||||
if ((weenieFlags & 0x00008000u) != 0) // Wielder u32
|
||||
{
|
||||
if (body.Length - pos < 4) throw new FormatException("trunc Wielder");
|
||||
pos += 4;
|
||||
}
|
||||
if ((weenieFlags & 0x00010000u) != 0) // ValidLocations u32
|
||||
{
|
||||
if (body.Length - pos < 4) throw new FormatException("trunc ValidLocations");
|
||||
pos += 4;
|
||||
}
|
||||
if ((weenieFlags & 0x00020000u) != 0) // CurrentlyWieldedLocation u32
|
||||
{
|
||||
if (body.Length - pos < 4) throw new FormatException("trunc CurrentlyWieldedLocation");
|
||||
pos += 4;
|
||||
}
|
||||
if ((weenieFlags & 0x00040000u) != 0) // Priority u32
|
||||
{
|
||||
if (body.Length - pos < 4) throw new FormatException("trunc Priority");
|
||||
pos += 4;
|
||||
}
|
||||
if ((weenieFlags & 0x00100000u) != 0) // RadarBlipColor u8
|
||||
{
|
||||
if (body.Length - pos < 1) throw new FormatException("trunc RadarBlipColor");
|
||||
pos += 1;
|
||||
}
|
||||
if ((weenieFlags & 0x00800000u) != 0) // RadarBehavior u8
|
||||
{
|
||||
if (body.Length - pos < 1) throw new FormatException("trunc RadarBehavior");
|
||||
pos += 1;
|
||||
}
|
||||
if ((weenieFlags & 0x08000000u) != 0) // PScript u16
|
||||
{
|
||||
if (body.Length - pos < 2) throw new FormatException("trunc PScript");
|
||||
pos += 2;
|
||||
}
|
||||
if ((weenieFlags & 0x01000000u) != 0) // Workmanship f32
|
||||
{
|
||||
if (body.Length - pos < 4) throw new FormatException("trunc Workmanship");
|
||||
pos += 4;
|
||||
}
|
||||
if ((weenieFlags & 0x00200000u) != 0) // Burden u16
|
||||
{
|
||||
if (body.Length - pos < 2) throw new FormatException("trunc Burden");
|
||||
pos += 2;
|
||||
}
|
||||
if ((weenieFlags & 0x00400000u) != 0) // Spell u16
|
||||
{
|
||||
if (body.Length - pos < 2) throw new FormatException("trunc Spell");
|
||||
pos += 2;
|
||||
}
|
||||
if ((weenieFlags & 0x02000000u) != 0) // HouseOwner u32
|
||||
{
|
||||
if (body.Length - pos < 4) throw new FormatException("trunc HouseOwner");
|
||||
pos += 4;
|
||||
}
|
||||
if ((weenieFlags & 0x04000000u) != 0) // HouseRestrictions (RestrictionDB)
|
||||
{
|
||||
// Wire layout per ACE RestrictionDB + RestrictionDBExtensions.Write:
|
||||
// u32 Version, u32 OpenStatus, u32 MonarchId,
|
||||
// u16 count, u16 numBuckets, then count × (u32 guid + u32 value).
|
||||
// Fixed header = 12 bytes; PackableHashTable header = 4 bytes.
|
||||
// Total = 16 + count * 8.
|
||||
if (body.Length - pos < 16) throw new FormatException("trunc RestrictionDB header");
|
||||
// Version(4) + OpenStatus(4) + MonarchId(4) = 12 bytes
|
||||
pos += 12;
|
||||
ushort tableCount = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos));
|
||||
pos += 2; // count u16
|
||||
pos += 2; // numBuckets u16
|
||||
int entryBytes = tableCount * 8; // each entry: u32 guid + u32 value
|
||||
if (body.Length - pos < entryBytes) throw new FormatException("trunc RestrictionDB entries");
|
||||
pos += entryBytes;
|
||||
}
|
||||
if ((weenieFlags & 0x20000000u) != 0) // HookItemTypes u32
|
||||
{
|
||||
if (body.Length - pos < 4) throw new FormatException("trunc HookItemTypes");
|
||||
pos += 4;
|
||||
}
|
||||
if ((weenieFlags & 0x00000040u) != 0) // Monarch u32
|
||||
{
|
||||
if (body.Length - pos < 4) throw new FormatException("trunc Monarch");
|
||||
pos += 4;
|
||||
}
|
||||
if ((weenieFlags & 0x10000000u) != 0) // HookType u16
|
||||
{
|
||||
if (body.Length - pos < 2) throw new FormatException("trunc HookType");
|
||||
pos += 2;
|
||||
}
|
||||
if ((weenieFlags & 0x40000000u) != 0) // IconOverlay PackedDwordOfKnownType(0x06000000) ← CAPTURE
|
||||
{
|
||||
iconOverlayId = ReadPackedDwordOfKnownType(body, ref pos, IconTypePrefix);
|
||||
}
|
||||
// IconUnderlay is gated by weenieFlags2 bit 0x01, not weenieFlags.
|
||||
// weenieFlags2 is only present when hasSecondHeader (captured above).
|
||||
if ((weenieFlags2 & 0x00000001u) != 0) // IconUnderlay PackedDwordOfKnownType(0x06000000) ← CAPTURE
|
||||
{
|
||||
iconUnderlayId = ReadPackedDwordOfKnownType(body, ref pos, IconTypePrefix);
|
||||
}
|
||||
}
|
||||
catch { /* truncated weenie tail — keep whatever we got. */ }
|
||||
|
||||
|
|
@ -619,7 +807,8 @@ public static class CreateObject
|
|||
physicsState, objectDescriptionFlags,
|
||||
friction, elasticity,
|
||||
IconId: iconId,
|
||||
Useability: useability, UseRadius: useRadius);
|
||||
Useability: useability, UseRadius: useRadius,
|
||||
IconOverlayId: iconOverlayId, IconUnderlayId: iconUnderlayId);
|
||||
|
||||
// Local helper: if we ran out of fields past PhysicsData, still
|
||||
// return the useful prefix (guid/position/setup/animParts/textures/palettes/scale/motion).
|
||||
|
|
|
|||
|
|
@ -82,7 +82,13 @@ public sealed class WorldSession : IDisposable
|
|||
uint? Useability = null,
|
||||
float? UseRadius = null,
|
||||
// D.5.1: icon datId from CreateObject WeenieHeader, for toolbar rendering.
|
||||
uint IconId = 0);
|
||||
uint IconId = 0,
|
||||
// D.5.1 (2026-06-17): icon overlay/underlay dat ids from the extended
|
||||
// WeenieHeader optional tail. Gated by WeenieHeaderFlag.IconOverlay
|
||||
// (0x40000000) and WeenieHeaderFlag2.IconUnderlay (0x01) respectively.
|
||||
// Zero when the server did not send the field (common for most entities).
|
||||
uint IconOverlayId = 0,
|
||||
uint IconUnderlayId = 0);
|
||||
|
||||
/// <summary>Fires when the session finishes parsing a CreateObject.</summary>
|
||||
public event Action<EntitySpawn>? EntitySpawned;
|
||||
|
|
@ -719,7 +725,9 @@ public sealed class WorldSession : IDisposable
|
|||
parsed.Value.Elasticity,
|
||||
parsed.Value.Useability,
|
||||
parsed.Value.UseRadius,
|
||||
parsed.Value.IconId));
|
||||
parsed.Value.IconId,
|
||||
parsed.Value.IconOverlayId,
|
||||
parsed.Value.IconUnderlayId));
|
||||
}
|
||||
}
|
||||
else if (op == DeleteObject.Opcode)
|
||||
|
|
|
|||
|
|
@ -144,13 +144,22 @@ public sealed class ItemRepository
|
|||
/// Raises ItemPropertiesUpdated whenever the item is found (matching the
|
||||
/// UpdateProperties convention — it fires on found regardless of whether a field
|
||||
/// actually changed) so bound widgets (the toolbar) re-render.
|
||||
/// <para>
|
||||
/// D.5.1 (2026-06-17): also accepts <paramref name="iconOverlayId"/> and
|
||||
/// <paramref name="iconUnderlayId"/> from the extended WeenieHeader tail. Both
|
||||
/// default to 0 (not sent by server). IconComposer.GetIcon already composites
|
||||
/// underlay/base/overlay in the correct retail layer order and early-returns on 0.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public bool EnrichItem(uint objectId, uint iconId, string name, ItemType type)
|
||||
public bool EnrichItem(uint objectId, uint iconId, string name, ItemType type,
|
||||
uint iconOverlayId = 0, uint iconUnderlayId = 0)
|
||||
{
|
||||
if (!_items.TryGetValue(objectId, out var item)) return false;
|
||||
if (iconId != 0) item.IconId = iconId;
|
||||
if (!string.IsNullOrEmpty(name)) item.Name = name;
|
||||
if (type != default) item.Type = type;
|
||||
if (iconOverlayId != 0) item.IconOverlayId = iconOverlayId;
|
||||
if (iconUnderlayId != 0) item.IconUnderlayId = iconUnderlayId;
|
||||
ItemPropertiesUpdated?.Invoke(item);
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue