Commit graph

7 commits

Author SHA1 Message Date
Erik
9d9280a069 fix(ui): morning gate — world tooltips ride retail's mouse-idle dwell, not the found edge
User finding 1 (side-by-side vs retail): our world-object tooltips popped
the instant the found object changed; retail's "lag". The night round's
derivation from RecvNotice_SmartBoxObjectFound @0x004E5AD0 misread the
notice as edge-MOUNTING: its immediate StartTooltipAtMouse @0x004E5DFB is
inside `if (s_pInstance->m_dragElement != 0)` (@0x004E5D8E) — and
m_dragElement is a real, distinct PDB field in acclient.h's
UIElementManager (separate from the m_pTooltipElement family), so the
immediate mount is DRAG-AND-DROP ONLY. The ordinary hover path merely
STAGES the name (SetTooltip @0x004E5D74 + the |=0x20 TooltipOn bit) and
the display rides the SAME UIElementManager::CheckTooltip @0x0045B6E0
mouse-idle dwell as UI tooltips: 250 ms (m_tooltipDelay @0x0045f75d)
since m_lastMouseMoveTime (stamped on EVERY move, MouseMoveHandler
@0x0045e736). Found swaps under an IDLE mouse replace the popup the same
frame (SetTooltip's own text-change teardown @0x004617FF -> ResetTooltip
@0x0045C360 tail-calling CheckTooltip); the 10 s duration expiry
(@0x0045b78a) requires a fresh mouse move before re-arming
(SwitchMouseOver(null) @0x0045b7b2 clears m_pElementLastEntered).

Port: UiRoot gains the unconditional last-mouse-move stamp
(m_lastMouseMoveTime 1:1 — the existing _hoverStartedMs stamps are
deliberately conditional) exposed as MouseIdleMs/NowMs;
RetailTooltipPresenter.UpdateWorldHoverTooltip now stages text at the
notice edge (ShowTooltips gate + name resolve read there, @0x004E5D21/
@0x004E5D3B, empty-name SetTooltip skip @0x004E5D48 included) and mounts
via the CheckTooltip dwell block (no-capture gate @0x0045b715,
m_tooltipEnable via MouseHover @0x0046254C — which the drag-immediate
branch faithfully bypasses). Session reset also forgets the staged text.

Tests: the world-hover fixture section rewritten to the corrected model —
found edge stages but never mounts before the dwell; a continuously
moving mouse never mounts until it rests; idle found-swap replaces
same-frame without stacking; duration auto-hide needs a move + fresh
dwell to remount; drag-in-progress mounts immediately. 38/38 pass.

Register TS-85 and ISSUES item 2 corrected honestly: the "edge-fired
(no dwell)" conclusion is superseded by the user's retail evidence and
the m_dragElement branch read.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 08:49:11 +02:00
Erik
ab84b54dfa fix(ui): night-round review — F5/F6 structural single-tooltip invariant
F5: moved the unconditional RemovePopup() call into
RetailTooltipPresenter.TryBuildAndMountPopup itself so the single-
popup invariant (retail's own single m_pTooltipElement slot) is
enforced structurally rather than relying on every caller to have
already cleared a stale popup. Closes a real hole: UpdateWorldHoverTooltip's
own clear is gated on _worldTooltipShowing (only true when the WORLD
path itself mounted the current popup), and its "a UI popup cannot be
showing here" comment assumed the host's hover query is null whenever
that branch runs — an assumption that breaks the instant a modal opens
over a stationary cursor. UiRoot.Modal claims EXCLUSIVE hit-testing, so
Pick(MouseX, MouseY) can return null even though a UI-dwell tooltip is
still mounted underneath; UpdateWorldHoverTooltip would then mount a
second popup on top without ever clearing the first.

F6: fixed WorldHover_ThenUiDwellTooltip_ReplacesRatherThanStacks to
actually exercise the transition with a follow-up presenter.Tick()
(the old test only proved OnTooltipShow's own clear worked, never
checked the world-side bookkeeping after). Added
UiDwellTooltip_ThenModalStealsHitTesting_WorldHoverReplacesRatherThanStacks
for F5's own case, using UiRoot.Modal to reproduce the exclusive-hit-
testing hole precisely — empirically verified this new test fails
(2 popups instead of 1) with the structural RemovePopup() reverted,
confirming it is a real regression test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 04:45:49 +02:00
Erik
97a7be12ee fix(ui): world tooltips never cleared, stacking dozens of popups — single-slot invariant restored on every found-object edge
RetailTooltipPresenter.UpdateWorldHoverTooltip only called RemovePopup()
on the found-object-LOST edge (found == 0u). An A->B found-object CHANGE
(walking past a run of NPCs/doors/lifestones with no intervening "nothing
found" frame) skipped straight to TryBuildAndMountPopup with the previous
popup still mounted as a child of _host -- only the _popupRoot reference
got overwritten, so every earlier popup was orphaned in the tree and never
removed. Matches the user's screenshot of 15+ stacked name boxes.

Fix: clear any showing world popup on ANY found-object edge -- change or
loss -- before evaluating whether to mount a new one, mirroring
OnTooltipShow's own unconditional RemovePopup() at its top.

Live-verified against local ACE (testaccount/+Acdream, session-config
launch): a temporary probe logged 103 mount/102 remove events across many
direct object-to-object transitions (Silver Tusker, Armored Tusker,
+Acdream); hostChildren never exceeded baseline+1 and popupSkinChildren
never exceeded 1 -- confirmed at most one tooltip ever exists. Probe
stripped before landing; two new fixture regressions
(WorldHover_FoundObjectChangesDirectly_ReplacesThePopupWithoutStacking,
WorldHover_ThenUiDwellTooltip_ReplacesRatherThanStacks) both fail pre-fix.

fix #409 (follow-on)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 00:23:54 +02:00
Erik
fe1bc70753 feat(ui): world-object hover tooltip — UIElement_SmartBoxWrapper::RecvNotice_SmartBoxObjectFound port
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 (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 ground pile of arrows shows "Arrows", not
"20 Arrows" — a real, decomp-confirmed asymmetry).

Ported as RetailTooltipPresenter.UpdateWorldHoverTooltip, driven by
the SAME world-hover pick CursorFeedbackController's own found-cursor
already uses (WorldSelectionQuery.PickAtCursor, includeSelf: true —
own player is included on that precedent) and the SAME
ClientObjectTable-backed name resolver SocialAllegiancePageController's
ResolveWorldObjectName already established as this codebase's
pattern. New WorldTooltipRuntimeBindings threads it through
RetailUiRuntimeBindings; wired at InteractionRetainedUiComposition
alongside the existing cursorFeedback construction.

Queried only when no UI element is hovered — a narrowing from
retail's literal "raycast even under non-item UI chrome" (FindObject's
m_pElementLastOver check), called out in the class's own doc note as
a scoped interpretation rather than a byte-exact port.

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. This
port reuses the same P0x47=0x10000395/P0x48=0x21000041 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.

Live-verified against a connected ACE session (session-config launch,
+Acdream): hovering a "Silver Tusker" near spawn mounted the correct
popup text and simultaneously flipped the cursor to its DefaultFound
variant, confirming the shared found-object pipeline drives both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 23:02:29 +02:00
Erik
5f9ca18155 fix(ui): #409 live-failure round — tooltips read the RUNTIME text first
User gate on 1.0.3-tt.a: tooltips appeared NOWHERE in-world except one on
the paperdoll. Root-caused, fixed, and live-verified against a connected
client the same day. Two findings, both measured; neither is a broken
hover/hit-test.

1. DOMINANT ROOT CAUSE — RetailTooltipPresenter.OnTooltipShow gated on
   widget.AuthoredTooltipText (P0x49) alone. Retail's
   UIElement::StartTooltipAtMouse @0x00460D70 takes the RUNTIME m_TTText
   first (@0x00460DA3 IsValid -> @0x00460DAA verbatim) and only falls back
   to InqProperty(0x49) at @0x00460DDF. acdream ALREADY had the runtime
   layer — UiElement.GetTooltipText(), written by the Options/Chat/Config
   page controllers, KeyboardConfigController, the social pages and
   UiCheckboxBitfield64 — but nothing read it.

   Live-DAT measured: the Options toggle-row checkbox (0x2100002B template
   root 0x10000218, leaf 0x10000219) authors P0x47=0x10000397
   P0x48=0x21000041 P0x4B=true and an EMPTY P0x49 — the popup locator and
   the on-bit are authored; only the text arrives at runtime, exactly as
   UIOption_CheckboxBitfield64::CreateChildren @0x00485E65 stamps its
   siTooltip array. Re-measured client-wide: ALL 187 no-literal-text
   tooltip elements author both locator ids, i.e. the whole set is
   runtime-text targets.

   Fixed by ResolveTooltipText (retail's order), plus:
   - the P0x4B gate now applies only to the AUTHORED-text path, because
     retail's eight game-code SetTooltip sites set the on-bit themselves
     (__bitfield164 |= 0x20 at @0x004E1D5E/@0x004A52F4/@0x004C63AC/
     @0x004C67ED/@0x004C7000/@0x004C7218/@0x004D9617/@0x00467076);
   - the P0x48-absent fallback to the element's own LayoutDesc
     (@0x00460E7E, this->m_layout->m_DID) is ported via the new
     UiElement.SourceLayoutDid, threaded from LayoutImporter.Build's new
     sourceLayoutDid parameter and passed by Import + the four template
     resolvers.

2. THE "243 SHOWABLE" NUMBER WAS NEVER AN IN-WORLD NUMBER. Grouped
   re-sweep: all 243 sit in CHARACTER-CREATION layouts. The inventory
   window (0x21000023) and paperdoll (0x21000024) author exactly two
   between them — 0x100001D6 "Drag clothing and armor here to wear them"
   (the doll drag mask) and 0x100005BE (the Slots button). The first IS
   the user's single working tooltip, so the paperdoll was never a
   differential against a broken mechanism. Reachability was measured and
   is fine: 238/243 build as real non-ClickThrough hover targets.

LIVE VERIFICATION (connected testaccount/+Acdream, Release,
ACDREAM_RETAIL_UI=1): Options -> Character -> "Vivid Targeting Indicator"
now shows its full ID_PlayerOption_*_Help sentence; a temporary hover probe
confirmed the hover target is element 0x10000219 with runtime=True. The
paperdoll tooltip still shows. An inventory ITEM still shows nothing —
that is UIElement_UIItem::UpdateTooltip @0x004E1CB0 (retail shows the item
name, "%d %s"-prefixed when the stack is > 1), which stays deferred:
UiItemSlot is constructed programmatically at 6+ sites and carries neither
the P0x47 locator nor a name source, so it is its own slice.

Bookkeeping: register TS-85 narrowed (m_TTText READ side now ported; the
row now enumerates all 15 SetTooltip call sites split into ported vs
no-acdream-analog). #409's gate note rewritten to lead with the in-world
surfaces — the old note listed only chargen, which is why it could not
have caught this. Filed #411 for the hover-cursor scope addition: an
exhaustive raw scan of every ElementDesc found only 101 authored
MediaDescCursor entries, all on Dragbar/Resizebar with the 5 DIDs
RetailCursorCatalog already hardcodes, so retail has NO per-element cursor
for inventory items; the likely mechanism is the rollover STATE
(UIElement::MouseOverTop @0x004615D0) that UiItemSlot lacks entirely.

Gates: Release build 0 errors; App suite (live-DAT env) 5424/5421 passed/3
skips (was 5416/5413/3, +8 new tests); Runtime 1735/0; full solution (no
env) 14,631/14,561 passed/70 skipped/0 failed (was 14,623/14,554/69).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 21:44:44 +02:00
Erik
2719782dc0 fix(ui): #409 tooltip review fix round — F1-F11
Opus review of a377b9bf returned architectural PASS-with-findings /
retail-fidelity FAIL with F1-F12 (F12 info-only). All eleven fixed,
each re-derived against docs/research/named-retail/acclient_2013_pseudo_c.txt:

- F1 PositionAtMouse: retail offsets BOTH axes +32px before the clamp
  (StartTooltip @0x00459700, @0x00459739/@0x00459747) — was landing
  flush at the cursor.
- F2 UiRoot: the dwell timer now anchors to mouse-IDLE like retail's
  m_lastMouseMoveTime (MouseMoveHandler @0x0045E710), resetting on
  every move within the same widget while !_tooltipFired, not just on
  hover-enter.
- F3 register TS-85 rewritten: the "dynamic InqProperty(0x49) override"
  framing was false — UIElement::InqProperty @0x004638D0's base impl
  reads the same authored bags this port already reads. The real
  second text source (m_TTText/SetTooltip, headed by the P0xD0
  truncated-text auto-tooltip @0x00466F80) needs a per-line-position
  truncation model UiText doesn't have — sized disproportionate for
  this round and left honestly deferred rather than stubbed.
- F4 OnTooltipShow: null LayoutPolicy + Anchors=None on the popup root
  and text child before resizing, mirroring RetailMessageDialogView's
  sibling shape.
- F5 OnTooltipShow: return without mounting when the P0x4A text child
  doesn't resolve to a UiText (retail's DynamicCast gate,
  StartTooltip @0x0045DE90 @0x0045df65/@0x0045df6f) — was mounting an
  empty 30x30 bevel artifact.
- F6 UiRoot.Tick: the dwell-arm branch now requires Captured is null
  (CheckTooltip @0x0045B6E0 @0x0045b715) — a widget hovered before a
  drag/resize/capture began must not pop mid-gesture.
- F7 UiRoot.ReleaseCapture: no longer resets _tooltipFired
  (ReleaseMouseCapture @0x0045D2B0 touches only the idle timestamp) —
  a mouse-up while a tooltip is shown no longer tears it down and
  silently re-fires it 250ms later.
- F8 ApplyTooltipText: applies ResizeTo's own max/min width/height
  clamps (P0x3C/0x3D/0x3E/0x3F, @0x00463C30) before assigning the
  grown size; zeroes text.Padding to keep the measured size margin-
  comparable. New ElementInfo/UiElement plumbing for the four
  properties, same shape as the existing tooltip fields.
- F9 doc precision: sweep counts corrected 434->430 / 191->187 (live-
  DAT re-measured), the "243 showable" claim now measured exactly
  (not assumed) via a new Showable column in the sweep test, and the
  MiscSettings citation split into its two real mechanisms
  (RegisterPreference in Init vs. AttachPreference/SetPreferenceRange
  elsewhere).
- F10 register AD-106: the topmost guarantee is versus dialogs/screens
  only (the overlay popup layer and drag ghost still paint above
  regardless), and the per-tick BringToFront ratchet has four rungs,
  not three.
- F11 RetailUiRuntime.ResetSessionDialogs: now also calls the new
  UiRoot.ResetTooltipTracking() so a post-reset hover re-shows
  immediately instead of waiting out the stale fired-latch.

New pinning tests (RetailTooltipPresenterTests: F1/F2/F5/F6/F7/F8) each
verified to fail against the pre-fix behavior via a temporary revert-
and-rerun before being confirmed against the restored fix.
PortalProjectionTests.ProjectToClipLease_ReusesPooledWorkWithoutResultArrays
recurrence logged on issue #346 (already the tracking issue for this
load-sensitive flake) — hit twice under load this review, standalone
26/26, unrelated to #409.

Gates: Release build 0 errors; App suite (live-DAT env) 5416/5413
passed/3 skips (was 5410/5407/3, +6 new tests); Runtime 1735/0;
UI.Abstractions 926/0; full solution (no env, 69 skips expected)
14,623/14,554 passed/69 skipped/0 failed (was 14,617/14,548, +6).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 20:54:15 +02:00
Erik
a377b9bff7 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>
2026-08-16 20:03:24 +02:00