docs: hover-feedback completion round — #409/#411 bookkeeping, TS-85 narrowed

Records the round that shipped item-cell tooltips, the world-object
hover tooltip, and the #411 cursor-swap fix: #409's write-up gains a
"hover-feedback completion round" section covering all three items
with live-verification notes; #411 is closed with the corrected
decomp reading; register row TS-85 is narrowed to reflect the two
newly-ported SetTooltip call sites (UIElement_UIItem::UpdateTooltip,
UIElement_SmartBoxWrapper::RecvNotice_SmartBoxObjectFound) and the
still-open ones (spellcasting endowment/cast-button/favorite/submenu,
map notes, character-panel attribute/skill info).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-16 23:02:50 +02:00
parent aed423174b
commit 708e35f610
2 changed files with 126 additions and 9 deletions

View file

@ -26,13 +26,33 @@ What does NOT go here:
## #411 — Hover feedback over interactive UI elements: no cursor swap, and item cells have no rollover state
**Status:** OPEN (filed 2026-08-16 during the #409 live-failure investigation, at the
lead's scope addition; user report: "the cursor should light up when I hover over an
item in inventory. it does not")
**Status:** CLOSED 2026-08-16 at the #409 hover-feedback completion round — the
user's answer to the open question below ("the POINTER changes, like it already
does over world NPCs") is CONFIRMED by decomp, not merely a memory to trust:
`UIElement_SmartBoxWrapper::FindObject @0x004E5430` calls
`SmartBox::set_found_object(itemID)` UNCONDITIONALLY whenever the hovered UI
element casts to `UIElement_UIItem` — not gated on target mode, as the port plan
below (written before this finding) assumed it would need to be. Fixed with a
ONE-LINE widening of `CursorFeedbackController.Update(UiRoot)`'s existing
(too-narrow) item-hover special case; `ResolveGlobalKind` needed no changes,
since it already read the "found" flag unconditionally across every mode. See
docs/ISSUES.md #409's own "hover-feedback completion round" write-up (item 3)
for the full derivation and `CursorFeedbackControllerTests.
UpdateFromRoot_HoveringAnItemSlot_ShowsFoundCursor_In{OrdinaryPeaceMode,CombatMode}`
for the pin. The rollover-STATE half of this investigation (item 3 below,
`UiItemSlot` has no `HoverEnter`/`HoverLeave`) was NOT in scope for the pointer
question and remains unaddressed if the user separately wants the highlight —
file a fresh issue if so; this closure covers only the pointer-swap question the
lead's scope addition asked about.
**Severity:** LOW (cosmetic/affordance; no gameplay impact)
**Depends on:** nothing — the hover dispatch it needs is already correct (see #409's
live-failure round, which proved `UiRoot.UpdateHover` selects the right widget).
**Original investigation (below), kept for its still-valid layer 1/2/3 breakdown —
only the "never fires over an inventory item" conclusion for layer 2 was
incomplete; see the closure note above for the corrected reading.**
**Retail mechanism, derived from `docs/research/named-retail/acclient_2013_pseudo_c.txt`.**
There are THREE separate hover-feedback layers, and the DAT decides which one applies:
@ -188,7 +208,7 @@ horizontal `HJustify` mapping while in this code, since it shares the
## #409 — Client-wide UI tooltip system is unshipped (GF-16, deferred out of Campaign CC gate round 1)
**Status:** CODE-COMPLETE 2026-08-16; review-fix round F1-F11 and the LIVE-FAILURE round both landed same day. The live-failure round's own fix is LIVE-VERIFIED (Options -> Character tab tooltip observed on a real connected client, screenshot evidence); the user's full connected gate is still owed.
**Status:** CODE-COMPLETE 2026-08-16; review-fix round F1-F11, the LIVE-FAILURE round, and the hover-feedback completion round (item-cell tooltips + world-object hover tooltip) all landed same day. The live-failure round's own fix is LIVE-VERIFIED (Options -> Character tab tooltip observed on a real connected client, screenshot evidence); the hover-feedback completion round's three items are automated-gate-verified (unit + live-DAT) but the user's connected gate for THOSE items specifically is still owed — see that round's own "Live-verify all three" note.
**Severity:** LOW-MEDIUM (cosmetic/discoverability — no gameplay impact, but retail shows a tooltip on hover for authored elements client-wide and acdream showed none before this fix)
**2026-08-16 re-derivation + port.** Full re-derivation from
@ -353,16 +373,113 @@ the "Slots" button.
spin arrows — longer text, should WRAP rather than run off-screen; (7) the
Heritage/Profession/Skills/Town/Summary tab buttons.
*Expected NOT to show anything yet (deferred, register TS-85):* hovering an
inventory ITEM icon. Retail shows the item name there
(`UIElement_UIItem::UpdateTooltip @0x004E1CB0`); acdream shows nothing.
Confirm also: the box sits offset down-right of the cursor (retail's +32px on
both axes), never runs off the edge of the window even near a corner, and
disappears on its own after ~10 s if you hold still without moving away. No
click-to-dismiss is expected — only moving off the control, or a very long
hold, closes it.
**2026-08-16 hover-feedback completion round.** Closes the two items the
live-failure round explicitly deferred (item-cell tooltips, and the #411
pointer question), plus the world-object hover tooltip the user's gate notes
called out separately.
1. **Inventory/shortcut/paperdoll item-name tooltips — SHIPPED.**
`UIElement_UIItem::UpdateTooltip @0x004E1CB0` is called from
`UIItem_Update` (an item-DATA-CHANGE refresh, not a hover handler — the
trigger that actually SHOWS it is the generic `CheckTooltip` dwell timer,
same as any other tooltip-bearing element). Re-derived and closed the gap
the live-failure round left open ("acdream's `UiItemSlot` carries neither
the `P0x47` popup locator nor a name source"): 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
(`TooltipLiveDatTests.UiItemCatalog_EveryPrototype_SharesTheSamePopupLocator`).
`UiItemSlot` now hardcodes that pair and exposes `GetTooltipText()` via a
new per-instance `TooltipTextResolve` delegate, wired at every physical-
item construction site — `InventoryController` (main-pack cell + grid
cells), `ExternalContainerController`, `PaperdollController` (closes the
`gmPaperDollUI::UpdateItemSlotTooltip @0x004A52EF` row too — same cell
class, same fix), `VendorUiController` (shop/buying/selling lists),
`SecureTradeUiController`, `ToolbarController`. Text is
`ClientObject.GetTooltipDisplayName()` (new Core method): `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`.
2. **World-object hover tooltip (NPCs, players, signs, chests, portals) —
SHIPPED.** NOT the UI-element dwell-timer path — retail's mechanism is
`UIElement_SmartBoxWrapper::RecvNotice_SmartBoxObjectFound @0x004E5AD0`,
fed every frame by `FindObject @0x004E5430`/`Global_Loop @0x004E5620`
using the current mouse position regardless of input focus. It fires
IMMEDIATELY (no dwell wait) on the found-object id CHANGING, gated by the
`PlayerModule::ShowTooltips` character option (`CharacterOptionId.ShowTooltips`
— already modeled in `CharacterOptionTable`, default true), with text
`ACCWeenieObject::GetObjectName(id, NAME_APPROPRIATE, 0)` — the SAME name
call as item tooltips, but WITHOUT the item-cell's separate stack-count
prefix (a real, decomp-confirmed asymmetry: a ground pile of arrows shows
"Arrows", not "20 Arrows"). Ported as `RetailTooltipPresenter.
UpdateWorldHoverTooltip`, driven by the SAME world-hover pick
`CursorFeedbackController`'s own found-cursor already uses
(`WorldSelectionQuery.PickAtCursor`, `includeSelf: true`) and the SAME
`ClientObjectTable`-backed name resolver `SocialAllegiancePageController`'s
`ResolveWorldObjectName` already established as this codebase's pattern.
Queried only when no UI element is hovered (this port's reading of
`FindObject`'s `m_pElementLastOver` check, narrowed from retail's literal
"raycast even under non-item UI chrome" — see the class's own doc note).
**Own player is included** (`includeSelf: true`, the same precedent the
cursor feedback wiring already set) — no decomp evidence was found either
confirming or excluding self from the found-object pipeline, so this
follows the established local precedent rather than guessing fresh; flag
if that reads wrong in the live gate. **The exact popup skin is an
inference, not a measured value** — an exhaustive live-DAT sweep found
`UIElement_SmartBoxWrapper` (class `0x10000030`) has NO authored
`ElementDesc` anywhere installed (unlike every other tooltip trigger, it
is evidently constructed directly by `gmGamePlayUI`'s own mode setup, not
from a walkable LayoutDesc — `TooltipLiveDatTests.
SmartBoxWrapper_HasNoAuthoredElementDesc_AnywhereInstalled`), so its real
`P0x47`/`P0x48` cannot be read off the DAT the way the item catalog's can.
This port reuses the SAME pair every other game-code `SetTooltip` caller
in this family resolves to — the best-evidenced choice, called out in
register row TS-85 rather than silently assumed exact.
3. **#411 resolved: retail DOES swap the pointer over inventory items,
unconditionally — the earlier investigation's "never fires over an
inventory item" finding was INCOMPLETE, not wrong about what it checked.**
The original #411 scan (below) correctly found no PER-ELEMENT authored
cursor on item cells and correctly found `SmartBox::get_found_object_id()`
is written only by `UIElement_SmartBoxWrapper` — but it had not yet traced
`FindObject @0x004E5430` far enough: when the currently-hovered UI element
(`m_pElementLastOver`) casts to `UIElement_UIItem` (class `0x10000032`),
`FindObject` calls `SmartBox::set_found_object(itemID, 0xFFFFFFFF)`
directly and returns WITHOUT running the 3D raycast — UNCONDITIONALLY, not
gated on target mode. `ClientUISystem::UpdateCursorState @0x00564630`
computes its "found" flag ONCE at the top of the function
(`ebx = SmartBox::get_found_object_id() != 0`, `@0x00564642`) and every
later branch (default/melee-missile/magic/use/examine/use-target/busy)
reads that SAME flag — so hovering an occupied item cell shows the
cursor's "...Found" variant in EVERY mode, not only during an active
`UseTarget` selection. `CursorFeedbackController.Update(UiRoot)` already
had the item-hover special case wired (from an earlier round) but
incorrectly gated it to `TargetMode.UseTarget` only; that one-line gate is
now removed — `ResolveGlobalKind`'s existing found/not-found branching
needed no changes at all, since it already read the snapshot's
`HoverTargetGuid` unconditionally across every mode. Live-DAT-independent
(pure decomp + unit fixture), so no DAT sweep was needed for this part;
two new `CursorFeedbackControllerTests` pin the widened behavior in
ordinary peace mode and in combat mode.
**Live-verify all three on the connected client** (session-config launch,
graceful close per the usual rules): hover an inventory item — a name
tooltip should appear (with a count prefix for a stack) AND the mouse
pointer should swap to its "found" variant; hover an NPC/creature — a name
tooltip should appear immediately (no perceptible delay) if "Show Tooltips"
is on; hover a sign/chest/portal similarly.
---
**Original GF-16 filing (superseded by the re-derivation above; kept for