fix(inventory): complete switches on wield response

Clear retail AutoWield on the matching authoritative WieldObject instead of waiting for unrelated rollback bookkeeping to drain. This prevents a completed switch from consuming the next weapon activation locally, including crossbows.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-13 09:00:54 +02:00
parent ea5cf5b842
commit 3e84027885
9 changed files with 47 additions and 21 deletions

View file

@ -84,7 +84,7 @@ internal sealed class AutoWieldController : IDisposable
_objects.ObjectMoved += OnObjectMoved;
_objects.ObjectRemoved += OnObjectRemoved;
_objects.MoveRequestFailed += OnMoveRequestFailed;
_objects.MoveRequestConfirmed += OnMoveRequestConfirmed;
_objects.WieldConfirmed += OnWieldConfirmed;
_objects.Cleared += OnObjectsCleared;
}
@ -191,7 +191,7 @@ internal sealed class AutoWieldController : IDisposable
TryWield(requested);
}
private void OnMoveRequestConfirmed(ClientObject item)
private void OnWieldConfirmed(ClientObject item)
{
if (_pendingSwitch is { BlockingItemId: 0 } pending
&& item.ObjectId == pending.RequestedItemId)
@ -319,7 +319,7 @@ internal sealed class AutoWieldController : IDisposable
_objects.ObjectMoved -= OnObjectMoved;
_objects.ObjectRemoved -= OnObjectRemoved;
_objects.MoveRequestFailed -= OnMoveRequestFailed;
_objects.MoveRequestConfirmed -= OnMoveRequestConfirmed;
_objects.WieldConfirmed -= OnWieldConfirmed;
_objects.Cleared -= OnObjectsCleared;
}

View file

@ -258,7 +258,7 @@ public static class GameEventWiring
p.Value.ItemGuid,
newContainerId: wielderGuid,
newEquipLocation: (AcDream.Core.Items.EquipMask)p.Value.EquipLoc);
items.ConfirmMove(p.Value.ItemGuid); // Slice 1: confirm an optimistic wield (mirrors the 0x0022 handler)
items.ConfirmWield(p.Value.ItemGuid); // Exact IR_WIELD completion + optimistic reconciliation.
});
dispatcher.Register(GameEventType.InventoryPutObjInContainer, e =>
{

View file

@ -86,11 +86,12 @@ public sealed class ClientObjectTable
public event Action<ClientObject>? MoveRolledBack;
/// <summary>
/// Fires after the server has confirmed the final outstanding request for
/// an optimistically moved item. This is later than <see cref="ObjectMoved"/>,
/// which also fires for the initial local projection.
/// Fires for an authoritative WieldObject response after its object state
/// has been applied. This is deliberately independent of optimistic-move
/// rollback bookkeeping: retail clears IR_WIELD on the matching server
/// response even when separate reconciliation state exists for the item.
/// </summary>
public event Action<ClientObject>? MoveRequestConfirmed;
public event Action<ClientObject>? WieldConfirmed;
/// <summary>
/// Fires for every InventoryServerSaveFailed response, including requests
@ -268,8 +269,6 @@ public sealed class ClientObjectTable
if (p.outstanding <= 1)
{
_pendingMoves.Remove(itemId);
if (_objects.TryGetValue(itemId, out ClientObject? item))
MoveRequestConfirmed?.Invoke(item);
}
else
{
@ -277,6 +276,20 @@ public sealed class ClientObjectTable
}
}
/// <summary>
/// Reconcile an authoritative WieldObject response and publish the exact
/// retail inventory-request completion boundary. The notification is not
/// conditional on the optimistic rollback counter reaching zero.
/// Retail: ACCWeenieObject::ServerSaysMoveItem @ 0x0058DBB0 clears the
/// matching prevRequestObjectID directly.
/// </summary>
public void ConfirmWield(uint itemId)
{
ConfirmMove(itemId);
if (_objects.TryGetValue(itemId, out ClientObject? item))
WieldConfirmed?.Invoke(item);
}
/// <summary>The server rejected a move (InventoryServerSaveFailed 0x00A0) — restore the item to its
/// pre-move (container, slot) and drop the snapshot entirely (the server's next snapshot reconciles
/// any still-outstanding moves). False if nothing was pending.</summary>