From e29c61a3a4738cf010eca638c1f5671b88cad42f Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 16 Aug 2026 23:02:10 +0200 Subject: [PATCH] =?UTF-8?q?feat(ui):=20inventory/shortcut/paperdoll=20item?= =?UTF-8?q?-name=20tooltips=20=E2=80=94=20UIElement=5FUIItem::UpdateToolti?= =?UTF-8?q?p=20port?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../UI/Layout/ExternalContainerController.cs | 1 + .../UI/Layout/InventoryController.cs | 12 ++++- .../UI/Layout/PaperdollController.cs | 1 + .../UI/Layout/SecureTradeUiController.cs | 1 + .../UI/Layout/ToolbarController.cs | 1 + .../UI/Layout/VendorUiController.cs | 6 +++ src/AcDream.App/UI/UiItemSlot.cs | 52 ++++++++++++++++++- src/AcDream.Core/Items/ClientObject.cs | 16 ++++++ 8 files changed, 87 insertions(+), 3 deletions(-) diff --git a/src/AcDream.App/UI/Layout/ExternalContainerController.cs b/src/AcDream.App/UI/Layout/ExternalContainerController.cs index d7bf4460..db66e5ed 100644 --- a/src/AcDream.App/UI/Layout/ExternalContainerController.cs +++ b/src/AcDream.App/UI/Layout/ExternalContainerController.cs @@ -363,6 +363,7 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine SpriteResolve = owner.SpriteResolve, SlotIndex = owner.GetNumUIItems(), SourceKind = source, + TooltipTextResolve = g => _objects.Get(g)?.GetTooltipDisplayName(), }; cell.SetItem(guid, icon, dragIconTexture: dragIcon); return cell; diff --git a/src/AcDream.App/UI/Layout/InventoryController.cs b/src/AcDream.App/UI/Layout/InventoryController.cs index f39b8e83..320a6eec 100644 --- a/src/AcDream.App/UI/Layout/InventoryController.cs +++ b/src/AcDream.App/UI/Layout/InventoryController.cs @@ -444,7 +444,11 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo { const uint PlayerPackBaseIcon = 0x0600127Eu; // constant main-pack backpack (visual gate) _topContainer.Flush(); - var main = new UiItemSlot { SpriteResolve = _topContainer.SpriteResolve }; + var main = new UiItemSlot + { + SpriteResolve = _topContainer.SpriteResolve, + TooltipTextResolve = g => _objects.Get(g)?.GetTooltipDisplayName(), + }; main.SetItem( p, _iconIds(ItemType.Container, PlayerPackBaseIcon, 0u, 0u, 0u), @@ -486,7 +490,11 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo uint dragTex = item is null ? 0u : _dragIconIds?.Invoke( item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects) ?? 0u; - var cell = new UiItemSlot { SpriteResolve = list.SpriteResolve }; + var cell = new UiItemSlot + { + SpriteResolve = list.SpriteResolve, + TooltipTextResolve = g => _objects.Get(g)?.GetTooltipDisplayName(), + }; cell.SetItem(guid, tex, dragIconTexture: dragTex); cell.SetWaitingState(waiting); cell.SlotIndex = list.GetNumUIItems(); // index it will occupy (== its slot in a packed list) diff --git a/src/AcDream.App/UI/Layout/PaperdollController.cs b/src/AcDream.App/UI/Layout/PaperdollController.cs index 9d84f10b..4d925ed9 100644 --- a/src/AcDream.App/UI/Layout/PaperdollController.cs +++ b/src/AcDream.App/UI/Layout/PaperdollController.cs @@ -88,6 +88,7 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo list.ExamineItemRequested = ExamineItem; list.Cell.SourceKind = ItemDragSource.Equipment; list.Cell.SlotIndex = i; // definition position = equipped drag-payload SourceSlot + list.Cell.TooltipTextResolve = g => _objects.Get(g)?.GetTooltipDisplayName(); list.Cell.EmptySprite = emptySlotSprites is not null && emptySlotSprites.TryGetValue(element, out uint authoredSprite) ? authoredSprite diff --git a/src/AcDream.App/UI/Layout/SecureTradeUiController.cs b/src/AcDream.App/UI/Layout/SecureTradeUiController.cs index b03fc02b..091a5574 100644 --- a/src/AcDream.App/UI/Layout/SecureTradeUiController.cs +++ b/src/AcDream.App/UI/Layout/SecureTradeUiController.cs @@ -295,6 +295,7 @@ public sealed class SecureTradeUiController : IRetainedPanelController // Your staged items carry retail's trading marker. ShowTradeOverlay = side == RuntimeTradeSide.Self, TradeOverlaySprite = TradeOverlaySpriteId, + TooltipTextResolve = g => _bindings.Objects.Get(g)?.GetTooltipDisplayName(), }; cell.SetItem(guid, icon); list.AddItem(cell); diff --git a/src/AcDream.App/UI/Layout/ToolbarController.cs b/src/AcDream.App/UI/Layout/ToolbarController.cs index 9d880d84..e0b0c010 100644 --- a/src/AcDream.App/UI/Layout/ToolbarController.cs +++ b/src/AcDream.App/UI/Layout/ToolbarController.cs @@ -145,6 +145,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont list.Cell.SlotIndex = i; list.Cell.SourceKind = ItemDragSource.ShortcutBar; list.Cell.DragAcceptSprite = 0x060011FAu; // green cross (toolbar), not the ring 0x060011F9 (inventory) + list.Cell.TooltipTextResolve = g => _repo.Get(g)?.GetTooltipDisplayName(); } } diff --git a/src/AcDream.App/UI/Layout/VendorUiController.cs b/src/AcDream.App/UI/Layout/VendorUiController.cs index b1edcc8b..b1ddf49e 100644 --- a/src/AcDream.App/UI/Layout/VendorUiController.cs +++ b/src/AcDream.App/UI/Layout/VendorUiController.cs @@ -1102,6 +1102,10 @@ public sealed class VendorUiController : IRetainedPanelController, IItemListDrag // comment for why this must be gated at the source, // not left to every destination handler to reject. AllowDragSource = false, + // Shop items are materialized into ClientObjectTable + // (VendorShopItemMaterializer), so the same resolver + // every other physical cell uses works here too. + TooltipTextResolve = g => _objects.Get(g)?.GetTooltipDisplayName(), }; cell.SetItem(item.ItemGuid, icon); cell.Selected = item.ItemGuid == selectedGuid; @@ -2205,6 +2209,7 @@ public sealed class VendorUiController : IRetainedPanelController, IItemListDrag SpriteResolve = list.SpriteResolve, SlotIndex = list.GetNumUIItems(), AllowDragSource = false, + TooltipTextResolve = g => _objects.Get(g)?.GetTooltipDisplayName(), }; cell.SetItem(shopItem.ItemGuid, icon); cell.Selected = shopItem.ItemGuid == selectedGuid; @@ -2243,6 +2248,7 @@ public sealed class VendorUiController : IRetainedPanelController, IItemListDrag SpriteResolve = list.SpriteResolve, SlotIndex = list.GetNumUIItems(), AllowDragSource = false, + TooltipTextResolve = g => _objects.Get(g)?.GetTooltipDisplayName(), }; cell.SetItem(item.ObjectId, icon); cell.Selected = item.ObjectId == selectedGuid; diff --git a/src/AcDream.App/UI/UiItemSlot.cs b/src/AcDream.App/UI/UiItemSlot.cs index 614ec3b6..8a06c830 100644 --- a/src/AcDream.App/UI/UiItemSlot.cs +++ b/src/AcDream.App/UI/UiItemSlot.cs @@ -12,13 +12,63 @@ namespace AcDream.App.UI; /// public class UiItemSlot : UiElement { - public UiItemSlot() { ClickThrough = false; } + /// + /// Retail's shared UIItem cell-template catalog (ItemListCellTemplate. + /// CatalogLayoutId, LayoutDesc 0x21000037) 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 0x1000033A, the toolbar's per-slot prototypes + /// 0x1000043B.., the container cell 0x1000033F, and every + /// paperdoll/armor slot skin alike) resolves P0x47=0x10000395 / + /// P0x48=0x21000041 through catalog inheritance, matching one of + /// the four popup skins already + /// mounts for every other tooltip-bearing element + /// (Layout.TooltipLiveDatTests.PopupSkinRootIds). Since + /// cells are built programmatically (never through + /// LayoutImporter.Build), this port hardcodes the uniform pair here + /// rather than re-deriving it per instance — the same "exhaustive scan, + /// then hardcode" shape as RetailCursorCatalog's five window-control + /// cursor DIDs and ItemListCellTemplate.CatalogLayoutId itself. + /// + private const uint ItemTooltipRootElementId = 0x10000395u; + private const uint ItemTooltipLayoutDid = 0x21000041u; + + public UiItemSlot() + { + ClickThrough = false; + AuthoredTooltipRootElementId = ItemTooltipRootElementId; + AuthoredTooltipLayoutDid = ItemTooltipLayoutDid; + } public override bool ConsumesDatChildren => true; /// Bound weenie guid (0 = empty). Retail UIElement_UIItem::itemID. public uint ItemId { get; private set; } + /// + /// Resolves to its retail tooltip text (a + /// + /// call bound by the owning controller — every construction site already + /// has a ClientObjectTable reference in scope, matching how + /// is wired). Null/empty result shows no + /// tooltip, matching retail's UIItem_Update early-out + /// (weenObj == 0 -> UIElement::ClearTooltip) for an + /// empty or not-yet-materialized cell. + /// + public Func? TooltipTextResolve { get; set; } + + /// + /// Port of UIElement_UIItem::UpdateTooltip @0x004E1CB0: 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 and + /// — rather than cached at + /// time, since nothing observes a stale value between + /// item-state changes and the next hover dwell. + /// + public override string? GetTooltipText() + => ItemId != 0 ? TooltipTextResolve?.Invoke(ItemId) : null; + /// Pre-composited icon GL texture for the bound item (0 = none). public uint IconTexture { get; private set; } diff --git a/src/AcDream.Core/Items/ClientObject.cs b/src/AcDream.Core/Items/ClientObject.cs index 5f4611d0..42a99890 100644 --- a/src/AcDream.Core/Items/ClientObject.cs +++ b/src/AcDream.Core/Items/ClientObject.cs @@ -347,6 +347,22 @@ public sealed class ClientObject if (string.IsNullOrEmpty(Name)) return Name; return Name[^1] == 's' ? Name + "es" : Name + "s"; } + + /// + /// Ports UIElement_UIItem::UpdateTooltip @0x004E1CB0 — the item-cell + /// hover-tooltip text every physical item/container/shortcut cell shows. + /// Retail resolves the name with NAME_APPROPRIATE (the same call + /// already ports) and, only when the stack + /// holds more than one (_stackSize_1 > 1 @0x004e1d12), prefixes the + /// count via PStringBase<unsigned short>::sprintf(&__return, + /// u"%d %s") — count first, one space, then the (already + /// singular-or-plural) name. + /// + public string GetTooltipDisplayName() + { + string name = GetAppropriateName(); + return StackSize > 1 ? $"{StackSize} {name}" : name; + } } ///