fix(inventory): await final wield confirmation

Keep the retail inventory transaction busy until authoritative WieldObject confirms the requested weapon. This prevents rapid weapon changes from overlapping on ACE and rejecting otherwise valid items such as spears.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-12 23:27:44 +02:00
parent 815ce0dec4
commit ea5cf5b842
8 changed files with 115 additions and 9 deletions

View file

@ -66,6 +66,8 @@ internal sealed class AutoWieldController : IDisposable
private PendingSwitch? _pendingSwitch;
private bool _disposed;
public bool IsBusy => _pendingSwitch is not null;
public AutoWieldController(
ClientObjectTable objects,
Func<uint> playerGuid,
@ -82,6 +84,7 @@ internal sealed class AutoWieldController : IDisposable
_objects.ObjectMoved += OnObjectMoved;
_objects.ObjectRemoved += OnObjectRemoved;
_objects.MoveRequestFailed += OnMoveRequestFailed;
_objects.MoveRequestConfirmed += OnMoveRequestConfirmed;
_objects.Cleared += OnObjectsCleared;
}
@ -160,8 +163,12 @@ internal sealed class AutoWieldController : IDisposable
{
if (_sendWield is null)
return false;
_pendingSwitch = new PendingSwitch(item.ObjectId, BlockingItemId: 0);
if (!_objects.WieldItemOptimistic(item.ObjectId, _playerGuid(), mask))
{
_pendingSwitch = null;
return false;
}
// Retail ACCWeenieObject::UIAttemptWield @ 0x0058D590.
_sendWield(item.ObjectId, (uint)mask);
@ -171,6 +178,7 @@ internal sealed class AutoWieldController : IDisposable
private void OnObjectMoved(ClientObject item, uint _, uint newContainerId)
{
if (_pendingSwitch is not { } pending
|| pending.BlockingItemId == 0
|| item.ObjectId != pending.BlockingItemId
|| newContainerId != _playerGuid()
|| item.CurrentlyEquippedLocation != EquipMask.None)
@ -183,6 +191,13 @@ internal sealed class AutoWieldController : IDisposable
TryWield(requested);
}
private void OnMoveRequestConfirmed(ClientObject item)
{
if (_pendingSwitch is { BlockingItemId: 0 } pending
&& item.ObjectId == pending.RequestedItemId)
_pendingSwitch = null;
}
private void OnMoveRequestFailed(MoveRequestFailure failure)
{
if (_pendingSwitch is { } pending
@ -304,6 +319,7 @@ internal sealed class AutoWieldController : IDisposable
_objects.ObjectMoved -= OnObjectMoved;
_objects.ObjectRemoved -= OnObjectRemoved;
_objects.MoveRequestFailed -= OnMoveRequestFailed;
_objects.MoveRequestConfirmed -= OnMoveRequestConfirmed;
_objects.Cleared -= OnObjectsCleared;
}

View file

@ -112,7 +112,8 @@ public sealed class ItemInteractionController : IDisposable
public int BusyCount => _busyCount;
public bool CanMakeInventoryRequest => _readyForInventoryRequest() && _busyCount == 0;
public bool CanMakeInventoryRequest =>
_readyForInventoryRequest() && _busyCount == 0 && !_autoWield.IsBusy;
/// <summary>
/// Raised for retail confirmation/auxiliary actions whose retained panel is
@ -246,7 +247,7 @@ public sealed class ItemInteractionController : IDisposable
Snapshot(item),
_playerGuid(),
_groundObjectId(),
_readyForInventoryRequest() && _busyCount == 0,
CanMakeInventoryRequest,
_activeVendorId(),
BypassClassification: false,
UseCurrentSelection: false,
@ -277,7 +278,7 @@ public sealed class ItemInteractionController : IDisposable
if (!compatible)
return false;
if (!_readyForInventoryRequest() || _busyCount != 0)
if (!CanMakeInventoryRequest)
return false;
_sendUseWithTarget?.Invoke(sourceGuid, targetGuid);
_busyCount++;
@ -309,7 +310,7 @@ public sealed class ItemInteractionController : IDisposable
Snapshot(item),
_playerGuid(),
_groundObjectId(),
_readyForInventoryRequest() && _busyCount == 0,
CanMakeInventoryRequest,
TargetId: 0,
Target: null,
AllowGroundFallback: true,