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:
parent
5f9ca18155
commit
e29c61a3a4
8 changed files with 87 additions and 3 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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> -> <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; }
|
||||
|
||||
|
|
|
|||
|
|
@ -347,6 +347,22 @@ public sealed class ClientObject
|
|||
if (string.IsNullOrEmpty(Name)) return Name;
|
||||
return Name[^1] == 's' ? Name + "es" : Name + "s";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ports <c>UIElement_UIItem::UpdateTooltip @0x004E1CB0</c> — the item-cell
|
||||
/// hover-tooltip text every physical item/container/shortcut cell shows.
|
||||
/// Retail resolves the name with <c>NAME_APPROPRIATE</c> (the same call
|
||||
/// <see cref="GetAppropriateName"/> already ports) and, only when the stack
|
||||
/// holds more than one (<c>_stackSize_1 > 1 @0x004e1d12</c>), prefixes the
|
||||
/// count via <c>PStringBase<unsigned short>::sprintf(&__return,
|
||||
/// u"%d %s")</c> — count first, one space, then the (already
|
||||
/// singular-or-plural) name.
|
||||
/// </summary>
|
||||
public string GetTooltipDisplayName()
|
||||
{
|
||||
string name = GetAppropriateName();
|
||||
return StackSize > 1 ? $"{StackSize} {name}" : name;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue