feat(D.5.4): PlayerDescription = membership manifest; drop WeenieClassId=ContainerType misuse

The old seeding block set WeenieClassId = inv.ContainerType (a 0/1/2
container-kind discriminator, not a weenie class id) and used MoveItem
for the equipped block. Replace both loops with RecordMembership calls:
inventory guids get a bare stub (WeenieClassId stays 0); equipped guids
get the equip slot set directly. Weenie data arrives via CreateObject /
ObjectTableWiring, not PlayerDescription.

New test PlayerDescription_SeedsMembership_NotWeenieClassIdMisuse proves:
(a) inv guid is registered, (b) WeenieClassId==0 not ContainerType, and
(c) equipped guid CurrentlyEquippedLocation is set to MeleeWeapon.
No existing tests pinned the old behavior; all 15 GameEventWiringTests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-18 16:38:54 +02:00
parent 82f5968316
commit cbbfe4cd49
2 changed files with 59 additions and 32 deletions

View file

@ -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()
{