fix(client): restore retail interaction parity
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:
parent
0c699240e0
commit
f6fe0f2a4f
151 changed files with 10162 additions and 1211 deletions
|
|
@ -116,6 +116,8 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
_itemInteraction = itemInteraction;
|
||||
_stackSplitQuantity = stackSplitQuantity;
|
||||
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
|
||||
if (_itemInteraction is not null)
|
||||
_itemInteraction.MergeAttempted += OnMergeAttempted;
|
||||
|
||||
WindowChromeController.BindCloseButton(layout, onClose);
|
||||
|
||||
|
|
@ -299,14 +301,13 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
if (containerId == EffectiveOpen() || containerId == _playerGuid())
|
||||
Populate();
|
||||
}
|
||||
private void OnInteractionStateChanged() => ApplyIndicators();
|
||||
private void OnInteractionStateChanged() => Populate();
|
||||
private void OnPendingBackpackPlacementRequested(PendingBackpackPlacement pending)
|
||||
{
|
||||
if (_pendingListPlacement is not null
|
||||
|| pending.ItemId == 0u
|
||||
|| pending.ContainerId != EffectiveOpen()
|
||||
|| _objects.Get(pending.ItemId) is not { } item
|
||||
|| IsBag(item))
|
||||
|| pending.ContainerId == 0u
|
||||
|| _objects.Get(pending.ItemId) is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -375,12 +376,33 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
|
||||
// Side-bag column: ALWAYS the player's bags (constant across container switches; only the
|
||||
// open/selected indicators move). Equipped items never appear here.
|
||||
var visibleBags = new List<uint>();
|
||||
foreach (var guid in _objects.GetContents(p))
|
||||
{
|
||||
var item = _objects.Get(guid);
|
||||
if (item is null || item.CurrentlyEquippedLocation != EquipMask.None) continue;
|
||||
bool isBag = IsBag(item);
|
||||
if (isBag) AddCell(_containerList, guid, isContainer: true);
|
||||
if (isBag) visibleBags.Add(guid);
|
||||
}
|
||||
|
||||
PendingListPlacement? pending = _pendingListPlacement;
|
||||
if (pending is { } bagProjection
|
||||
&& bagProjection.ContainerId == p
|
||||
&& _objects.Get(bagProjection.ItemId) is { } pendingBag
|
||||
&& IsBag(pendingBag))
|
||||
{
|
||||
visibleBags.Remove(bagProjection.ItemId);
|
||||
int index = Math.Clamp(bagProjection.Placement, 0, visibleBags.Count);
|
||||
visibleBags.Insert(index, bagProjection.ItemId);
|
||||
}
|
||||
|
||||
foreach (uint guid in visibleBags)
|
||||
{
|
||||
bool waiting = IsWaitingSource(guid)
|
||||
|| pending is { } waitingBagProjection
|
||||
&& waitingBagProjection.ContainerId == p
|
||||
&& waitingBagProjection.ItemId == guid;
|
||||
AddCell(_containerList, guid, isContainer: true, waiting);
|
||||
}
|
||||
|
||||
// Contents grid: the OPEN container's loose items. (Bags live in the column; a side bag has
|
||||
|
|
@ -394,20 +416,20 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
if (!isBag) visibleContents.Add(guid);
|
||||
}
|
||||
|
||||
PendingListPlacement? pending = _pendingListPlacement;
|
||||
if (pending is { } projection
|
||||
&& projection.ContainerId == open
|
||||
&& !visibleContents.Contains(projection.ItemId)
|
||||
&& _objects.Get(projection.ItemId) is { } pendingItem
|
||||
&& !IsBag(pendingItem))
|
||||
{
|
||||
visibleContents.Remove(projection.ItemId);
|
||||
int index = Math.Clamp(projection.Placement, 0, visibleContents.Count);
|
||||
visibleContents.Insert(index, projection.ItemId);
|
||||
}
|
||||
|
||||
foreach (uint guid in visibleContents)
|
||||
{
|
||||
bool waiting = pending is { } waitingProjection
|
||||
bool waiting = IsWaitingSource(guid)
|
||||
|| pending is { } waitingProjection
|
||||
&& waitingProjection.ContainerId == open
|
||||
&& waitingProjection.ItemId == guid;
|
||||
AddCell(_contentsGrid, guid, isContainer: false, waiting);
|
||||
|
|
@ -455,8 +477,8 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
dragIconTexture: _dragIconIds?.Invoke(
|
||||
ItemType.Container, PlayerPackBaseIcon, 0u, 0u, 0u) ?? 0u);
|
||||
main.DragAcceptSprite = 0x060011F7u; main.DragRejectSprite = 0x060011F8u;
|
||||
main.SetWaitingState(IsWaitingSource(p));
|
||||
main.Clicked = () => OpenContainer(p);
|
||||
main.DoubleClicked = () => _itemInteraction?.ActivateItem(p);
|
||||
SetCapacityBar(main, p); // main-pack fullness (items / ItemsCapacity)
|
||||
_topContainer.AddItem(main);
|
||||
}
|
||||
|
|
@ -474,6 +496,21 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
|| item.Type.HasFlag(ItemType.Container)
|
||||
|| item.ItemsCapacity > 0;
|
||||
|
||||
private int CountLooseContents(uint containerId)
|
||||
{
|
||||
int count = 0;
|
||||
foreach (uint guid in _objects.GetContents(containerId))
|
||||
{
|
||||
if (_objects.Get(guid) is { } item
|
||||
&& item.CurrentlyEquippedLocation == EquipMask.None
|
||||
&& !IsBag(item))
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private uint EffectiveOpen() => _openContainer != 0 ? _openContainer : _playerGuid();
|
||||
|
||||
/// <summary>The owned destination retail PlaceInBackpack currently uses.</summary>
|
||||
|
|
@ -499,12 +536,15 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
cell.SetWaitingState(waiting);
|
||||
cell.SlotIndex = list.GetNumUIItems(); // index it will occupy (== its slot in a packed list)
|
||||
ConfigureDropFeedback(list, cell);
|
||||
cell.DoubleClicked = () => _itemInteraction?.ActivateItem(guid);
|
||||
if (isContainer)
|
||||
{
|
||||
cell.Clicked = () => OpenContainer(guid);
|
||||
SetCapacityBar(cell, guid);
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.DoubleClicked = () => _itemInteraction?.ActivateItem(guid);
|
||||
}
|
||||
list.AddItem(cell);
|
||||
}
|
||||
|
||||
|
|
@ -513,7 +553,10 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
if (_itemInteraction?.OfferPrimaryClick(guid)
|
||||
is not null and not ItemPrimaryClickResult.NotActive)
|
||||
return true;
|
||||
SelectItem(guid);
|
||||
if (_objects.Get(guid) is { } item && IsBag(item))
|
||||
OpenContainer(guid);
|
||||
else
|
||||
SelectItem(guid);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -522,7 +565,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
if (_itemInteraction?.OfferSelfPrimaryClick()
|
||||
is not null and not ItemPrimaryClickResult.NotActive)
|
||||
return true;
|
||||
SelectItem(guid);
|
||||
OpenContainer(guid);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -556,11 +599,15 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
{
|
||||
int cap = _objects.Get(containerGuid)?.ItemsCapacity ?? 0;
|
||||
if (cap <= 0) { cell.CapacityFill = -1f; return; }
|
||||
int n = _objects.GetContents(containerGuid).Count;
|
||||
// Player contents contain two independent retail lists: loose items
|
||||
// and side packs. ItemsCapacity applies only to the former; counting
|
||||
// packs here made a main pack stay visually/full logically rejected
|
||||
// even after the player freed an item slot.
|
||||
int n = CountLooseContents(containerGuid);
|
||||
cell.CapacityFill = Math.Clamp(n / (float)cap, 0f, 1f);
|
||||
}
|
||||
|
||||
// ── IItemListDragHandler (B-Drag) — drop an item to move it (optimistic + wire) ──────────────
|
||||
// ── IItemListDragHandler (B-Drag) — request first; server owns placement ────────────────────
|
||||
/// <summary>Retail ItemList_BeginDrag selects an unselected item before enabling its waiting
|
||||
/// mesh. Inventory items do not lift-remove (unlike the toolbar): the item stays in its slot
|
||||
/// until the server confirms the eventual drop.</summary>
|
||||
|
|
@ -583,35 +630,15 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
// remove-on-lift stands.
|
||||
if (payload.SourceKind == ItemDragSource.ShortcutBar)
|
||||
return ItemDragAcceptance.None;
|
||||
if (payload.ObjId == 0)
|
||||
return ItemDragAcceptance.Reject;
|
||||
bool sourceIsBag = _objects.Get(payload.ObjId) is { } source && IsBag(source);
|
||||
if (targetList == _contentsGrid)
|
||||
return sourceIsBag
|
||||
? ItemDragAcceptance.Reject
|
||||
: ItemDragAcceptance.Accept;
|
||||
if (targetList == _containerList || targetList == _topContainer)
|
||||
{
|
||||
// UIElement_ItemList::ItemList_DragOver @0x004E3400 checks the
|
||||
// dragged object's container flag before interpreting this list.
|
||||
// A container drag addresses the player's contained-container
|
||||
// list itself; an empty authored slot is therefore a valid pack
|
||||
// destination rather than "no target".
|
||||
if (sourceIsBag)
|
||||
return targetCell.ItemId == payload.ObjId
|
||||
? ItemDragAcceptance.Reject
|
||||
: ItemDragAcceptance.Accept;
|
||||
if (targetCell.ItemId == 0 || targetCell.ItemId == payload.ObjId)
|
||||
return ItemDragAcceptance.Reject;
|
||||
return IsContainerFull(targetCell.ItemId)
|
||||
? ItemDragAcceptance.Reject
|
||||
: ItemDragAcceptance.Accept;
|
||||
}
|
||||
return ItemDragAcceptance.Reject;
|
||||
return EvaluateDrop(targetList, targetCell, payload.ObjId, out _, out _)
|
||||
== InventoryContainerPlacementRejection.None
|
||||
? ItemDragAcceptance.Accept
|
||||
: ItemDragAcceptance.Reject;
|
||||
}
|
||||
|
||||
/// <summary>Resolve the destination and either split or move the stack. A partial split waits
|
||||
/// for the server-created object's guid; a whole move remains optimistic. Retail:
|
||||
/// for the server-created object's guid; a whole move displays only the
|
||||
/// destination list's waiting projection until the server responds. Retail:
|
||||
/// <c>ItemHolder::AttemptToPlaceInContainer @ 0x00588140</c>.</summary>
|
||||
public void HandleDropRelease(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
|
||||
{
|
||||
|
|
@ -627,8 +654,24 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
// DropReleased is still delivered to the list after a reject overlay;
|
||||
// pin the release to the same retail policy instead of relying on the
|
||||
// advisory color alone.
|
||||
if (OnDragOver(targetList, targetCell, payload) != ItemDragAcceptance.Accept)
|
||||
InventoryContainerPlacementRejection legality = EvaluateDrop(
|
||||
targetList,
|
||||
targetCell,
|
||||
item,
|
||||
out _,
|
||||
out uint legalityDestination);
|
||||
if (legality != InventoryContainerPlacementRejection.None)
|
||||
{
|
||||
if (InventoryContainerPlacementPolicy.ComposeClientLocal(
|
||||
legality,
|
||||
_objects.Get(item),
|
||||
_objects.Get(legalityDestination),
|
||||
_playerGuid()) is { } refusal)
|
||||
{
|
||||
_itemInteraction?.ReportClientLocal(refusal);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// UIElement_ItemList::AcceptDragObject @ 0x004E4250 rejects every
|
||||
// release while m_pendingItem exists, before merge, split, or ordinary
|
||||
|
|
@ -662,7 +705,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
container = EffectiveOpen();
|
||||
placement = targetCell.ItemId != 0
|
||||
? targetCell.SlotIndex // insert-before = the target's GRID INDEX (gapless), not its raw ContainerSlot
|
||||
: _objects.GetContents(container).Count; // first empty = append
|
||||
: CountLooseContents(container); // first empty = append after visible loose items
|
||||
}
|
||||
else if (targetList == _containerList || targetList == _topContainer)
|
||||
{
|
||||
|
|
@ -697,79 +740,65 @@ 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.
|
||||
DispatchInventoryRequest(
|
||||
InventoryRequestKind.SplitToContainer,
|
||||
item,
|
||||
() =>
|
||||
{
|
||||
if (_sendStackableSplitToContainer is null)
|
||||
return false;
|
||||
_sendStackableSplitToContainer(
|
||||
item,
|
||||
container,
|
||||
(uint)placement,
|
||||
splitSize);
|
||||
return true;
|
||||
});
|
||||
if (_itemInteraction is not null)
|
||||
{
|
||||
_itemInteraction.TrySplitToContainer(
|
||||
item,
|
||||
container,
|
||||
(uint)placement,
|
||||
splitSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
DispatchInventoryRequest(
|
||||
InventoryRequestKind.SplitToContainer,
|
||||
item,
|
||||
() =>
|
||||
{
|
||||
if (_sendStackableSplitToContainer is null)
|
||||
return false;
|
||||
_sendStackableSplitToContainer(
|
||||
item,
|
||||
container,
|
||||
(uint)placement,
|
||||
splitSize);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// External-container contents retain canonical ownership while the request
|
||||
// is in flight, but retail immediately inserts an m_pendingItem copy into
|
||||
// the chosen destination slot and ghosts it. The server move/failure notice
|
||||
// resolves that visual projection. UIElement_ItemList::HandleDropRelease
|
||||
// @ 0x004E4790; ACCWeenieObject::UIAttemptPutInContainer @ 0x0058D680.
|
||||
if (payload.SourceKind == ItemDragSource.Ground)
|
||||
// Canonical ownership never changes on request. Retail immediately
|
||||
// publishes the destination ItemList's m_pendingItem projection and
|
||||
// resolves it from the server move/failure response.
|
||||
if (_itemInteraction 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;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_pendingListPlacement is not null)
|
||||
return;
|
||||
_pendingListPlacement = new PendingListPlacement(0u, item, container, placement);
|
||||
Populate();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_itemInteraction is not null)
|
||||
{
|
||||
DispatchInventoryRequest(
|
||||
InventoryRequestKind.PutInContainer,
|
||||
InventoryRequestKind kind = payload.SourceKind == ItemDragSource.Ground
|
||||
? InventoryRequestKind.Pickup
|
||||
: InventoryRequestKind.PutInContainer;
|
||||
if (!_itemInteraction.TryDispatchPendingBackpackPlacement(
|
||||
item,
|
||||
container,
|
||||
placement,
|
||||
kind,
|
||||
() =>
|
||||
{
|
||||
if (_sendPutItemInContainer is null
|
||||
|| !_objects.MoveItemOptimistic(item, container, placement))
|
||||
{
|
||||
if (_sendPutItemInContainer is null)
|
||||
return false;
|
||||
}
|
||||
_sendPutItemInContainer(item, container, placement);
|
||||
return true;
|
||||
});
|
||||
}))
|
||||
{
|
||||
return;
|
||||
}
|
||||
_objects.MoveItemOptimistic(item, container, placement);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_pendingListPlacement is not null)
|
||||
return;
|
||||
_pendingListPlacement = new PendingListPlacement(0u, item, container, placement);
|
||||
Populate();
|
||||
_sendPutItemInContainer?.Invoke(item, container, placement);
|
||||
}
|
||||
|
||||
|
|
@ -832,9 +861,57 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
{
|
||||
int cap = _objects.Get(container)?.ItemsCapacity ?? 0;
|
||||
if (cap <= 0) return false;
|
||||
return _objects.GetContents(container).Count >= cap;
|
||||
return CountLooseContents(container) >= cap;
|
||||
}
|
||||
|
||||
private void OnMergeAttempted(uint sourceId, uint targetId)
|
||||
{
|
||||
_notifyMergeAttempt?.Invoke(sourceId, targetId);
|
||||
_selection.Select(targetId, SelectionChangeSource.Inventory);
|
||||
}
|
||||
|
||||
private InventoryContainerPlacementRejection EvaluateDrop(
|
||||
UiItemList targetList,
|
||||
UiItemSlot targetCell,
|
||||
uint itemId,
|
||||
out bool sourceIsBag,
|
||||
out uint destinationId)
|
||||
{
|
||||
sourceIsBag = _objects.Get(itemId) is { } source && IsBag(source);
|
||||
destinationId = 0u;
|
||||
if (itemId == 0u)
|
||||
return InventoryContainerPlacementRejection.InvalidItem;
|
||||
|
||||
if (ReferenceEquals(targetList, _contentsGrid))
|
||||
{
|
||||
destinationId = EffectiveOpen();
|
||||
// Carried containers belong to the authored container selector,
|
||||
// never the loose-item grid, even when both address the player.
|
||||
if (sourceIsBag)
|
||||
return InventoryContainerPlacementRejection.ContainerCapacityFull;
|
||||
}
|
||||
else if (ReferenceEquals(targetList, _containerList)
|
||||
|| ReferenceEquals(targetList, _topContainer))
|
||||
{
|
||||
destinationId = sourceIsBag ? _playerGuid() : targetCell.ItemId;
|
||||
if (!sourceIsBag && (targetCell.ItemId == 0u || targetCell.ItemId == itemId))
|
||||
return InventoryContainerPlacementRejection.InvalidDestination;
|
||||
}
|
||||
else
|
||||
{
|
||||
return InventoryContainerPlacementRejection.InvalidDestination;
|
||||
}
|
||||
|
||||
return InventoryContainerPlacementPolicy.Evaluate(
|
||||
_objects,
|
||||
itemId,
|
||||
destinationId,
|
||||
_playerGuid());
|
||||
}
|
||||
|
||||
private bool IsWaitingSource(uint itemGuid)
|
||||
=> _itemInteraction?.IsPendingInventorySource(itemGuid) == true;
|
||||
|
||||
/// <summary>Select an item (panel-wide green square) without changing the open container or
|
||||
/// touching the wire. Retail: UIElement_ItemList::ItemList_SetSelectedItem (0x004e2fe0).</summary>
|
||||
private void SelectItem(uint guid)
|
||||
|
|
@ -895,7 +972,8 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
{
|
||||
var cell = list.GetItem(i);
|
||||
if (cell is null) continue;
|
||||
bool pendingTargetSource = _itemInteraction?.IsPendingSource(cell.ItemId) == true;
|
||||
bool pendingTargetSource = _itemInteraction?.IsPendingSource(cell.ItemId) == true
|
||||
|| IsWaitingSource(cell.ItemId);
|
||||
cell.Selected = cell.ItemId != 0
|
||||
&& cell.ItemId == _selection.SelectedObjectId
|
||||
&& !pendingTargetSource;
|
||||
|
|
@ -1012,6 +1090,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
}
|
||||
if (_itemInteraction is not null)
|
||||
{
|
||||
_itemInteraction.MergeAttempted -= OnMergeAttempted;
|
||||
_itemInteraction.StateChanged -= OnInteractionStateChanged;
|
||||
_itemInteraction.PendingBackpackPlacementRequested -= OnPendingBackpackPlacementRequested;
|
||||
_itemInteraction.PendingBackpackPlacementCancelled -= OnPendingBackpackPlacementCancelled;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue