fix(client): restore retail interaction parity
All checks were successful
CI / linux-portable (push) Successful in 3m27s
CI / windows-gate (push) Successful in 6m42s
CI / release (push) Successful in 2m12s

Harden keyboard and camera routing, inventory and vendor interactions, chat/emotes, relog portal flow, and paperdoll rendering. Add retail research, connected gate coverage, and release-gate validation.
This commit is contained in:
Erik 2026-08-26 20:45:11 +02:00
parent 0c699240e0
commit f6fe0f2a4f
151 changed files with 10162 additions and 1211 deletions

View file

@ -43,6 +43,7 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
private readonly UiItemList _contentsList;
private uint _openContainer;
private PendingBackpackPlacement? _pendingPlacement;
private bool _closeRequested;
private bool _disposed;
@ -115,6 +116,9 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
_objects.Cleared += OnObjectsCleared;
_selection.Changed += OnSelectionChanged;
_itemInteraction.StateChanged += OnInteractionStateChanged;
_itemInteraction.PendingBackpackPlacementRequested += OnPendingPlacementRequested;
_itemInteraction.PendingBackpackPlacementCancelled += OnPendingPlacementCancelled;
_itemInteraction.PendingBackpackPlacementResolved += OnPendingPlacementResolved;
ClearLists();
}
@ -210,13 +214,14 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
UiItemSlot targetCell,
ItemDragPayload payload)
{
if (payload.SourceKind == ItemDragSource.ShortcutBar)
return ItemDragAcceptance.None;
if (!ReferenceEquals(targetList, _contentsList)
|| payload.SourceKind == ItemDragSource.ShortcutBar
|| payload.ObjId == 0u
|| _openContainer == 0u
|| payload.ObjId == _openContainer)
|| _openContainer == 0u)
return ItemDragAcceptance.Reject;
return ItemDragAcceptance.Accept;
return EvaluateDrop(payload.ObjId) == InventoryContainerPlacementRejection.None
? ItemDragAcceptance.Accept
: ItemDragAcceptance.Reject;
}
public void HandleDropRelease(
@ -224,8 +229,19 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
UiItemSlot targetCell,
ItemDragPayload payload)
{
if (OnDragOver(targetList, targetCell, payload) != ItemDragAcceptance.Accept)
InventoryContainerPlacementRejection legality = EvaluateDrop(payload.ObjId);
if (legality != InventoryContainerPlacementRejection.None)
{
if (InventoryContainerPlacementPolicy.ComposeClientLocal(
legality,
_objects.Get(payload.ObjId),
_objects.Get(_openContainer),
playerId: 0u) is { } refusal)
{
_itemInteraction.ReportClientLocal(refusal);
}
return;
}
if (!_itemInteraction.EnsureInventoryRequestReady())
return;
if (_objects.Get(payload.ObjId) is not { } item)
@ -246,17 +262,30 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
InventoryRequestKind kind = amount < fullStack
? InventoryRequestKind.SplitToContainer
: InventoryRequestKind.PutInContainer;
_itemInteraction.TryDispatchInventoryRequest(
kind,
item.ObjectId,
() =>
{
if (amount < fullStack)
if (amount < fullStack)
{
_itemInteraction.TryDispatchInventoryRequest(
kind,
item.ObjectId,
() =>
{
_sendSplitToContainer(item.ObjectId, _openContainer, (uint)placement, amount);
else
return true;
});
}
else
{
_itemInteraction.TryDispatchPendingBackpackPlacement(
item.ObjectId,
_openContainer,
placement,
kind,
() =>
{
_sendPutItemInContainer(item.ObjectId, _openContainer, placement);
return true;
});
return true;
});
}
}
private void OnExternalContainerChanged(ExternalContainerTransition transition)
@ -314,10 +343,29 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
AddContainerCell(guid);
}
var visibleContents = new List<uint>();
foreach (uint guid in _objects.GetContents(_openContainer))
{
if (!IsContainer(_objects.Get(guid)))
AddContentsCell(guid);
visibleContents.Add(guid);
}
if (_pendingPlacement is { } pending
&& pending.ContainerId == _openContainer
&& _objects.Get(pending.ItemId) is { } pendingItem
&& !IsContainer(pendingItem))
{
visibleContents.Remove(pending.ItemId);
visibleContents.Insert(
Math.Clamp(pending.Placement, 0, visibleContents.Count),
pending.ItemId);
}
foreach (uint guid in visibleContents)
{
bool waiting = _itemInteraction.IsPendingInventorySource(guid)
|| _pendingPlacement is { } projection
&& projection.ContainerId == _openContainer
&& projection.ItemId == guid;
AddContentsCell(guid, waiting);
}
ApplyIndicators();
}
@ -334,15 +382,15 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
private void AddContainerCell(uint guid)
{
UiItemSlot cell = CreateCell(_containerList, guid, ItemDragSource.Ground);
cell.Clicked = () => OpenNestedContainer(guid);
SetCapacity(cell, guid);
_containerList.AddItem(cell);
}
private void AddContentsCell(uint guid)
private void AddContentsCell(uint guid, bool waiting = false)
{
UiItemSlot cell = CreateCell(_contentsList, guid, ItemDragSource.Ground);
cell.DoubleClicked = () => _itemInteraction.ActivateItem(guid);
cell.SetWaitingState(waiting);
cell.DragAcceptSprite = 0x060011F9u;
cell.DragRejectSprite = 0x060011F8u;
_contentsList.AddItem(cell);
@ -387,7 +435,10 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
{
if (_itemInteraction.OfferPrimaryClick(guid) != ItemPrimaryClickResult.NotActive)
return true;
Select(guid);
if (IsContainer(_objects.Get(guid)) && guid != _state.CurrentContainerId)
OpenNestedContainer(guid);
else
Select(guid);
return false;
}
@ -406,7 +457,8 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
bool pendingSource = _itemInteraction.IsPendingSource(cell.ItemId);
cell.Selected = cell.ItemId != 0u
&& cell.ItemId == _selection.SelectedObjectId
&& !pendingSource;
&& !pendingSource
&& !_itemInteraction.IsPendingInventorySource(cell.ItemId);
cell.IsOpenContainer = cell.ItemId != 0u && cell.ItemId == _openContainer;
}
}
@ -486,7 +538,48 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
}
private void OnSelectionChanged(SelectionTransition _) => ApplyIndicators();
private void OnInteractionStateChanged() => ApplyIndicators();
private void OnInteractionStateChanged()
{
if (_window.IsVisible)
Populate();
else
ApplyIndicators();
}
private void OnPendingPlacementRequested(PendingBackpackPlacement pending)
{
if (pending.ContainerId != _openContainer)
return;
_pendingPlacement = pending;
if (_window.IsVisible)
Populate();
}
private void OnPendingPlacementCancelled(PendingBackpackPlacement pending)
=> ResolvePendingPlacement(pending);
private void OnPendingPlacementResolved(PendingBackpackPlacement pending)
=> ResolvePendingPlacement(pending);
private void ResolvePendingPlacement(PendingBackpackPlacement pending)
{
if (_pendingPlacement is not { } current || current.Token != pending.Token)
return;
_pendingPlacement = null;
if (_window.IsVisible)
Populate();
}
private InventoryContainerPlacementRejection EvaluateDrop(uint itemId)
{
if (_objects.Get(itemId) is { } source && IsContainer(source))
return InventoryContainerPlacementRejection.ContainerCapacityFull;
return InventoryContainerPlacementPolicy.Evaluate(
_objects,
itemId,
_openContainer,
playerId: 0u);
}
private static bool IsContainer(ClientObject? item)
=> item is not null
@ -573,6 +666,9 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
_objects.Cleared -= OnObjectsCleared;
_selection.Changed -= OnSelectionChanged;
_itemInteraction.StateChanged -= OnInteractionStateChanged;
_itemInteraction.PendingBackpackPlacementRequested -= OnPendingPlacementRequested;
_itemInteraction.PendingBackpackPlacementCancelled -= OnPendingPlacementCancelled;
_itemInteraction.PendingBackpackPlacementResolved -= OnPendingPlacementResolved;
_topContainer.PrimaryItemPressed = null;
_containerList.PrimaryItemPressed = null;
_contentsList.PrimaryItemPressed = null;