feat(D.2b): WieldItemOptimistic + equip-aware rollback snapshot (Core)

Add WieldItemOptimistic (instant optimistic wield — sets ContainerId=WielderId=player,
CurrentlyEquippedLocation=equipMask, snapshots pre-wield position) and extend the
_pendingMoves tuple to carry the pre-move EquipMask so RollbackMove restores EQUIPPED
state faithfully on server rejection. Shared RecordPending helper replaces the inline
snapshot block in MoveItemOptimistic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-22 22:03:47 +02:00
parent 6d65177357
commit 0c353185c3
2 changed files with 76 additions and 8 deletions

View file

@ -453,4 +453,50 @@ public sealed class ClientObjectTableTests
Assert.Equal(new[] { 0x541u, 0x540u }, table.GetContents(0xC4u));
Assert.Equal(2, table.GetContents(0xC4u).Count); // no duplicate from the same-container move
}
[Fact]
public void WieldItemOptimistic_equipsInstantly_andSetsWielderAndContainer()
{
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.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);
}
[Fact]
public void WieldItemOptimistic_rollback_unequips_andReturnsToPack()
{
var table = new ClientObjectTable();
const uint player = 0x50000001u, pack = 0x40000005u;
table.AddOrUpdate(new ClientObject { ObjectId = 0x941u });
table.MoveItem(0x941u, pack, newSlot: 2);
table.WieldItemOptimistic(0x941u, player, EquipMask.HeadWear);
Assert.True(table.RollbackMove(0x941u)); // server rejected the wield
var o = table.Get(0x941u)!;
Assert.Equal(EquipMask.None, o.CurrentlyEquippedLocation); // un-equipped
Assert.Equal(pack, o.ContainerId); // back in the pack
Assert.Equal(2, o.ContainerSlot); // at its original slot
}
[Fact]
public void RollbackMove_restoresPreMoveEquipLocation() // unwield-reject must snap back to EQUIPPED
{
var table = new ClientObjectTable();
const uint player = 0x50000001u;
table.AddOrUpdate(new ClientObject { ObjectId = 0x942u });
table.MoveItem(0x942u, player, newSlot: -1, newEquipLocation: EquipMask.Shield); // equipped
table.MoveItemOptimistic(0x942u, player, 0); // optimistic UNWIELD into the pack (clears equip)
Assert.Equal(EquipMask.None, table.Get(0x942u)!.CurrentlyEquippedLocation);
Assert.True(table.RollbackMove(0x942u)); // server rejected the unwield
Assert.Equal(EquipMask.Shield, table.Get(0x942u)!.CurrentlyEquippedLocation); // restored to equipped
}
[Fact]
public void WieldItemOptimistic_unknownItem_false()
=> Assert.False(new ClientObjectTable().WieldItemOptimistic(0xDEADu, 0x1u, EquipMask.HeadWear));
}