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

@ -44,10 +44,10 @@ public sealed class ClientObjectTable
private readonly ConcurrentDictionary<uint, Container> _containers = new();
private readonly Dictionary<uint, List<uint>> _containerIndex = new();
// B-Drag: pre-move snapshots for optimistic inventory moves. itemId → (container, slot) BEFORE
// B-Drag: pre-move snapshots for optimistic inventory moves. itemId → (container, slot, equip) BEFORE
// the optimistic MoveItem; restored by RollbackMove on InventoryServerSaveFailed (0x00A0),
// cleared by ConfirmMove on the InventoryPutObjInContainer (0x0022) echo.
private readonly Dictionary<uint, (uint container, int slot, int outstanding)> _pendingMoves = new();
private readonly Dictionary<uint, (uint container, int slot, EquipMask equip, int outstanding)> _pendingMoves = new();
/// <summary>Fires when an object is first added to the session.</summary>
public event Action<ClientObject>? ObjectAdded;
@ -135,6 +135,19 @@ public sealed class ClientObjectTable
return true;
}
/// <summary>Snapshot the item's pre-move (container, slot, equip) the FIRST time it moves, and
/// bump the OUTSTANDING count on subsequent in-flight moves of the same item (so an early confirm
/// can't clear a still-pending later move — the I1 hardening). Shared by MoveItemOptimistic (unwield/
/// move) and WieldItemOptimistic (wield).</summary>
private void RecordPending(uint itemId, ClientObject item)
{
if (_pendingMoves.TryGetValue(itemId, out var p))
_pendingMoves[itemId] = (p.container, p.slot, p.equip, p.outstanding + 1);
else
_pendingMoves[itemId] = (item.ContainerId, item.ContainerSlot,
item.CurrentlyEquippedLocation, 1);
}
/// <summary>
/// Optimistic (instant) move: snapshot the item's current (ContainerId, ContainerSlot) the FIRST
/// time it moves, then <see cref="MoveItem"/> (immediate repaint via ObjectMoved). The wire
@ -148,10 +161,7 @@ public sealed class ClientObjectTable
// count stops an early confirm (0x0022) for the first of several in-flight moves of the SAME
// item from clearing the snapshot while a later move is still unconfirmed — else a later
// reject would find nothing pending and strand the item at the rejected position.
if (_pendingMoves.TryGetValue(itemId, out var p))
_pendingMoves[itemId] = (p.container, p.slot, p.outstanding + 1);
else
_pendingMoves[itemId] = (item.ContainerId, item.ContainerSlot, 1);
RecordPending(itemId, item);
// Gapless INSERT — treat newSlot as the insert INDEX, shift the other items, and renumber both
// containers' slots 0..N-1. A raw MoveItem sets ONLY this item's slot, which then collides with
@ -180,6 +190,18 @@ public sealed class ClientObjectTable
return true;
}
/// <summary>Optimistic (instant) wield: snapshot the pre-wield (container, slot, equip), then set
/// ContainerId = WielderId = wielderGuid and CurrentlyEquippedLocation = equipMask (matching the
/// server's WieldObject 0x0023 confirm), firing ObjectMoved for an immediate repaint. The caller
/// sends GetAndWieldItem; ConfirmMove (on the 0x0023 echo) / RollbackMove (on 0x00A0) reconcile.</summary>
public bool WieldItemOptimistic(uint itemId, uint wielderGuid, EquipMask equipMask)
{
if (!_objects.TryGetValue(itemId, out var item)) return false;
RecordPending(itemId, item);
item.WielderId = wielderGuid; // unambiguously the player's gear during the optimistic window
return MoveItem(itemId, wielderGuid, newSlot: -1, newEquipLocation: equipMask);
}
/// <summary>Assign each item in <paramref name="list"/> a sequential ContainerSlot (0..N-1) matching
/// its list position — keeps a container gapless + collision-free so the grid order is stable.</summary>
private void RenumberContainer(List<uint> list)
@ -195,7 +217,7 @@ public sealed class ClientObjectTable
{
if (!_pendingMoves.TryGetValue(itemId, out var p)) return;
if (p.outstanding <= 1) _pendingMoves.Remove(itemId);
else _pendingMoves[itemId] = (p.container, p.slot, p.outstanding - 1);
else _pendingMoves[itemId] = (p.container, p.slot, p.equip, p.outstanding - 1);
}
/// <summary>The server rejected a move (InventoryServerSaveFailed 0x00A0) — restore the item to its
@ -205,7 +227,7 @@ public sealed class ClientObjectTable
{
if (!_pendingMoves.TryGetValue(itemId, out var pre)) return false;
_pendingMoves.Remove(itemId);
return MoveItem(itemId, pre.container, pre.slot);
return MoveItem(itemId, pre.container, pre.slot, pre.equip);
}
/// <summary>