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:
parent
815ce0dec4
commit
ea5cf5b842
8 changed files with 115 additions and 9 deletions
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -85,6 +85,13 @@ public sealed class ClientObjectTable
|
|||
/// </summary>
|
||||
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.
|
||||
/// </summary>
|
||||
public event Action<ClientObject>? MoveRequestConfirmed;
|
||||
|
||||
/// <summary>
|
||||
/// Fires for every InventoryServerSaveFailed response, including requests
|
||||
/// which did not mutate the table optimistically. Transaction coordinators
|
||||
|
|
@ -258,8 +265,16 @@ public sealed class ClientObjectTable
|
|||
public void ConfirmMove(uint itemId)
|
||||
{
|
||||
if (!_pendingMoves.TryGetValue(itemId, out var p)) return;
|
||||
if (p.outstanding <= 1) _pendingMoves.Remove(itemId);
|
||||
else _pendingMoves[itemId] = (p.container, p.slot, p.equip, p.outstanding - 1);
|
||||
if (p.outstanding <= 1)
|
||||
{
|
||||
_pendingMoves.Remove(itemId);
|
||||
if (_objects.TryGetValue(itemId, out ClientObject? item))
|
||||
MoveRequestConfirmed?.Invoke(item);
|
||||
}
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue