fix(ui): assess retained items on right click

Port UIElement_ItemList's physical-item right-click branch through the shared retained list. Select and appraise backpack, loot, paperdoll, and shortcut items through their canonical owners, while preventing RMB movement from lifting items or issuing appraisal requests.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-24 05:55:51 +02:00
parent 5ad32d5753
commit 2dd5cb80d2
17 changed files with 264 additions and 5 deletions

View file

@ -83,6 +83,9 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
ConfigureList(_topContainer, ContainerCellSize, horizontalScroll: false, containerEmptySprite);
ConfigureList(_containerList, ContainerCellSize, horizontalScroll: true, containerEmptySprite);
ConfigureList(_contentsList, ItemCellSize, horizontalScroll: true, contentsEmptySprite);
_topContainer.ExamineItemRequested = ExamineItem;
_containerList.ExamineItemRequested = ExamineItem;
_contentsList.ExamineItemRequested = ExamineItem;
_contentsList.RegisterDragHandler(this);
if (layout.FindElement(ContentsScrollbarId) is UiScrollbar scrollbar)
@ -375,6 +378,12 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
private void Select(uint guid)
=> _selection.Select(guid, SelectionChangeSource.ExternalContainer);
private void ExamineItem(uint guid)
{
Select(guid);
_itemInteraction.ExamineSelectedOrEnterMode(guid);
}
private void HandlePrimaryClick(uint guid, Action fallback)
{
if (_itemInteraction.OfferPrimaryClick(guid) != ItemPrimaryClickResult.NotActive)
@ -564,5 +573,8 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
_objects.Cleared -= OnObjectsCleared;
_selection.Changed -= OnSelectionChanged;
_itemInteraction.StateChanged -= OnInteractionStateChanged;
_topContainer.ExamineItemRequested = null;
_containerList.ExamineItemRequested = null;
_contentsList.ExamineItemRequested = null;
}
}

View file

@ -153,6 +153,9 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
_contentsGrid?.RegisterDragHandler(this);
_containerList?.RegisterDragHandler(this);
_topContainer?.RegisterDragHandler(this);
if (_contentsGrid is not null) _contentsGrid.ExamineItemRequested = ExamineItem;
if (_containerList is not null) _containerList.ExamineItemRequested = ExamineItem;
if (_topContainer is not null) _topContainer.ExamineItemRequested = ExamineItem;
// Burden meter: vertical 11×58 bar (gmBackpackUI m_burdenMeter, retail direction 4).
_burdenMeter = layout.FindElement(BurdenMeterId) as UiMeter;
@ -778,6 +781,17 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
_selection.Select(guid, SelectionChangeSource.Inventory);
}
/// <summary>
/// Retail ItemList right-click: select the physical item, then enter the
/// shared appraisal request owner. This is intentionally independent from
/// left-click target mode and double-click Use/AutoWield.
/// </summary>
private void ExamineItem(uint guid)
{
SelectItem(guid);
_itemInteraction?.ExamineSelectedOrEnterMode(guid);
}
/// <summary>Open a container (side bag or main pack): it becomes both the selected item and the
/// open container. Sends Use to open a side bag server-side (ViewContents arrives async).
/// Owned packs are not ClientUISystem::groundObject and never send
@ -917,6 +931,9 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
_objects.ObjectUpdated -= OnObjectChanged;
_objects.Cleared -= OnObjectsCleared;
_selection.Changed -= OnSelectionChanged;
if (_contentsGrid is not null) _contentsGrid.ExamineItemRequested = null;
if (_containerList is not null) _containerList.ExamineItemRequested = null;
if (_topContainer is not null) _topContainer.ExamineItemRequested = null;
if (_itemInteraction is not null)
{
_itemInteraction.StateChanged -= OnInteractionStateChanged;

View file

@ -84,6 +84,7 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
var (element, mask, _, unlockBit) = PaperdollSlotBackgrounds.Definitions[i];
if (layout.FindElement(element) is not UiItemList list) continue;
list.RegisterDragHandler(this);
list.ExamineItemRequested = ExamineItem;
list.Cell.SourceKind = ItemDragSource.Equipment;
list.Cell.SlotIndex = i; // definition position = equipped drag-payload SourceSlot
list.Cell.EmptySprite = emptySlotSprites is not null
@ -350,6 +351,12 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
foreach (var a in _armorSlots) a.Visible = _viewState.ArmorSlotsVisible;
}
private void ExamineItem(uint itemId)
{
_selection.Select(itemId, SelectionChangeSource.Paperdoll);
_itemInteraction.ExamineSelectedOrEnterMode(itemId);
}
/// <summary>Detach event handlers (idempotent).</summary>
public void Dispose()
{
@ -361,6 +368,8 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
_objects.ObjectUpdated -= OnObjectChanged;
_objects.Cleared -= OnObjectsCleared;
_selection.Changed -= OnSelectionChanged;
foreach (var (_, list) in _slots)
list.ExamineItemRequested = null;
if (_dollViewport is UiViewport doll)
doll.ClickedAt = null;
switch (_dollDragMask)

View file

@ -138,6 +138,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
if (_slots[i] is { } list)
{
WireClick(list);
list.ExamineItemRequested = ExamineItem;
// B.1 drag-drop spine: this controller is the drop handler for every
// toolbar slot list; each cell knows its slot index + that it's a
// shortcut-bar source (retail UIElement_ItemList::RegisterItemListDragHandler).
@ -531,6 +532,15 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
};
}
private void ExamineItem(uint itemId)
{
if (_selectItem is not null)
_selectItem(itemId);
else
_selection?.Select(itemId, SelectionChangeSource.Toolbar);
_itemInteraction?.ExamineSelectedOrEnterMode(itemId);
}
private static ItemDragAcceptance InventoryButtonDragOver(ItemDragPayload payload)
=> payload.ObjId != 0 && payload.SourceKind != ItemDragSource.ShortcutBar
? ItemDragAcceptance.Accept
@ -765,6 +775,9 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
_combatState.CombatModeChanged -= SetCombatMode;
if (_selection is not null)
_selection.Changed -= OnSelectionChanged;
foreach (UiItemList? list in _slots)
if (list is not null)
list.ExamineItemRequested = null;
_repo.ObjectAdded -= OnRepositoryObjectChanged;
_repo.ObjectUpdated -= OnRepositoryObjectChanged;
_repo.ObjectRemoved -= OnRepositoryObjectChanged;

View file

@ -73,6 +73,15 @@ public sealed class UiItemList : UiElement
/// </summary>
public Action<object, int, int>? CatalogDropped { get; set; }
/// <summary>
/// Appraisal command supplied by the owning panel controller for physical
/// item cells. Retail <c>UIElement_ItemList::ListenToElementMessage
/// @ 0x004E4D50</c> handles its right-click branch at <c>0x004E4ECA</c>:
/// select the occupied UIItem's itemID, then call
/// <c>ClientUISystem::ExamineObject</c>.
/// </summary>
public Action<uint>? ExamineItemRequested { get; set; }
private uint _cellEmptySprite;
/// <summary>Empty-slot sprite for THIS list's cells, resolved from the dat cell template
/// (retail attribute 0x1000000e -> catalog 0x21000037 prototype's ItemSlot_Empty; see

View file

@ -274,6 +274,11 @@ public class UiItemSlot : UiElement
case UiEventType.DoubleClick:
DoubleClicked?.Invoke();
return true;
case UiEventType.RightClick:
if (ItemId != 0
&& FindList() is { ExamineItemRequested: { } examine })
examine(ItemId);
return true;
case UiEventType.DragBegin:
// Notify the source list's handler so it can lift (remove + wire) — retail

View file

@ -496,7 +496,10 @@ public sealed class UiRoot : UiElement
}
else
{
_dragCandidate = true;
// Retail item drag begins from left-button movement. A right-button
// press remains a complete-click candidate for ItemList appraisal;
// it must never lift or move an inventory item.
_dragCandidate = btn == UiMouseButton.Left;
}
// Dispatch raw MouseDown event (retail uses WM_LBUTTONDOWN = 0x201).
@ -594,7 +597,10 @@ public sealed class UiRoot : UiElement
_lastClickX = x;
_lastClickY = y;
}
else if (btn == UiMouseButton.Right && ContainsAbsolute(target, x, y))
else if (btn == UiMouseButton.Right
&& ContainsAbsolute(target, x, y)
&& Math.Abs(x - _pressX) <= DragDistanceThreshold
&& Math.Abs(y - _pressY) <= DragDistanceThreshold)
{
var click = new UiEvent(target.EventId, target, UiEventType.RightClick,
Data0: (int)flags);