feat(ui): #409 — client-wide retail tooltip system

Full re-derivation from named-retail decomp: UIElement::StartTooltipAtMouse
@0x00460D70 -> UIElementManager::StartTooltip @0x0045DE90/@0x00459700,
UIElement::MouseHover @0x00462520 (P0x4B TooltipOn gate + global
m_tooltipEnable), UIElementManager::CheckTooltip @0x0045B6E0 (dwell/
auto-hide timer, default 0.25s/10s), SwitchMouseOver/DeletingElement
(dismissal). Corrects the earlier GF-16 investigation: P0x47 is the
element-desc id WITHIN the popup LayoutDesc (P0x48), not a "behavior
enum"; P0x4A is read off the popup's own instantiated root, not the
trigger element.

- ElementInfo/UiElement gain six tooltip data fields (P0x47/48/49/4A/4B/50),
  read generically by ElementReader and copied through LayoutImporter,
  mirroring the existing AuthoredInvisible passthrough pattern.
- UiRoot's existing CheckTooltip-derived hover timer gains TooltipShow/
  TooltipHide events, a per-element P0x50 delay override, and dismissal
  wiring at every retail-confirmed teardown site.
- RetailTooltipPresenter (owned by RetailUiRuntime, mounted alongside
  RetailDialogFactory) builds the popup via the existing LayoutImporter
  dat-lock seam, auto-resizes by the measured-vs-authored text delta
  (word-wrapped via the existing UiText.WrapWords primitive), positions
  at the mouse clamped to the display, and stays topmost over dialogs via
  its own later per-tick BringToFront (register AD-106).
- Misc.TooltipEnable/Misc.TooltipDelay are client-local UserPreferences
  (retail's own 2013 Config tab authors no visible row for either) —
  SettingsStore gains a MiscSettings section, no new options-panel row.
- Live-DAT sweep: 434 elements author >=1 trigger property (243 with
  literal text this port shows; 191 rely on retail's dynamic
  InqProperty(0x49) override, deferred as register TS-85 alongside the
  unmodeled P0x3D wrap-width override).

Gates: Release build 0 errors; App suite (live-DAT env) 5410/5407 passed/
3 skipped (was 5379/3); Runtime 1735/0 unchanged; UI.Abstractions 926/0;
full solution 14,617/14,548 passed/69 skipped/0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-16 20:03:24 +02:00
parent d3755eb231
commit a377b9bff7
16 changed files with 1386 additions and 9 deletions

View file

@ -107,8 +107,75 @@ 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:** OPEN
**Severity:** LOW-MEDIUM (cosmetic/discoverability — no gameplay impact, but retail shows a tooltip on hover for ~253 authored elements client-wide and acdream shows none)
**Status:** CODE-COMPLETE 2026-08-16 (this commit) — pending the user's connected visual gate (see the gate note at the bottom of this entry).
**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 (this commit).** Full re-derivation from
`docs/research/named-retail/acclient_2013_pseudo_c.txt` corrected two things
the original GF-16 filing below got wrong from a shallower pass: **`P0x47`
is NOT a "tooltip behavior enum" — it is the element-desc id WITHIN the
popup LayoutDesc (`P0x48`) to instantiate as the popup's root**
(`UIElementManager::StartTooltip @0x0045DE90` passes it straight to
`LayoutDesc::AccessElementDesc`), and **`P0x4A` is read off the freshly
INSTANTIATED popup's own root element, not the hovering trigger element**
(it names that popup's text-child id). A live-DAT sweep (installed EoR
build) found **434 elements author at least one of the five trigger
properties** (243 with literal `P0x49` `StringInfo` text this port shows;
the other 191 rely on retail's DYNAMIC `InqProperty(0x49)` text override,
out of scope — see register row TS-85), superseding the original "~253"
estimate. Layout `0x21000041` holds four 30x30 popup skins
(`0x10000487`/`0x10000395`/`0x10000397`/`0x10000398`), each a four-piece
bevel frame around one shared Type-12 text child `0x10000396` — confirmed
by `TooltipLiveDatTests`.
**Shipped:** the six-property data layer (`ElementInfo`/`UiElement`
`Tooltip*`/`AuthoredTooltip*` fields, `ElementReader`,
`DatWidgetFactory.ResolveTooltipText`, `LayoutImporter.BuildWidget`); the
hover-dwell/auto-hide/dismissal state machine (`UiRoot.Tick`'s existing
`CheckTooltip` port gained `TooltipShow`/`TooltipHide` C# events, a
per-element `P0x50` delay-override consult, and dismissal wiring at every
retail-confirmed teardown site — hover-target change, owner-element
removal, duration timeout); `RetailTooltipPresenter` (owned by
`RetailUiRuntime`, mounted alongside `RetailDialogFactory`) — builds the
popup via the SAME `LayoutImporter`/dat-lock seam dialogs use, auto-resizes
by the retail measured-vs-authored-text delta (word-wrapped at the display
width via the existing `UiText.WrapWords` primitive), positions at the
mouse cursor clamped to the display, keeps itself topmost over dialogs via
its own later per-tick `BringToFront` (register AD-106), and gates on both
the global `Misc.TooltipEnable` preference (client-local, `SettingsStore`'s
new `MiscSettings` section — retail's OWN 2013 Config tab authors no
visible row for it either, confirmed by the OP campaign's own research, so
no new options-panel row was added) and the widget's own `P0x4B`. **No
click-dismissal was ported** — `UIElementManager::MouseDownEvent
@0x0045DB60` calls the SAME `SwitchMouseOver` hover-change check that
already drives dismissal, and it no-ops when the hit-tested element hasn't
changed, so retail itself does not dismiss a tooltip by clicking its own
owner.
**Deferred (register TS-85):** dynamic per-element tooltip text
(`InqProperty(0x49)`, 191 elements) and the `P0x3D` per-element wrap-width
override (zero elements author one today). **Deferred (register AD-106):**
the topmost-z-order mechanism is a sibling-with-later-reraise adaptation,
not retail's literal separate presentation layer.
**Gate note (5-10 min, `ACDREAM_RETAIL_UI=1`):** hover the mouse over any
of these and hold still — a small tooltip box should appear after a brief
pause (~0.25 s, matching retail's registered default) and disappear when
you move to a different control: (1) chargen Appearance page — the rotate
arrows beside the preview ("Rotate left."/"Rotate right.") and any color
swatch ("Changes color of selected clothing or body part."); (2) the same
page's hair/eyes/nose spin arrows (longer help text — should WRAP across
multiple lines, not run off-screen); (3) any other screen with a tooltip
you recall from retail. Confirm: the box sits right at the cursor (not
offset), 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.
---
**Original GF-16 filing (superseded by the re-derivation above; kept for
investigation history).**
Found during Campaign CC gate round 1's Batch D root-cause investigation
(`docs/research/2026-08-16-campaign-cc-gate-round1-findings.md`, GF-16

File diff suppressed because one or more lines are too long