diff --git a/src/AcDream.Core.Net/GameEventWiring.cs b/src/AcDream.Core.Net/GameEventWiring.cs index 83030988..dba723c6 100644 --- a/src/AcDream.Core.Net/GameEventWiring.cs +++ b/src/AcDream.Core.Net/GameEventWiring.cs @@ -400,40 +400,15 @@ public static class GameEventWiring Console.WriteLine($"vitals: PD-ench spell={ench.SpellId} layer={ench.Layer} bucket={ench.Bucket} key={ench.StatModKey} val={ench.StatModValue}"); } - // Issue #13 — register inventory entries with ClientObjectTable so - // panels (inventory, paperdoll, hotbars) light up after login. - // Equipped entries share the same ObjectId as inventory entries - // (an equipped item is also in inventory) — register both, but - // the equipped record carries the slot mask which we surface via - // MoveItem so paperdoll can render. + // D.5.4: PlayerDescription is a membership MANIFEST, not the data + // source. Record existence (+ equip slot); CreateObject fills the + // actual weenie data via ObjectTableWiring. (Previously this seeded + // stubs with WeenieClassId = ContainerType, a misuse — ContainerType + // is a 0/1/2 container-kind discriminator, not a weenie class id.) foreach (var inv in p.Value.Inventory) - { - if (items.Get(inv.Guid) is null) - { - items.AddOrUpdate(new ClientObject - { - ObjectId = inv.Guid, - WeenieClassId = inv.ContainerType, - }); - } - } + items.RecordMembership(inv.Guid); foreach (var eq in p.Value.Equipped) - { - if (items.Get(eq.Guid) is null) - { - items.AddOrUpdate(new ClientObject - { - ObjectId = eq.Guid, - WeenieClassId = 0, - }); - } - // Reflect the equip slot — paperdoll uses CurrentlyEquippedLocation. - items.MoveItem( - itemId: eq.Guid, - newContainerId: 0, - newSlot: -1, - newEquipLocation: (EquipMask)eq.EquipLocation); - } + items.RecordMembership(eq.Guid, equip: (EquipMask)eq.EquipLocation); // D.5.1 Task 4: forward shortcut bar entries to the caller so the // toolbar can read them without holding a parser reference. diff --git a/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs b/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs index 45008de9..d0a92463 100644 --- a/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs +++ b/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs @@ -375,6 +375,58 @@ public sealed class GameEventWiringTests Assert.NotNull(items.Get(0x50000A02u)); } + [Fact] + public void PlayerDescription_SeedsMembership_NotWeenieClassIdMisuse() + { + // D.5.4: PlayerDescription is a membership MANIFEST, not the data + // source. The old code set WeenieClassId = inv.ContainerType (a + // 0/1/2 discriminator), which is a misuse. After the fix, the + // registered stub has WeenieClassId == 0 and the equipped item's + // CurrentlyEquippedLocation is set to MeleeWeapon (0x1). + // Uses the SAME wire fixture as PlayerDescription_RegistersInventoryEntries_InClientObjectTable. + var dispatcher = new GameEventDispatcher(); + var items = new ClientObjectTable(); + var combat = new CombatState(); + var spellbook = new Spellbook(); + var chat = new ChatLog(); + GameEventWiring.WireAll(dispatcher, items, combat, spellbook, chat); + + var sb = new MemoryStream(); + using var w = new BinaryWriter(sb); + w.Write(0u); // propertyFlags = 0 + w.Write(0x52u); // weenieType + w.Write(0x201u); // vectorFlags = ATTRIBUTE | ENCHANTMENT + w.Write(1u); // has_health + w.Write(0u); // attribute_flags = 0 (no attrs) + w.Write(0u); // enchantment_mask = 0 + + w.Write(0u); // option_flags = None (no GAMEPLAY_OPTIONS → strict inv path) + w.Write(0u); // options1 + w.Write(0u); // legacy hotbar list count = 0 + w.Write(0u); // spellbook_filters + + // Inventory: 1 entry with ContainerType=1 (the OLD code would have + // set WeenieClassId=1; the new code must leave WeenieClassId==0). + w.Write(1u); + w.Write(0x700u); w.Write(1u); // guid=0x700, ContainerType=1 + + // Equipped: 1 entry with EquipLocation = MeleeWeapon (0x1). + // Wire format: guid(4) + loc(4) + priority(4) = 12 bytes per entry. + w.Write(1u); + w.Write(0x701u); w.Write((uint)EquipMask.MeleeWeapon); w.Write(0u); // guid=0x701, slot=MeleeWeapon, prio=0 + + var env = GameEventEnvelope.TryParse(WrapEnvelope(GameEventType.PlayerDescription, sb.ToArray())); + dispatcher.Dispatch(env!.Value); + + // (a) inventory guid is registered + Assert.NotNull(items.Get(0x700u)); + // (b) WeenieClassId must be 0, NOT the ContainerType discriminator (1) — misuse gone + Assert.Equal(0u, items.Get(0x700u)!.WeenieClassId); + // (c) equipped guid has its equip slot set + Assert.NotNull(items.Get(0x701u)); + Assert.Equal(EquipMask.MeleeWeapon, items.Get(0x701u)!.CurrentlyEquippedLocation); + } + [Fact] public void WireAll_PlayerDescription_invokesOnShortcuts() {