fix(D.2b): wield is ContainerId-based — drop the WielderId write (code review)

Code-quality review (needs-changes) on Task 3: WieldItemOptimistic wrote
item.WielderId directly, but RollbackMove (via MoveItem) never cleared it
→ a wield-rollback left a pack item with a stale WielderId=player.

Root-cause fix (vs the reviewer's snapshot-WielderId suggestion): acdream's
existing WieldObject 0x0023 confirm models a wielded item as ContainerId=
wielder + equip=mask and does NOT touch WielderId. So the optimistic path
must match — drop the WielderId write entirely. Optimistic state now equals
the confirmed state, rollback fully restores through MoveItem alone, and the
stale-state class is structurally eliminated. The paperdoll's
(WielderId==p || ContainerId==p) filter still matches optimistic wields via
ContainerId (login-equipped items match via WielderId from their CreateObject).

Also: + the wield+move outstanding-count combo test (spec §8), MoveItem doc
note that it doesn't manage WielderId, and the test pins WielderId==0 post-
optimistic-wield to document the model. Core 74/74 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-22 22:15:18 +02:00
parent 0c353185c3
commit c1a84cbe0c
3 changed files with 31 additions and 10 deletions

View file

@ -455,17 +455,17 @@ public sealed class ClientObjectTableTests
}
[Fact]
public void WieldItemOptimistic_equipsInstantly_andSetsWielderAndContainer()
public void WieldItemOptimistic_equipsInstantly_setsContainerAndEquip()
{
var table = new ClientObjectTable();
const uint player = 0x50000001u, pack = 0x40000005u;
table.AddOrUpdate(new ClientObject { ObjectId = 0x940u });
table.MoveItem(0x940u, pack, newSlot: 2); // a pack item
table.MoveItem(0x940u, pack, newSlot: 2); // a pack item (WielderId stays 0)
table.WieldItemOptimistic(0x940u, player, EquipMask.HeadWear);
var o = table.Get(0x940u)!;
Assert.Equal(EquipMask.HeadWear, o.CurrentlyEquippedLocation); // equipped now (instant)
Assert.Equal(player, o.WielderId);
Assert.Equal(player, o.ContainerId);
Assert.Equal(player, o.ContainerId); // contained-by-wielder (matches the WieldObject 0x0023 confirm)
Assert.Equal(0u, o.WielderId); // optimistic wield does NOT write WielderId (ContainerId-based model)
}
[Fact]
@ -499,4 +499,21 @@ public sealed class ClientObjectTableTests
[Fact]
public void WieldItemOptimistic_unknownItem_false()
=> Assert.False(new ClientObjectTable().WieldItemOptimistic(0xDEADu, 0x1u, EquipMask.HeadWear));
[Fact]
public void WieldThenMove_oneConfirm_rollsBackToPreWield() // outstanding-count across wield+move (shared RecordPending)
{
var table = new ClientObjectTable();
const uint player = 0x50000001u, pack = 0x40000005u, pack2 = 0x40000006u;
table.AddOrUpdate(new ClientObject { ObjectId = 0x950u });
table.MoveItem(0x950u, pack, newSlot: 2); // pre-wield: pack slot 2
table.WieldItemOptimistic(0x950u, player, EquipMask.HeadWear); // outstanding 1, snapshot (pack, 2, None)
table.MoveItemOptimistic(0x950u, pack2, 0); // outstanding 2 (same snapshot)
table.ConfirmMove(0x950u); // confirm the first → outstanding 1, still pending
Assert.True(table.RollbackMove(0x950u)); // the second rejected → roll back
var o = table.Get(0x950u)!;
Assert.Equal(pack, o.ContainerId); // to the ORIGINAL pre-wield container
Assert.Equal(2, o.ContainerSlot); // and slot
Assert.Equal(EquipMask.None, o.CurrentlyEquippedLocation); // pre-wield was un-equipped
}
}