feat(D.2b): ClientObjectTable optimistic move + confirm/rollback for inventory drag
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b657c82df5
commit
f7f9ae052e
2 changed files with 70 additions and 0 deletions
|
|
@ -44,6 +44,11 @@ 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
|
||||
// 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)> _pendingMoves = new();
|
||||
|
||||
/// <summary>Fires when an object is first added to the session.</summary>
|
||||
public event Action<ClientObject>? ObjectAdded;
|
||||
|
||||
|
|
@ -130,6 +135,33 @@ public sealed class ClientObjectTable
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <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
|
||||
/// PutItemInContainer is sent by the caller; <see cref="ConfirmMove"/>/<see cref="RollbackMove"/>
|
||||
/// reconcile against the server. Retail: local ItemList_InsertItem + ServerSaysMoveItem reconcile.
|
||||
/// </summary>
|
||||
public bool MoveItemOptimistic(uint itemId, uint newContainerId, int newSlot)
|
||||
{
|
||||
if (!_objects.TryGetValue(itemId, out var item)) return false;
|
||||
if (!_pendingMoves.ContainsKey(itemId))
|
||||
_pendingMoves[itemId] = (item.ContainerId, item.ContainerSlot); // snapshot the ORIGINAL once
|
||||
return MoveItem(itemId, newContainerId, newSlot);
|
||||
}
|
||||
|
||||
/// <summary>The server confirmed the move (InventoryPutObjInContainer 0x0022 echo) — drop the
|
||||
/// pending snapshot. No-op for a server-initiated move we never tracked.</summary>
|
||||
public void ConfirmMove(uint itemId) => _pendingMoves.Remove(itemId);
|
||||
|
||||
/// <summary>The server rejected the move (InventoryServerSaveFailed 0x00A0) — restore the item to
|
||||
/// its pre-move (container, slot) and drop the snapshot. False if nothing was pending.</summary>
|
||||
public bool RollbackMove(uint itemId)
|
||||
{
|
||||
if (!_pendingMoves.TryGetValue(itemId, out var pre)) return false;
|
||||
_pendingMoves.Remove(itemId);
|
||||
return MoveItem(itemId, pre.container, pre.slot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle a server-driven remove (destroyed item, dropped into 3D
|
||||
/// space, stolen, etc).
|
||||
|
|
|
|||
|
|
@ -358,6 +358,44 @@ public sealed class ClientObjectTableTests
|
|||
Assert.Null(table.Get(0xA01u));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveItemOptimistic_movesAndRemembersPreMove()
|
||||
{
|
||||
var table = new ClientObjectTable();
|
||||
table.Ingest(FullWeenie(0x700u, container: 0xC0u)); table.MoveItem(0x700u, 0xC0u, newSlot: 3);
|
||||
table.MoveItemOptimistic(0x700u, 0xC1u, 0);
|
||||
Assert.Equal(0xC1u, table.Get(0x700u)!.ContainerId); // moved now (instant)
|
||||
Assert.True(table.RollbackMove(0x700u)); // pre-move was remembered
|
||||
Assert.Equal(0xC0u, table.Get(0x700u)!.ContainerId); // snapped back to the original container
|
||||
Assert.Equal(3, table.Get(0x700u)!.ContainerSlot); // and slot
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfirmMove_clearsPending_soRollbackIsNoOp()
|
||||
{
|
||||
var table = new ClientObjectTable();
|
||||
table.Ingest(FullWeenie(0x701u, container: 0xC0u));
|
||||
table.MoveItemOptimistic(0x701u, 0xC1u, 0);
|
||||
table.ConfirmMove(0x701u);
|
||||
Assert.False(table.RollbackMove(0x701u)); // nothing pending → no rollback
|
||||
Assert.Equal(0xC1u, table.Get(0x701u)!.ContainerId); // stays at the confirmed container
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveItemOptimistic_twice_keepsTheOriginalPreMove()
|
||||
{
|
||||
var table = new ClientObjectTable();
|
||||
table.Ingest(FullWeenie(0x702u, container: 0xC0u));
|
||||
table.MoveItemOptimistic(0x702u, 0xC1u, 0);
|
||||
table.MoveItemOptimistic(0x702u, 0xC2u, 0); // a second move before any confirm
|
||||
table.RollbackMove(0x702u);
|
||||
Assert.Equal(0xC0u, table.Get(0x702u)!.ContainerId); // rolls back to the FIRST pre-move
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RollbackMove_unknownItem_false()
|
||||
=> Assert.False(new ClientObjectTable().RollbackMove(0xDEADu));
|
||||
|
||||
[Fact]
|
||||
public void ContainerIndex_SlotChange_ResortsInPlace() // guards the Reindex same-container early-out
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue