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
|
|
@ -67,6 +67,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
private bool _disposed;
|
||||
|
||||
private readonly record struct PendingListPlacement(
|
||||
ulong Token,
|
||||
uint ItemId,
|
||||
uint ContainerId,
|
||||
int Placement);
|
||||
|
|
@ -183,6 +184,8 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
{
|
||||
_itemInteraction.StateChanged += OnInteractionStateChanged;
|
||||
_itemInteraction.PendingBackpackPlacementRequested += OnPendingBackpackPlacementRequested;
|
||||
_itemInteraction.PendingBackpackPlacementCancelled += OnPendingBackpackPlacementCancelled;
|
||||
_itemInteraction.PendingBackpackPlacementResolved += OnPendingBackpackPlacementResolved;
|
||||
}
|
||||
|
||||
Populate();
|
||||
|
|
@ -242,7 +245,9 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
}
|
||||
private void OnObjectRemoved(ClientObject o)
|
||||
{
|
||||
if (_pendingListPlacement?.ItemId == o.ObjectId)
|
||||
bool fallbackResolved = _itemInteraction is null
|
||||
&& _pendingListPlacement?.ItemId == o.ObjectId;
|
||||
if (fallbackResolved)
|
||||
_pendingListPlacement = null;
|
||||
if (_selection.SelectedObjectId == o.ObjectId)
|
||||
{
|
||||
|
|
@ -250,15 +255,16 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
SelectionChangeSource.System,
|
||||
SelectionChangeReason.SelectedObjectRemoved);
|
||||
}
|
||||
if (Concerns(o)) Populate();
|
||||
if (fallbackResolved || Concerns(o)) Populate();
|
||||
}
|
||||
private void OnObjectMoved(ClientObjectMove move)
|
||||
{
|
||||
bool resolvedPending = _pendingListPlacement?.ItemId == move.ItemId;
|
||||
if (resolvedPending)
|
||||
bool fallbackResolved = _itemInteraction is null
|
||||
&& _pendingListPlacement?.ItemId == move.ItemId;
|
||||
if (fallbackResolved)
|
||||
_pendingListPlacement = null;
|
||||
uint player = _playerGuid();
|
||||
if (resolvedPending
|
||||
if (fallbackResolved
|
||||
|| (move.Item is { } item && Concerns(item))
|
||||
|| move.Previous.ContainerId == player
|
||||
|| move.Current.ContainerId == player
|
||||
|
|
@ -272,30 +278,45 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
Populate();
|
||||
}
|
||||
private void OnInteractionStateChanged() => ApplyIndicators();
|
||||
private void OnPendingBackpackPlacementRequested(
|
||||
uint itemId,
|
||||
uint containerId,
|
||||
int placement)
|
||||
private void OnPendingBackpackPlacementRequested(PendingBackpackPlacement pending)
|
||||
{
|
||||
if (_pendingListPlacement is not null
|
||||
|| itemId == 0u
|
||||
|| containerId != EffectiveOpen()
|
||||
|| _objects.Get(itemId) is not { } item
|
||||
|| pending.ItemId == 0u
|
||||
|| pending.ContainerId != EffectiveOpen()
|
||||
|| _objects.Get(pending.ItemId) is not { } item
|
||||
|| IsBag(item))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingListPlacement = new PendingListPlacement(
|
||||
itemId,
|
||||
containerId,
|
||||
placement);
|
||||
pending.Token,
|
||||
pending.ItemId,
|
||||
pending.ContainerId,
|
||||
pending.Placement);
|
||||
Populate();
|
||||
}
|
||||
private void OnPendingBackpackPlacementCancelled(PendingBackpackPlacement pending)
|
||||
{
|
||||
if (_pendingListPlacement is not { } projection
|
||||
|| projection.Token != pending.Token)
|
||||
return;
|
||||
_pendingListPlacement = null;
|
||||
Populate();
|
||||
}
|
||||
private void OnPendingBackpackPlacementResolved(PendingBackpackPlacement pending)
|
||||
{
|
||||
if (_pendingListPlacement is not { } projection
|
||||
|| projection.Token != pending.Token)
|
||||
return;
|
||||
_pendingListPlacement = null;
|
||||
Populate();
|
||||
}
|
||||
private void OnSelectionChanged(SelectionTransition _) => ApplyIndicators();
|
||||
private void OnMoveRequestFailed(MoveRequestFailure failure)
|
||||
{
|
||||
if (_pendingListPlacement?.ItemId != failure.ItemId)
|
||||
if (_itemInteraction is not null
|
||||
|| _pendingListPlacement?.ItemId != failure.ItemId)
|
||||
return;
|
||||
_pendingListPlacement = null;
|
||||
Populate();
|
||||
|
|
@ -531,9 +552,6 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
return ItemDragAcceptance.None;
|
||||
if (payload.ObjId == 0)
|
||||
return ItemDragAcceptance.Reject;
|
||||
if (payload.SourceKind == ItemDragSource.Ground
|
||||
&& _pendingListPlacement is not null)
|
||||
return ItemDragAcceptance.Reject;
|
||||
if (targetList == _contentsGrid)
|
||||
return ItemDragAcceptance.Accept;
|
||||
if (targetList == _containerList || targetList == _topContainer)
|
||||
|
|
@ -561,6 +579,23 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
uint item = payload.ObjId;
|
||||
if (item == 0) return;
|
||||
|
||||
// UIElement_ItemList::AcceptDragObject @ 0x004E4250 rejects every
|
||||
// release while m_pendingItem exists, before merge, split, or ordinary
|
||||
// placement. ItemList_DragOver has no equivalent gate, so hover may
|
||||
// still show the authored accept cursor immediately before this no-op.
|
||||
if (targetList == _contentsGrid
|
||||
&& _pendingListPlacement is { } localPending
|
||||
&& localPending.ContainerId == EffectiveOpen())
|
||||
{
|
||||
_itemInteraction?.ReportPendingBackpackPlacementConflict();
|
||||
return;
|
||||
}
|
||||
if (_itemInteraction is not null
|
||||
&& !_itemInteraction.EnsureInventoryRequestReady())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// UIElement_ItemList::AcceptDragObject tries ItemHolder::AttemptMerge first
|
||||
// when a physical item is released on an occupied inventory cell. A legal
|
||||
// merge consumes the drop; an illegal merge falls through to insert/move.
|
||||
|
|
@ -597,8 +632,20 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
{
|
||||
// UIAttemptSplitToContainer leaves the source stack where it is. ACE will
|
||||
// publish the reduced source plus a newly-guided destination stack.
|
||||
_sendStackableSplitToContainer?.Invoke(
|
||||
item, container, (uint)placement, splitSize);
|
||||
DispatchInventoryRequest(
|
||||
InventoryRequestKind.SplitToContainer,
|
||||
item,
|
||||
() =>
|
||||
{
|
||||
if (_sendStackableSplitToContainer is null)
|
||||
return false;
|
||||
_sendStackableSplitToContainer(
|
||||
item,
|
||||
container,
|
||||
(uint)placement,
|
||||
splitSize);
|
||||
return true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -610,13 +657,52 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
// @ 0x004E4790; ACCWeenieObject::UIAttemptPutInContainer @ 0x0058D680.
|
||||
if (payload.SourceKind == ItemDragSource.Ground)
|
||||
{
|
||||
if (_pendingListPlacement is not null)
|
||||
if (_itemInteraction is not null)
|
||||
{
|
||||
if (!_itemInteraction.TryDispatchPendingBackpackPlacement(
|
||||
item,
|
||||
container,
|
||||
placement,
|
||||
InventoryRequestKind.Pickup,
|
||||
() =>
|
||||
{
|
||||
if (_sendPutItemInContainer is null)
|
||||
return false;
|
||||
_sendPutItemInContainer(item, container, placement);
|
||||
return true;
|
||||
}))
|
||||
{
|
||||
return;
|
||||
}
|
||||
return;
|
||||
_pendingListPlacement = new PendingListPlacement(item, container, placement);
|
||||
Populate();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_pendingListPlacement is not null)
|
||||
return;
|
||||
_pendingListPlacement = new PendingListPlacement(0u, item, container, placement);
|
||||
Populate();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_itemInteraction is not null)
|
||||
{
|
||||
DispatchInventoryRequest(
|
||||
InventoryRequestKind.PutInContainer,
|
||||
item,
|
||||
() =>
|
||||
{
|
||||
if (_sendPutItemInContainer is null
|
||||
|| !_objects.MoveItemOptimistic(item, container, placement))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_sendPutItemInContainer(item, container, placement);
|
||||
return true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
_objects.MoveItemOptimistic(item, container, placement);
|
||||
}
|
||||
_sendPutItemInContainer?.Invoke(item, container, placement);
|
||||
|
|
@ -651,14 +737,30 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
if (plan is not { } merge)
|
||||
return false;
|
||||
|
||||
_sendStackableMerge(merge.SourceObjectId, merge.TargetObjectId, merge.Amount);
|
||||
// ItemHolder::AttemptMerge broadcasts this immediately after UIAttemptMerge;
|
||||
// it is an attempt notice, not a later server-confirm event.
|
||||
_notifyMergeAttempt?.Invoke(merge.SourceObjectId, merge.TargetObjectId);
|
||||
_selection.Select(merge.TargetObjectId, SelectionChangeSource.Inventory);
|
||||
return true;
|
||||
return DispatchInventoryRequest(
|
||||
InventoryRequestKind.Merge,
|
||||
merge.SourceObjectId,
|
||||
() =>
|
||||
{
|
||||
_sendStackableMerge(
|
||||
merge.SourceObjectId,
|
||||
merge.TargetObjectId,
|
||||
merge.Amount);
|
||||
// ItemHolder::AttemptMerge broadcasts this immediately after UIAttemptMerge;
|
||||
// it is an attempt notice, not a later server-confirm event.
|
||||
_notifyMergeAttempt?.Invoke(merge.SourceObjectId, merge.TargetObjectId);
|
||||
_selection.Select(merge.TargetObjectId, SelectionChangeSource.Inventory);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private bool DispatchInventoryRequest(
|
||||
InventoryRequestKind kind,
|
||||
uint itemId,
|
||||
Func<bool> dispatch)
|
||||
=> _itemInteraction?.TryDispatchInventoryRequest(kind, itemId, dispatch)
|
||||
?? dispatch();
|
||||
|
||||
/// <summary>True only when we KNOW the container is full (capacity known + contents indexed). A
|
||||
/// closed bag (unknown count) returns false → advisory accept; the server is authoritative.</summary>
|
||||
private bool IsContainerFull(uint container)
|
||||
|
|
@ -691,7 +793,12 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
uint p = _playerGuid();
|
||||
_openContainer = guid;
|
||||
if (guid != p)
|
||||
_sendUse?.Invoke(guid); // open the side bag (ViewContents will land)
|
||||
{
|
||||
if (_itemInteraction is not null)
|
||||
_itemInteraction.ActivateItem(guid);
|
||||
else
|
||||
_sendUse?.Invoke(guid); // open the side bag (ViewContents will land)
|
||||
}
|
||||
Populate(); // repaint the grid for the new open container
|
||||
}
|
||||
|
||||
|
|
@ -814,6 +921,8 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
{
|
||||
_itemInteraction.StateChanged -= OnInteractionStateChanged;
|
||||
_itemInteraction.PendingBackpackPlacementRequested -= OnPendingBackpackPlacementRequested;
|
||||
_itemInteraction.PendingBackpackPlacementCancelled -= OnPendingBackpackPlacementCancelled;
|
||||
_itemInteraction.PendingBackpackPlacementResolved -= OnPendingBackpackPlacementResolved;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue