feat(ui): inventory/shortcut/paperdoll item-name tooltips — UIElement_UIItem::UpdateTooltip port

Retail UIElement_UIItem::UpdateTooltip @0x004E1CB0 caches the item's
NAME_APPROPRIATE display name (stack-count-prefixed "%d %s" when
StackSize > 1) as m_TTText every UIItem_Update refresh; the generic
UIElementManager::CheckTooltip dwell timer is what actually shows it
on hover — no special-cased trigger of its own.

UiItemSlot cells are built programmatically (never through
LayoutImporter.Build), so #409's original round left this deferred:
the class carried neither the popup locator (P0x47/P0x48) nor a name
source. A live-DAT sweep of the shared UIItem cell-template catalog
(ItemListCellTemplate.CatalogLayoutId, 0x21000037) found all 47
UIItem-type (class 0x10000032) prototypes — inventory's cell, every
toolbar slot, every paperdoll/armor slot skin — resolve the IDENTICAL
popup locator (P0x47=0x10000395/P0x48=0x21000041) through catalog
inheritance, with no literal text authored on any of them. UiItemSlot
now hardcodes that pair and exposes GetTooltipText() via a new
TooltipTextResolve delegate, wired at every physical-item
construction site: InventoryController (main-pack cell + grid cells),
ExternalContainerController, PaperdollController (closes the
separate gmPaperDollUI::UpdateItemSlotTooltip @0x004A52EF gap too —
same cell class, same fix), VendorUiController (shop/buying/selling
lists), SecureTradeUiController, ToolbarController.

Text is the new ClientObject.GetTooltipDisplayName(): GetAppropriateName()
prefixed with the stack count via "{count} {name}" when StackSize > 1,
matching UpdateTooltip's exact NAME_APPROPRIATE + "%d %s" sprintf.
UiCatalogSlot (spell/component catalog cells, a different UiItemSlot
subclass) is unaffected — it already overrides GetTooltipText() with
its own Label.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-16 23:02:10 +02:00
parent 5f9ca18155
commit e29c61a3a4
8 changed files with 87 additions and 3 deletions

View file

@ -12,13 +12,63 @@ namespace AcDream.App.UI;
/// </summary>
public class UiItemSlot : UiElement
{
public UiItemSlot() { ClickThrough = false; }
/// <summary>
/// Retail's shared UIItem cell-template catalog (<c>ItemListCellTemplate.
/// CatalogLayoutId</c>, LayoutDesc <c>0x21000037</c>) authors the SAME
/// tooltip popup locator on every one of its 49 standalone prototypes —
/// live-DAT-probed 2026-08-16: every top-level catalog child (inventory's
/// 32x32 cell <c>0x1000033A</c>, the toolbar's per-slot prototypes
/// <c>0x1000043B</c>.., the container cell <c>0x1000033F</c>, and every
/// paperdoll/armor slot skin alike) resolves <c>P0x47=0x10000395</c> /
/// <c>P0x48=0x21000041</c> through catalog inheritance, matching one of
/// the four popup skins <see cref="Layout.RetailTooltipPresenter"/> already
/// mounts for every other tooltip-bearing element
/// (<c>Layout.TooltipLiveDatTests.PopupSkinRootIds</c>). Since
/// <see cref="UiItemSlot"/> cells are built programmatically (never through
/// <c>LayoutImporter.Build</c>), this port hardcodes the uniform pair here
/// rather than re-deriving it per instance — the same "exhaustive scan,
/// then hardcode" shape as <c>RetailCursorCatalog</c>'s five window-control
/// cursor DIDs and <c>ItemListCellTemplate.CatalogLayoutId</c> itself.
/// </summary>
private const uint ItemTooltipRootElementId = 0x10000395u;
private const uint ItemTooltipLayoutDid = 0x21000041u;
public UiItemSlot()
{
ClickThrough = false;
AuthoredTooltipRootElementId = ItemTooltipRootElementId;
AuthoredTooltipLayoutDid = ItemTooltipLayoutDid;
}
public override bool ConsumesDatChildren => true;
/// <summary>Bound weenie guid (0 = empty). Retail UIElement_UIItem::itemID.</summary>
public uint ItemId { get; private set; }
/// <summary>
/// Resolves <see cref="ItemId"/> to its retail tooltip text (a
/// <see cref="AcDream.Core.Items.ClientObject.GetTooltipDisplayName"/>
/// call bound by the owning controller — every construction site already
/// has a <c>ClientObjectTable</c> reference in scope, matching how
/// <see cref="SpriteResolve"/> is wired). Null/empty result shows no
/// tooltip, matching retail's <c>UIItem_Update</c> early-out
/// (<c>weenObj == 0</c> -&gt; <c>UIElement::ClearTooltip</c>) for an
/// empty or not-yet-materialized cell.
/// </summary>
public Func<uint, string?>? TooltipTextResolve { get; set; }
/// <summary>
/// Port of <c>UIElement_UIItem::UpdateTooltip @0x004E1CB0</c>: called every
/// refresh (retail's own callers are heartbeat/state-change driven), but
/// computed lazily here — the same "runtime text on demand" shape already
/// established by <see cref="UiButton.GetTooltipText"/> and
/// <see cref="UiRadar.GetTooltipText"/> — rather than cached at
/// <see cref="SetItem"/> time, since nothing observes a stale value between
/// item-state changes and the next hover dwell.
/// </summary>
public override string? GetTooltipText()
=> ItemId != 0 ? TooltipTextResolve?.Invoke(ItemId) : null;
/// <summary>Pre-composited icon GL texture for the bound item (0 = none).</summary>
public uint IconTexture { get; private set; }