fix(interaction): close selection lifecycle review gaps
Bind queued actions and pending inventory requests to exact live incarnations, separate optimistic placement from authoritative responses, and serialize retail-style inventory ownership across UI surfaces. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
d2bb5af453
commit
5acc3f01cf
23 changed files with 2635 additions and 168 deletions
|
|
@ -12,6 +12,31 @@ public enum ItemPrimaryClickResult
|
|||
ConsumedRejected,
|
||||
}
|
||||
|
||||
public readonly record struct PendingBackpackPlacement(
|
||||
ulong Token,
|
||||
uint ItemId,
|
||||
uint ContainerId,
|
||||
int Placement,
|
||||
ClientObject? ItemIdentity);
|
||||
|
||||
public enum InventoryRequestKind
|
||||
{
|
||||
Pickup,
|
||||
PutInContainer,
|
||||
SplitToContainer,
|
||||
Merge,
|
||||
DropToWorld,
|
||||
SplitToWorld,
|
||||
Give,
|
||||
}
|
||||
|
||||
public readonly record struct PendingInventoryRequest(
|
||||
ulong Token,
|
||||
InventoryRequestKind Kind,
|
||||
uint ItemId,
|
||||
ClientObject? ItemIdentity,
|
||||
bool Dispatched);
|
||||
|
||||
/// <summary>
|
||||
/// Shared retail item interaction orchestrator. UI widgets route item clicks,
|
||||
/// target acquisition, and drag-out drops here instead of duplicating
|
||||
|
|
@ -20,6 +45,8 @@ public enum ItemPrimaryClickResult
|
|||
public sealed class ItemInteractionController : IDisposable
|
||||
{
|
||||
private const long RetailUseThrottleMs = 200;
|
||||
internal const string InventoryRequestBusyMessage =
|
||||
"You can only move or use one item at a time";
|
||||
private const long RetailDoubleClickMs = 500;
|
||||
|
||||
private readonly ClientObjectTable _objects;
|
||||
|
|
@ -56,6 +83,9 @@ public sealed class ItemInteractionController : IDisposable
|
|||
private uint _consumedPrimaryClickTarget;
|
||||
private long _consumedPrimaryClickMs = long.MinValue / 2;
|
||||
private int _busyCount;
|
||||
private ulong _nextInventoryRequestToken;
|
||||
private PendingBackpackPlacement? _pendingBackpackPlacement;
|
||||
private PendingInventoryRequest? _pendingInventoryRequest;
|
||||
private bool _disposed;
|
||||
|
||||
public ItemInteractionController(
|
||||
|
|
@ -119,6 +149,11 @@ public sealed class ItemInteractionController : IDisposable
|
|||
_systemMessage = systemMessage;
|
||||
_interactionState = interactionState ?? new InteractionState();
|
||||
_interactionState.Changed += OnInteractionModeChanged;
|
||||
_objects.ObjectMoved += OnInventoryObjectMoved;
|
||||
_objects.MoveRequestFailed += OnInventoryMoveFailed;
|
||||
_objects.ObjectRemoved += OnInventoryObjectRemoved;
|
||||
_objects.StackSizeUpdated += OnInventoryStackSizeUpdated;
|
||||
_objects.Cleared += OnInventoryObjectsCleared;
|
||||
_autoWield = new AutoWieldController(
|
||||
_objects,
|
||||
_playerGuid,
|
||||
|
|
@ -137,7 +172,20 @@ public sealed class ItemInteractionController : IDisposable
|
|||
/// panel inserts a waiting projection before the pickup request is sent.
|
||||
/// The destination and placement are the same values sent on the wire.
|
||||
/// </summary>
|
||||
public event Action<uint, uint, int>? PendingBackpackPlacementRequested;
|
||||
public event Action<PendingBackpackPlacement>? PendingBackpackPlacementRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Withdraws a waiting pickup projection when the approach is rejected or
|
||||
/// cancelled before a request reaches the server.
|
||||
/// </summary>
|
||||
public event Action<PendingBackpackPlacement>? PendingBackpackPlacementCancelled;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the exact waiting projection after an authoritative response.
|
||||
/// The token prevents a late response for a recycled GUID from erasing a
|
||||
/// newer incarnation's projection in the inventory panel.
|
||||
/// </summary>
|
||||
public event Action<PendingBackpackPlacement>? PendingBackpackPlacementResolved;
|
||||
|
||||
public uint PlayerGuid => _playerGuid();
|
||||
|
||||
|
|
@ -146,8 +194,151 @@ public sealed class ItemInteractionController : IDisposable
|
|||
public int BusyCount => _busyCount;
|
||||
|
||||
public bool CanMakeInventoryRequest =>
|
||||
BaseCanMakeInventoryRequest && _pendingInventoryRequest is null;
|
||||
|
||||
private bool BaseCanMakeInventoryRequest =>
|
||||
_readyForInventoryRequest() && _busyCount == 0 && !_autoWield.IsBusy;
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>ACCWeenieObject::IsPlayerReadyToMakeInventoryRequest</c>.
|
||||
/// The destination ItemList's separate <c>m_pendingItem</c> rejection is
|
||||
/// reported by <see cref="ReportPendingBackpackPlacementConflict"/>.
|
||||
/// </summary>
|
||||
public bool EnsureInventoryRequestReady()
|
||||
{
|
||||
if (CanMakeInventoryRequest)
|
||||
return true;
|
||||
|
||||
if (_pendingInventoryRequest is not null
|
||||
|| _busyCount != 0
|
||||
|| _autoWield.IsBusy)
|
||||
_systemMessage?.Invoke(InventoryRequestBusyMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>UIElement_ItemList::AcceptDragObject</c>'s local
|
||||
/// <c>m_pendingItem</c> branch. This wording belongs only to the destination
|
||||
/// list which already displays the waiting projection.
|
||||
/// </summary>
|
||||
public void ReportPendingBackpackPlacementConflict()
|
||||
{
|
||||
if (_pendingBackpackPlacement is not { } pending)
|
||||
return;
|
||||
string? itemName = _objects.Get(pending.ItemId)?.Name;
|
||||
string name = string.IsNullOrWhiteSpace(itemName) ? "that item" : itemName;
|
||||
_systemMessage?.Invoke($"Already attempting to place {name} here");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispatches one retail inventory request. A provisional global
|
||||
/// <c>ACCWeenieObject::prevRequest</c> owner closes callback reentrancy,
|
||||
/// then becomes dispatched immediately after a successful wire send.
|
||||
/// The callback returns false when it could not dispatch anything.
|
||||
/// Retail: <c>RecordRequest @ 0x0058C220</c>, UIAttempt family
|
||||
/// <c>0x0058D590..0x0058D850</c>.
|
||||
/// </summary>
|
||||
public bool TryDispatchInventoryRequest(
|
||||
InventoryRequestKind kind,
|
||||
uint itemId,
|
||||
Func<bool> dispatch,
|
||||
ulong reservationToken = 0u)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dispatch);
|
||||
if (itemId == 0u)
|
||||
return false;
|
||||
|
||||
if (reservationToken != 0u)
|
||||
{
|
||||
if (_pendingInventoryRequest is not { } reserved
|
||||
|| reserved.Token != reservationToken
|
||||
|| reserved.ItemId != itemId
|
||||
|| reserved.Kind != kind
|
||||
|| reserved.Dispatched)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bool reservedDispatch;
|
||||
try
|
||||
{
|
||||
reservedDispatch = dispatch();
|
||||
}
|
||||
catch
|
||||
{
|
||||
CancelPendingBackpackPlacement(itemId, reservationToken);
|
||||
throw;
|
||||
}
|
||||
if (!reservedDispatch)
|
||||
{
|
||||
CancelPendingBackpackPlacement(itemId, reservationToken);
|
||||
return false;
|
||||
}
|
||||
// A synchronous test transport or reentrant table listener may
|
||||
// already have completed this exact request. Never resurrect it or
|
||||
// overwrite a newer transaction acquired from that response.
|
||||
if (_pendingInventoryRequest is { } reservationCurrent
|
||||
&& reservationCurrent.Token == reserved.Token)
|
||||
{
|
||||
_pendingInventoryRequest = reserved with { Dispatched = true };
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!EnsureInventoryRequestReady())
|
||||
return false;
|
||||
|
||||
ulong token = ++_nextInventoryRequestToken;
|
||||
if (token == 0u)
|
||||
token = ++_nextInventoryRequestToken;
|
||||
var provisional = new PendingInventoryRequest(
|
||||
token,
|
||||
kind,
|
||||
itemId,
|
||||
_objects.Get(itemId),
|
||||
Dispatched: false);
|
||||
// Provisional ownership closes the reentrancy window around optimistic
|
||||
// table mutation and arbitrary send adapters. Retail writes prevRequest
|
||||
// after its synchronous event call; our callback seam can publish local
|
||||
// notices reentrantly, so the owner is reserved before entering it.
|
||||
_pendingInventoryRequest = provisional;
|
||||
StateChanged?.Invoke();
|
||||
|
||||
bool dispatched;
|
||||
try
|
||||
{
|
||||
dispatched = dispatch();
|
||||
}
|
||||
catch
|
||||
{
|
||||
ClearPendingInventoryRequest(token);
|
||||
throw;
|
||||
}
|
||||
if (!dispatched)
|
||||
{
|
||||
ClearPendingInventoryRequest(token);
|
||||
return false;
|
||||
}
|
||||
if (_pendingInventoryRequest is { } current
|
||||
&& current.Token == token)
|
||||
{
|
||||
_pendingInventoryRequest = provisional with { Dispatched = true };
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryGetPendingInventoryRequest(out PendingInventoryRequest pending)
|
||||
{
|
||||
if (_pendingInventoryRequest is { } current)
|
||||
{
|
||||
pending = current;
|
||||
return true;
|
||||
}
|
||||
pending = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Increments retail's shared <c>ClientUISystem</c> busy reference after a
|
||||
/// request issued by another retained controller has been sent. The
|
||||
|
|
@ -287,6 +478,8 @@ public sealed class ItemInteractionController : IDisposable
|
|||
|
||||
if (!ConsumeUseThrottle())
|
||||
return true;
|
||||
if (!EnsureInventoryRequestReady())
|
||||
return false;
|
||||
|
||||
var input = new ItemUsePolicyInput(
|
||||
Snapshot(item),
|
||||
|
|
@ -317,11 +510,195 @@ public sealed class ItemInteractionController : IDisposable
|
|||
if (containerId == 0u)
|
||||
containerId = _playerGuid();
|
||||
const int placement = 0;
|
||||
PendingBackpackPlacementRequested?.Invoke(itemGuid, containerId, placement);
|
||||
if (!TryBeginPendingBackpackPlacement(
|
||||
itemGuid,
|
||||
containerId,
|
||||
placement,
|
||||
out _))
|
||||
{
|
||||
// Retail permits only one inventory request at a time. Consume the
|
||||
// input without replacing a pending projection or issuing a wire
|
||||
// request while another inventory transaction owns the gate.
|
||||
return true;
|
||||
}
|
||||
// This callback may install a retail MoveToObject approach instead of
|
||||
// sending immediately. SelectionInteractionController promotes the
|
||||
// reservation to Dispatched only when it actually emits the wire move.
|
||||
_placeInBackpack(itemGuid, containerId, placement);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryBeginPendingBackpackPlacement(
|
||||
uint itemGuid,
|
||||
uint containerId,
|
||||
int placement,
|
||||
out PendingBackpackPlacement pending)
|
||||
=> TryBeginPendingBackpackPlacement(
|
||||
itemGuid,
|
||||
containerId,
|
||||
placement,
|
||||
InventoryRequestKind.Pickup,
|
||||
out pending);
|
||||
|
||||
private bool TryBeginPendingBackpackPlacement(
|
||||
uint itemGuid,
|
||||
uint containerId,
|
||||
int placement,
|
||||
InventoryRequestKind kind,
|
||||
out PendingBackpackPlacement pending)
|
||||
{
|
||||
if (itemGuid == 0u || containerId == 0u)
|
||||
{
|
||||
pending = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_pendingBackpackPlacement is { } existing)
|
||||
{
|
||||
ReportPendingBackpackPlacementConflict();
|
||||
pending = existing;
|
||||
return false;
|
||||
}
|
||||
if (!EnsureInventoryRequestReady())
|
||||
{
|
||||
pending = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
ulong token = ++_nextInventoryRequestToken;
|
||||
if (token == 0u)
|
||||
token = ++_nextInventoryRequestToken;
|
||||
ClientObject? identity = _objects.Get(itemGuid);
|
||||
pending = new PendingBackpackPlacement(
|
||||
token,
|
||||
itemGuid,
|
||||
containerId,
|
||||
placement,
|
||||
identity);
|
||||
_pendingBackpackPlacement = pending;
|
||||
_pendingInventoryRequest = new PendingInventoryRequest(
|
||||
token,
|
||||
kind,
|
||||
itemGuid,
|
||||
identity,
|
||||
Dispatched: false);
|
||||
PendingBackpackPlacementRequested?.Invoke(pending);
|
||||
if (_pendingBackpackPlacement != pending
|
||||
|| _pendingInventoryRequest is not { } published
|
||||
|| published.Token != token
|
||||
|| published.ItemId != itemGuid
|
||||
|| published.Kind != kind
|
||||
|| published.Dispatched)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
StateChanged?.Invoke();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes retail's destination waiting projection, dispatches the move,
|
||||
/// then records the global request. Failed dispatch withdraws only the
|
||||
/// projection created by this call.
|
||||
/// </summary>
|
||||
public bool TryDispatchPendingBackpackPlacement(
|
||||
uint itemGuid,
|
||||
uint containerId,
|
||||
int placement,
|
||||
InventoryRequestKind kind,
|
||||
Func<bool> dispatch)
|
||||
{
|
||||
// Callers such as gmToolbarUI check the global prevRequest before they
|
||||
// ask CPlayerSystem to publish ShowPendingInPlayer. This preserves the
|
||||
// generic busy wording when both states happen to be present.
|
||||
if (!EnsureInventoryRequestReady())
|
||||
return false;
|
||||
if (!TryBeginPendingBackpackPlacement(
|
||||
itemGuid,
|
||||
containerId,
|
||||
placement,
|
||||
kind,
|
||||
out PendingBackpackPlacement pending))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_pendingBackpackPlacement != pending
|
||||
|| _pendingInventoryRequest is not { } reserved
|
||||
|| reserved.Token != pending.Token
|
||||
|| reserved.ItemId != pending.ItemId
|
||||
|| reserved.Kind != kind
|
||||
|| reserved.Dispatched)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (TryDispatchInventoryRequest(
|
||||
kind,
|
||||
itemGuid,
|
||||
dispatch,
|
||||
pending.Token))
|
||||
return true;
|
||||
|
||||
CancelPendingBackpackPlacement(itemGuid, pending.Token);
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetPendingBackpackPlacement(
|
||||
uint itemGuid,
|
||||
out PendingBackpackPlacement pending)
|
||||
{
|
||||
if (_pendingBackpackPlacement is { } current
|
||||
&& current.ItemId == itemGuid)
|
||||
{
|
||||
pending = current;
|
||||
return true;
|
||||
}
|
||||
|
||||
pending = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void CancelPendingBackpackPlacement(uint itemGuid = 0u, ulong token = 0u)
|
||||
{
|
||||
if (_pendingBackpackPlacement is not { } pending
|
||||
|| (itemGuid != 0u && pending.ItemId != itemGuid)
|
||||
|| (token != 0u && pending.Token != token))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingBackpackPlacement = null;
|
||||
if (_pendingInventoryRequest is { } request
|
||||
&& request.Token == pending.Token
|
||||
&& !request.Dispatched)
|
||||
{
|
||||
_pendingInventoryRequest = null;
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
PendingBackpackPlacementCancelled?.Invoke(pending);
|
||||
}
|
||||
|
||||
private void ClearPendingInventoryRequest(ulong token)
|
||||
{
|
||||
if (_pendingInventoryRequest is not { } pending
|
||||
|| pending.Token != token)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_pendingInventoryRequest = null;
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
|
||||
internal bool TryCaptureObjectIdentity(uint objectId, out ClientObject item)
|
||||
{
|
||||
item = _objects.Get(objectId)!;
|
||||
return item is not null;
|
||||
}
|
||||
|
||||
internal bool IsCurrentObjectIdentity(uint objectId, ClientObject item)
|
||||
=> ReferenceEquals(_objects.Get(objectId), item);
|
||||
|
||||
/// <summary>
|
||||
/// Retail paperdoll drop entry point. The paperdoll resolves the exact
|
||||
/// target side/location; this shared interaction owner performs AutoWield's
|
||||
|
|
@ -333,6 +710,8 @@ public sealed class ItemInteractionController : IDisposable
|
|||
return false;
|
||||
if (_objects.Get(itemGuid) is not { } item)
|
||||
return false;
|
||||
if (!EnsureInventoryRequestReady())
|
||||
return false;
|
||||
return _autoWield.TryWield(item, targetMask);
|
||||
}
|
||||
|
||||
|
|
@ -360,7 +739,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
if (!compatible)
|
||||
return false;
|
||||
|
||||
if (!CanMakeInventoryRequest)
|
||||
if (!EnsureInventoryRequestReady())
|
||||
return false;
|
||||
_sendUseWithTarget?.Invoke(sourceGuid, targetGuid);
|
||||
_busyCount++;
|
||||
|
|
@ -380,6 +759,8 @@ public sealed class ItemInteractionController : IDisposable
|
|||
{
|
||||
if (objectId == 0u || _sendUse is null)
|
||||
return false;
|
||||
if (!EnsureInventoryRequestReady())
|
||||
return false;
|
||||
_sendUse(objectId);
|
||||
_busyCount++;
|
||||
return true;
|
||||
|
|
@ -407,6 +788,8 @@ public sealed class ItemInteractionController : IDisposable
|
|||
return false;
|
||||
if (payload.ObjId == 0 || _objects.Get(payload.ObjId) is not { } item)
|
||||
return false;
|
||||
if (!EnsureInventoryRequestReady())
|
||||
return false;
|
||||
|
||||
uint fullStack = (uint)Math.Max(1, item.StackSize);
|
||||
int splitSize = (int)(_stackSplitQuantity?.GetObjectSplitSize(
|
||||
|
|
@ -496,22 +879,54 @@ public sealed class ItemInteractionController : IDisposable
|
|||
switch (action.Kind)
|
||||
{
|
||||
case ItemPolicyActionKind.DropToWorld:
|
||||
_objects.MoveItemOptimistic(action.ObjectId, newContainerId: 0u, newSlot: -1);
|
||||
_sendDrop?.Invoke(action.ObjectId);
|
||||
TryDispatchInventoryRequest(
|
||||
InventoryRequestKind.DropToWorld,
|
||||
action.ObjectId,
|
||||
() =>
|
||||
{
|
||||
if (_sendDrop is null
|
||||
|| !_objects.MoveItemOptimistic(
|
||||
action.ObjectId,
|
||||
newContainerId: 0u,
|
||||
newSlot: -1))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_sendDrop(action.ObjectId);
|
||||
return true;
|
||||
});
|
||||
break;
|
||||
case ItemPolicyActionKind.SplitToWorld:
|
||||
// UIAttemptSplitTo3D leaves the original object in its container;
|
||||
// the server assigns the split-off ground stack a new guid.
|
||||
_sendSplitToWorld?.Invoke(action.ObjectId, (uint)action.Amount);
|
||||
TryDispatchInventoryRequest(
|
||||
InventoryRequestKind.SplitToWorld,
|
||||
action.ObjectId,
|
||||
() =>
|
||||
{
|
||||
if (_sendSplitToWorld is null)
|
||||
return false;
|
||||
_sendSplitToWorld(action.ObjectId, (uint)action.Amount);
|
||||
return true;
|
||||
});
|
||||
break;
|
||||
case ItemPolicyActionKind.GiveToTarget:
|
||||
// UIAttemptGive @ 0x0058D620 sends the request and leaves
|
||||
// the source untouched until the authoritative server
|
||||
// InventoryRemoveObject / SetStackSize response arrives.
|
||||
_sendGive?.Invoke(
|
||||
action.TargetId,
|
||||
TryDispatchInventoryRequest(
|
||||
InventoryRequestKind.Give,
|
||||
action.ObjectId,
|
||||
(uint)Math.Max(1, action.Amount));
|
||||
() =>
|
||||
{
|
||||
if (_sendGive is null)
|
||||
return false;
|
||||
_sendGive(
|
||||
action.TargetId,
|
||||
action.ObjectId,
|
||||
(uint)Math.Max(1, action.Amount));
|
||||
return true;
|
||||
});
|
||||
break;
|
||||
case ItemPolicyActionKind.PlaceInContainer:
|
||||
{
|
||||
|
|
@ -519,10 +934,35 @@ public sealed class ItemInteractionController : IDisposable
|
|||
1,
|
||||
_objects.Get(action.ObjectId)?.StackSize ?? action.Amount);
|
||||
uint amount = (uint)Math.Max(1, action.Amount);
|
||||
if (amount < fullStack)
|
||||
_sendSplitToContainer?.Invoke(action.ObjectId, action.TargetId, 0u, amount);
|
||||
else
|
||||
_sendPutItemInContainer?.Invoke(action.ObjectId, action.TargetId, 0);
|
||||
InventoryRequestKind kind = amount < fullStack
|
||||
? InventoryRequestKind.SplitToContainer
|
||||
: InventoryRequestKind.PutInContainer;
|
||||
TryDispatchInventoryRequest(
|
||||
kind,
|
||||
action.ObjectId,
|
||||
() =>
|
||||
{
|
||||
if (amount < fullStack)
|
||||
{
|
||||
if (_sendSplitToContainer is null)
|
||||
return false;
|
||||
_sendSplitToContainer(
|
||||
action.ObjectId,
|
||||
action.TargetId,
|
||||
0u,
|
||||
amount);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_sendPutItemInContainer is null)
|
||||
return false;
|
||||
_sendPutItemInContainer(
|
||||
action.ObjectId,
|
||||
action.TargetId,
|
||||
0);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ItemPolicyActionKind.Reject:
|
||||
|
|
@ -571,11 +1011,93 @@ public sealed class ItemInteractionController : IDisposable
|
|||
private void OnInteractionModeChanged(InteractionModeTransition _)
|
||||
=> StateChanged?.Invoke();
|
||||
|
||||
private void OnInventoryObjectMoved(ClientObjectMove move)
|
||||
{
|
||||
if (move.Origin != ClientObjectMoveOrigin.AuthoritativeResponse)
|
||||
return;
|
||||
|
||||
CompleteInventoryResponse(move.ItemId, move.Item);
|
||||
}
|
||||
|
||||
private void OnInventoryMoveFailed(MoveRequestFailure failure)
|
||||
{
|
||||
ClientObject? identity = _objects.Get(failure.ItemId);
|
||||
CompleteInventoryResponse(failure.ItemId, identity);
|
||||
}
|
||||
|
||||
private void OnInventoryObjectRemoved(ClientObject item)
|
||||
{
|
||||
CompleteInventoryResponse(item.ObjectId, item);
|
||||
}
|
||||
|
||||
private void OnInventoryStackSizeUpdated(ClientObject item)
|
||||
{
|
||||
CompleteInventoryResponse(item.ObjectId, item);
|
||||
}
|
||||
|
||||
private void CompleteInventoryResponse(uint itemId, ClientObject? identity)
|
||||
{
|
||||
PendingInventoryRequest? completedRequest = null;
|
||||
PendingBackpackPlacement? completedPlacement = null;
|
||||
|
||||
if (_pendingInventoryRequest is { } request
|
||||
&& request.ItemId == itemId
|
||||
&& MatchesIdentity(request.ItemIdentity, itemId, identity))
|
||||
{
|
||||
completedRequest = request;
|
||||
_pendingInventoryRequest = null;
|
||||
}
|
||||
|
||||
if (_pendingBackpackPlacement is { } placement
|
||||
&& placement.ItemId == itemId
|
||||
&& MatchesIdentity(placement.ItemIdentity, itemId, identity))
|
||||
{
|
||||
completedPlacement = placement;
|
||||
_pendingBackpackPlacement = null;
|
||||
}
|
||||
|
||||
// ACCWeenieObject::RecordResponse @ 0x0058CAB0 clears prevRequest
|
||||
// before ServerSaysMoveItem publishes the item-list notice. Clear the
|
||||
// coupled global owner and local waiting projection atomically before
|
||||
// either callback so observers never see a half-completed transaction.
|
||||
if (completedPlacement is { } resolved)
|
||||
PendingBackpackPlacementResolved?.Invoke(resolved);
|
||||
if (completedRequest is not null)
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
|
||||
private bool MatchesIdentity(
|
||||
ClientObject? expected,
|
||||
uint itemId,
|
||||
ClientObject? actual)
|
||||
{
|
||||
if (expected is null)
|
||||
return true;
|
||||
if (actual is not null)
|
||||
return ReferenceEquals(expected, actual);
|
||||
return _objects.Get(itemId) is not { } current
|
||||
|| ReferenceEquals(expected, current);
|
||||
}
|
||||
|
||||
private void OnInventoryObjectsCleared()
|
||||
{
|
||||
bool changed = _pendingInventoryRequest is not null;
|
||||
_pendingInventoryRequest = null;
|
||||
_pendingBackpackPlacement = null;
|
||||
if (changed)
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
_interactionState.Changed -= OnInteractionModeChanged;
|
||||
_objects.ObjectMoved -= OnInventoryObjectMoved;
|
||||
_objects.MoveRequestFailed -= OnInventoryMoveFailed;
|
||||
_objects.ObjectRemoved -= OnInventoryObjectRemoved;
|
||||
_objects.StackSizeUpdated -= OnInventoryStackSizeUpdated;
|
||||
_objects.Cleared -= OnInventoryObjectsCleared;
|
||||
_autoWield.Dispose();
|
||||
}
|
||||
|
||||
|
|
@ -602,9 +1124,11 @@ public sealed class ItemInteractionController : IDisposable
|
|||
/// </summary>
|
||||
public void ResetSession()
|
||||
{
|
||||
bool busyChanged = _busyCount != 0;
|
||||
CancelPendingBackpackPlacement();
|
||||
bool busyChanged = _busyCount != 0 || _pendingInventoryRequest is not null;
|
||||
bool modeChanged = IsAnyTargetModeActive;
|
||||
_busyCount = 0;
|
||||
_pendingInventoryRequest = null;
|
||||
_lastUseMs = long.MinValue / 2;
|
||||
_consumedPrimaryClickTarget = 0u;
|
||||
_consumedPrimaryClickMs = long.MinValue / 2;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue