Merge campaign-409-tooltips: #409 runtime-tooltip resolution fix — live-verified
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / portable-launcher (ubuntu-latest) (push) Waiting to run
Headless portability / portable-launcher (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / portable-launcher (ubuntu-latest) (push) Waiting to run
Headless portability / portable-launcher (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Retail reads the runtime m_TTText FIRST (StartTooltipAtMouse @0x00460DA3) with authored P0x49 as fallback; the presenter read only authored text, so every runtime-written tooltip (Options rows, checkbox bitfields, both social pages — writers acdream already had) never showed. Fixed with retail's resolution order + the P0x48 own-layout fallback; live-verified on a connected client. The authored-243 population measured as entirely chargen-resident. Deferred honestly: inventory item-name tooltips (UIElement_UIItem::UpdateTooltip — UiItemSlot lacks the plumbing) and #411 (hover cursor/rollover feedback, full mechanism mapped). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
commit
34ec397e9e
8 changed files with 487 additions and 33 deletions
181
docs/ISSUES.md
181
docs/ISSUES.md
|
|
@ -24,6 +24,87 @@ What does NOT go here:
|
||||||
- Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending.
|
- Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending.
|
||||||
- Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed.
|
- Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed.
|
||||||
|
|
||||||
|
## #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")
|
||||||
|
**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).
|
||||||
|
|
||||||
|
**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:
|
||||||
|
|
||||||
|
1. **Per-element cursor** — `UIElementManager::CheckCursor @0x0045ABF0`, called from
|
||||||
|
`SwitchMouseOver @0x0045B5F8` whenever the entered element changes. It takes
|
||||||
|
`m_pElementWithMouseCapture` if it `HasCursor()`, else `m_pElementLastEntered` if it
|
||||||
|
`HasCursor()`, and calls `SetCursor(elem->m_cursorDID, m_cursorHotX, m_cursorHotY, 0)`;
|
||||||
|
otherwise it restores `m_defaultCursorDID` with the "is default" flag 1.
|
||||||
|
`UIElement::HasCursor @0x00464980` is just `m_cursorDID != INVALID_DID`, and the ONLY
|
||||||
|
writer of `m_cursorDID` in the whole binary is `UIElement::SetCursor @0x0045FF50`,
|
||||||
|
whose ONLY caller is `MediaMachine::Update_Cursor @0x00465A80` — i.e. this layer is
|
||||||
|
100% authored `MediaDescCursor` state media, never game code.
|
||||||
|
**MEASURED (exhaustive raw scan of every `ElementDesc` in every `LayoutDesc`,
|
||||||
|
template roots and nested children included): exactly 101 authored cursor media,
|
||||||
|
on element types 9 (Resizebar) and 2 (Dragbar) only, using only 5 cursor DIDs —
|
||||||
|
`0x06006119`, `0x06006126`, `0x06006127`, `0x06006128`, `0x06005E66`.** Those five
|
||||||
|
are precisely what `RetailCursorCatalog.TryGetWindowControlCursor` already
|
||||||
|
hardcodes. **So NO inventory item, item list, button, or option row authors a
|
||||||
|
per-element cursor in retail, and acdream is not missing a cursor swap for them.**
|
||||||
|
What acdream IS missing here is the general seam: `UiElement.StateCursors` is parsed
|
||||||
|
(`LayoutImporter.ReadState`) and stored (`UiElement.SetStateCursors`) but has ZERO
|
||||||
|
consumers — the window move/resize cursors reach the screen through the hardcoded
|
||||||
|
catalog instead. Porting `CheckCursor` properly means driving the cursor off the
|
||||||
|
hovered element's own `StateCursors` and deleting the hardcode.
|
||||||
|
|
||||||
|
2. **Global "found" cursor** — `ClientUISystem::UpdateCursorState @0x00564630` picks the
|
||||||
|
`...Found` variant of whichever cursor the current combat/target/busy mode selects,
|
||||||
|
keyed ONLY on `SmartBox::get_found_object_id() != 0`. The only writer is
|
||||||
|
`UIElement_SmartBoxWrapper::FindObject @0x004E545D` — the 3D viewport's world pick.
|
||||||
|
So this layer never fires over an inventory item either.
|
||||||
|
|
||||||
|
3. **Per-element rollover STATE** — `UIElementManager::SwitchMouseOver @0x0045B560` calls
|
||||||
|
`m_pElementLastEntered->MouseOverTop(1)` on enter and `(0)` on leave.
|
||||||
|
`UIElement::MouseOverTop @0x004615D0` sets `__bitfield164` bit 0 and broadcasts
|
||||||
|
element message `0x1B`; the media machine then swaps the element to its rollover
|
||||||
|
media. `UIElement_Button::MouseOverTop @0x004721F0` and
|
||||||
|
`UIElement_Field::MouseOverTop @0x00472360` override it (the Field override is the
|
||||||
|
drag-drop accept/reject state pair, states 9/10).
|
||||||
|
|
||||||
|
**Most likely what the user is seeing.** Because layers 1 and 2 are measurably not
|
||||||
|
involved for an inventory item, "the cursor should light up" is most likely layer 3 —
|
||||||
|
the item cell's own rollover highlight. acdream's `UiButton` honors rollover
|
||||||
|
(`RolloverEnabled`, dat property `0x13`, `UiButton.cs:462` + its HoverEnter/HoverLeave
|
||||||
|
handling at `UiButton.cs:831/835`), but **`UiItemSlot` has no hover handling at all** —
|
||||||
|
no HoverEnter/HoverLeave, no rollover state. That is the concrete gap to close first.
|
||||||
|
|
||||||
|
**Open question that needs one user observation (or a retail side-by-side).** Whether
|
||||||
|
the retail behavior the user remembers is (a) the item cell highlighting, or (b) the
|
||||||
|
mouse pointer bitmap actually changing. If it is (b), the mechanism is NOT any of the
|
||||||
|
three above for a UI item and needs a fresh derivation — do not guess. Ask before
|
||||||
|
porting.
|
||||||
|
|
||||||
|
**Port plan (in dependency order).**
|
||||||
|
1. Give `UiItemSlot` a HoverEnter/HoverLeave rollover state (retail
|
||||||
|
`UIElement::MouseOverTop @0x004615D0` bit 0 + message `0x1B`), sourced from the
|
||||||
|
cell's own authored rollover media where it has one.
|
||||||
|
2. Replace `RetailCursorCatalog.TryGetWindowControlCursor`'s five hardcoded DIDs with a
|
||||||
|
real `CheckCursor @0x0045ABF0` port off `UiElement.StateCursors` + a capture-wins
|
||||||
|
precedence, driven from `UiRoot`'s existing hover-change edge (the same edge #409's
|
||||||
|
tooltip dwell already uses). This is behavior-neutral for the 101 measured elements
|
||||||
|
and removes a hardcode; it is the prerequisite for any future DAT that authors a
|
||||||
|
cursor somewhere new.
|
||||||
|
3. Only if the user confirms (b) above: derive the item-hover cursor mechanism fresh.
|
||||||
|
|
||||||
|
**Files:** `src/AcDream.App/UI/UiItemSlot.cs`, `src/AcDream.App/UI/UiRoot.cs`
|
||||||
|
(`UpdateHover`), `src/AcDream.App/UI/RetailCursorCatalog.cs`,
|
||||||
|
`src/AcDream.App/UI/CursorFeedbackController.cs`, `src/AcDream.App/UI/UiElement.cs`
|
||||||
|
(`StateCursors`, today unconsumed), `src/AcDream.App/UI/Layout/LayoutImporter.cs`
|
||||||
|
(`ReadState`'s `MediaDescCursor` read).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## #410 — Client-wide VJustify (vertical text justification) enum mapping + unauthored default are wrong (retail default is Top, not Center)
|
## #410 — Client-wide VJustify (vertical text justification) enum mapping + unauthored default are wrong (retail default is Top, not Center)
|
||||||
|
|
||||||
**Status:** OPEN
|
**Status:** OPEN
|
||||||
|
|
@ -107,7 +188,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)
|
## #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 landed same day (`fix(ui): #409 tooltip review fix round`) — pending the user's connected visual gate (see the gate note at the bottom of this entry).
|
**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.
|
||||||
**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)
|
**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
|
**2026-08-16 re-derivation + port.** Full re-derivation from
|
||||||
|
|
@ -125,6 +206,63 @@ the other 187 have no literal text and show nothing — see register row
|
||||||
TS-85 for the honest scope of what's missing there), superseding the
|
TS-85 for the honest scope of what's missing there), superseding the
|
||||||
original "~253" estimate.
|
original "~253" estimate.
|
||||||
|
|
||||||
|
**2026-08-16 LIVE-FAILURE round (user gate on build `1.0.3-tt.a`: "tooltips do not appear
|
||||||
|
anywhere except one on the paperdoll").** Root-caused, fixed, and live-verified the same
|
||||||
|
day. TWO findings, both measured, neither of them a broken hover/hit-test:
|
||||||
|
|
||||||
|
1. **The dominant root cause: the presenter read only the AUTHORED text.**
|
||||||
|
`RetailTooltipPresenter.OnTooltipShow` gated on `widget.AuthoredTooltipText`
|
||||||
|
(`P0x49`) alone. Retail's `UIElement::StartTooltipAtMouse @0x00460D70` takes the
|
||||||
|
RUNTIME `m_TTText` first (`@0x00460DA3` `StringInfo::IsValid` -> `@0x00460DAA`
|
||||||
|
verbatim copy) and only falls back to `InqProperty(0x49)` at `@0x00460DDF`.
|
||||||
|
acdream ALREADY had the runtime layer — `UiElement.GetTooltipText()`, populated by
|
||||||
|
`CharacterOptionsPageController:477`, `ChatOptionsPageController:502`,
|
||||||
|
`ConfigOptionsPageController` (5 sites), `KeyboardConfigController:477`,
|
||||||
|
`SocialAllegiancePageController:412`, `SocialFellowshipPageController:458`, and
|
||||||
|
`UiCheckboxBitfield64:216` — but nothing consulted it. **Live-DAT measured:** the
|
||||||
|
Options toggle-row checkbox (`0x2100002B` template root `0x10000218`, checkbox 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 retail's `UIOption_CheckboxBitfield64::CreateChildren
|
||||||
|
@0x00485E65` stamps its `siTooltip` array. Re-measured across every LayoutDesc: all
|
||||||
|
187 no-literal-text tooltip elements author BOTH locator ids, i.e. the whole set is
|
||||||
|
runtime-text targets. FIXED: `RetailTooltipPresenter.ResolveTooltipText` now uses
|
||||||
|
retail's order, and the `P0x48`-absent fallback to the element's own LayoutDesc
|
||||||
|
(`@0x00460E7E`, `this->m_layout->m_DID`) is ported through the new
|
||||||
|
`UiElement.SourceLayoutDid` threaded from `LayoutImporter.Build`'s new
|
||||||
|
`sourceLayoutDid` parameter. (`RowTemplateResolver`'s build delegate carries no
|
||||||
|
layout id, so social row templates leave `SourceLayoutDid` at 0 — they author
|
||||||
|
`P0x48` anyway, so no live case needs the fallback there.)
|
||||||
|
|
||||||
|
2. **The "243 showable" number was never an in-world number.** A grouped re-sweep of
|
||||||
|
the same 243 found them concentrated in CHARACTER-CREATION layouts (`0x21000038`
|
||||||
|
heritage/profession/skills/appearance/town/summary tabs, `0x21000047` attributes,
|
||||||
|
`0x21000049`/`0x2100004C` appearance+skills, `0x21000005`/`0x2100000F`/`0x21000068`
|
||||||
|
the shared appearance page, `0x21000046` heritage picks). 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,
|
||||||
|
`PaperdollController.DollDragMaskId`) and `0x100005BE` "When this option is chosen,
|
||||||
|
you will see explicit equipment slots instead of a portrait" (the Slots button).
|
||||||
|
**That first one IS the user's single working tooltip** — confirmed live. So the
|
||||||
|
paperdoll was never a differential against a broken mechanism; it was the only
|
||||||
|
authored-text tooltip in the panel being hovered. Reachability was also measured
|
||||||
|
and is NOT a problem: 238 of the 243 build as real, non-`ClickThrough` hover
|
||||||
|
targets (230 `UiButton`, 6 `UiScrollbar`, 2 `UiField`; the 5 misses are Type-12
|
||||||
|
prototypes the importer skips by design).
|
||||||
|
|
||||||
|
**Live verification (2026-08-16, connected `testaccount`/`+Acdream`, Release,
|
||||||
|
`ACDREAM_RETAIL_UI=1`):** hovering Options -> Character -> "Vivid Targeting Indicator"
|
||||||
|
now shows "Enable this option to apply a targeting indicator around selected objects
|
||||||
|
and monsters for better visual reference"; a temporary hover probe confirmed the hover
|
||||||
|
target is element `0x10000219` with `runtime=True`. Hovering the paperdoll still shows
|
||||||
|
"Drag clothing and armor here to wear them". Hovering 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: acdream's
|
||||||
|
`UiItemSlot` is constructed programmatically at 6+ sites and carries neither the
|
||||||
|
`P0x47` popup locator nor a name source, so porting it is its own slice, not a
|
||||||
|
one-line seam. Register TS-85 is narrowed accordingly and now enumerates all 15
|
||||||
|
`SetTooltip` call sites split into ported / no-acdream-analog.
|
||||||
|
|
||||||
**2026-08-16 review-fix round (F1-F11), same day.** An Opus review of the
|
**2026-08-16 review-fix round (F1-F11), same day.** An Opus review of the
|
||||||
port above returned architectural PASS-with-findings / retail-fidelity FAIL
|
port above returned architectural PASS-with-findings / retail-fidelity FAIL
|
||||||
with twelve findings; F1-F11 landed in one fix commit (F12 was info-only).
|
with twelve findings; F1-F11 landed in one fix commit (F12 was info-only).
|
||||||
|
|
@ -196,19 +334,34 @@ and the per-tick `BringToFront` re-raise chain now has four rungs
|
||||||
`RetailDialogFactory`, `RetailTooltipPresenter`) — bounded and enumerable
|
`RetailDialogFactory`, `RetailTooltipPresenter`) — bounded and enumerable
|
||||||
today, but a design smell worth flagging.
|
today, but a design smell worth flagging.
|
||||||
|
|
||||||
**Gate note (5-10 min, `ACDREAM_RETAIL_UI=1`):** hover the mouse over any
|
**Gate note (5-10 min, `ACDREAM_RETAIL_UI=1`) — REWRITTEN at the live-failure
|
||||||
of these and hold still — a small tooltip box should appear after a brief
|
round, because the original note only listed chargen surfaces and so could not
|
||||||
pause (~0.25 s, matching retail's registered default) and disappear when
|
have caught the in-world gap the user's gate found.** Hover and hold still; a
|
||||||
you move to a different control: (1) chargen Appearance page — the rotate
|
small box should appear after ~0.25 s and vanish when you move to a different
|
||||||
arrows beside the preview ("Rotate left."/"Rotate right.") and any color
|
control.
|
||||||
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
|
*In-world (this is what the live-failure fix added — check these FIRST):*
|
||||||
multiple lines, not run off-screen); (3) any other screen with a tooltip
|
(1) Options (F11) -> **Character** tab, any checkbox row (e.g. "Vivid Targeting
|
||||||
you recall from retail. Confirm: the box sits right at the cursor (not
|
Indicator") — a full help sentence. Same for the **Chat** and **Config** tabs'
|
||||||
offset), never runs off the edge of the window even near a corner, and
|
rows, sliders and dropdowns. (2) Options -> Configure Keyboard, any key button.
|
||||||
disappears on its own after ~10 s if you hold still without moving away.
|
(3) Social panel (F3/F4) -> the Allegiance/Fellowship checkboxes. (4) Inventory
|
||||||
No click-to-dismiss is expected — only moving off the control, or a very
|
-> hover the paperdoll figure ("Drag clothing and armor here to wear them") and
|
||||||
long hold, closes it.
|
the "Slots" button.
|
||||||
|
|
||||||
|
*Chargen (already worked before this round):* (5) Appearance page rotate arrows
|
||||||
|
("Rotate left."/"Rotate right.") and any color swatch; (6) the hair/eyes/nose
|
||||||
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -87,17 +87,24 @@ public static class LayoutImporter
|
||||||
/// font at build time instead of the shared <paramref name="datFont"/> fallback.
|
/// font at build time instead of the shared <paramref name="datFont"/> fallback.
|
||||||
/// Null preserves the original single-font behavior for all callers that don't
|
/// Null preserves the original single-font behavior for all callers that don't
|
||||||
/// pass it — no behavior change for the live game path.</param>
|
/// pass it — no behavior change for the live game path.</param>
|
||||||
|
/// <param name="sourceLayoutDid">#409: the LayoutDesc DID these infos came from,
|
||||||
|
/// recorded on every built widget as <see cref="UiElement.SourceLayoutDid"/>.
|
||||||
|
/// Retail keeps the same back-pointer (<c>UIElement::m_layout</c>) and reads it in
|
||||||
|
/// <c>StartTooltipAtMouse @0x00460E7E</c> as the tooltip popup layout when the
|
||||||
|
/// element authors no <c>P0x48</c>. Zero (the default) leaves it unknown, which
|
||||||
|
/// simply declines that fallback — callers with no dat context pass nothing.</param>
|
||||||
public static ImportedLayout Build(
|
public static ImportedLayout Build(
|
||||||
ElementInfo rootInfo,
|
ElementInfo rootInfo,
|
||||||
Func<uint, (uint, int, int)> resolve,
|
Func<uint, (uint, int, int)> resolve,
|
||||||
UiDatFont? datFont,
|
UiDatFont? datFont,
|
||||||
Func<uint, UiDatFont?>? fontResolve = null,
|
Func<uint, UiDatFont?>? fontResolve = null,
|
||||||
Func<UiStringInfoValue, string?>? stringResolve = null)
|
Func<UiStringInfoValue, string?>? stringResolve = null,
|
||||||
|
uint sourceLayoutDid = 0u)
|
||||||
{
|
{
|
||||||
var byId = new Dictionary<uint, UiElement>();
|
var byId = new Dictionary<uint, UiElement>();
|
||||||
// Root is never a Type-12 prototype in practice; fall back to a generic
|
// Root is never a Type-12 prototype in practice; fall back to a generic
|
||||||
// container if the factory returns null for an exotic root type.
|
// container if the factory returns null for an exotic root type.
|
||||||
var root = BuildWidget(rootInfo, resolve, datFont, fontResolve, stringResolve, byId);
|
var root = BuildWidget(rootInfo, resolve, datFont, fontResolve, stringResolve, byId, sourceLayoutDid);
|
||||||
if (root is null)
|
if (root is null)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[D.2b] LayoutImporter: root element 0x{rootInfo.Id:X8} (type {rootInfo.Type}) produced no widget — using empty container fallback.");
|
Console.WriteLine($"[D.2b] LayoutImporter: root element 0x{rootInfo.Id:X8} (type {rootInfo.Type}) produced no widget — using empty container fallback.");
|
||||||
|
|
@ -112,11 +119,15 @@ public static class LayoutImporter
|
||||||
UiDatFont? datFont,
|
UiDatFont? datFont,
|
||||||
Func<uint, UiDatFont?>? fontResolve,
|
Func<uint, UiDatFont?>? fontResolve,
|
||||||
Func<UiStringInfoValue, string?>? stringResolve,
|
Func<UiStringInfoValue, string?>? stringResolve,
|
||||||
Dictionary<uint, UiElement> byId)
|
Dictionary<uint, UiElement> byId,
|
||||||
|
uint sourceLayoutDid)
|
||||||
{
|
{
|
||||||
var w = DatWidgetFactory.Create(info, resolve, datFont, fontResolve, stringResolve);
|
var w = DatWidgetFactory.Create(info, resolve, datFont, fontResolve, stringResolve);
|
||||||
if (w is null) return null; // Type-12 style prototype — skip
|
if (w is null) return null; // Type-12 style prototype — skip
|
||||||
|
|
||||||
|
// #409: see the Build overload's own sourceLayoutDid doc comment.
|
||||||
|
w.SourceLayoutDid = sourceLayoutDid;
|
||||||
|
|
||||||
// GF-13: pure data passthrough — see UiElement.AuthoredInvisible's own
|
// GF-13: pure data passthrough — see UiElement.AuthoredInvisible's own
|
||||||
// doc comment for why this does NOT set Visible here.
|
// doc comment for why this does NOT set Visible here.
|
||||||
w.AuthoredInvisible = info.Invisible;
|
w.AuthoredInvisible = info.Invisible;
|
||||||
|
|
@ -152,7 +163,7 @@ public static class LayoutImporter
|
||||||
{
|
{
|
||||||
foreach (var child in info.Children)
|
foreach (var child in info.Children)
|
||||||
{
|
{
|
||||||
var cw = BuildWidget(child, resolve, datFont, fontResolve, stringResolve, byId);
|
var cw = BuildWidget(child, resolve, datFont, fontResolve, stringResolve, byId, sourceLayoutDid);
|
||||||
if (cw is not null) w.AddChild(cw);
|
if (cw is not null) w.AddChild(cw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -178,7 +189,7 @@ public static class LayoutImporter
|
||||||
foreach (var child in info.Children)
|
foreach (var child in info.Children)
|
||||||
{
|
{
|
||||||
if (child.Type == 3) continue; // slice containers: already consumed by BuildMeter
|
if (child.Type == 3) continue; // slice containers: already consumed by BuildMeter
|
||||||
var cw = BuildWidget(child, resolve, datFont, fontResolve, stringResolve, byId);
|
var cw = BuildWidget(child, resolve, datFont, fontResolve, stringResolve, byId, sourceLayoutDid);
|
||||||
if (cw is not null) w.AddChild(cw);
|
if (cw is not null) w.AddChild(cw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -206,7 +217,7 @@ public static class LayoutImporter
|
||||||
foreach (var child in info.Children)
|
foreach (var child in info.Children)
|
||||||
{
|
{
|
||||||
if (child.StateMedia.Count == 0) continue;
|
if (child.StateMedia.Count == 0) continue;
|
||||||
var cw = BuildWidget(child, resolve, datFont, fontResolve, stringResolve, byId);
|
var cw = BuildWidget(child, resolve, datFont, fontResolve, stringResolve, byId, sourceLayoutDid);
|
||||||
if (cw is null) continue;
|
if (cw is null) continue;
|
||||||
// F5/F6 (Campaign CC gate round 1 closeout): a NARROW honor
|
// F5/F6 (Campaign CC gate round 1 closeout): a NARROW honor
|
||||||
// of AuthoredInvisible, scoped to children reached through
|
// of AuthoredInvisible, scoped to children reached through
|
||||||
|
|
@ -397,7 +408,7 @@ public static class LayoutImporter
|
||||||
var rootInfo = ImportInfos(dats, layoutId);
|
var rootInfo = ImportInfos(dats, layoutId);
|
||||||
if (rootInfo is null) return null;
|
if (rootInfo is null) return null;
|
||||||
var strings = new DatStringResolver(dats);
|
var strings = new DatStringResolver(dats);
|
||||||
return Build(rootInfo, resolve, datFont, fontResolve, strings.Resolve);
|
return Build(rootInfo, resolve, datFont, fontResolve, strings.Resolve, layoutId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Import one selected root from a catalog-style LayoutDesc.</summary>
|
/// <summary>Import one selected root from a catalog-style LayoutDesc.</summary>
|
||||||
|
|
@ -412,7 +423,7 @@ public static class LayoutImporter
|
||||||
var rootInfo = ImportInfos(dats, layoutId, rootElementId);
|
var rootInfo = ImportInfos(dats, layoutId, rootElementId);
|
||||||
if (rootInfo is null) return null;
|
if (rootInfo is null) return null;
|
||||||
var strings = new DatStringResolver(dats);
|
var strings = new DatStringResolver(dats);
|
||||||
return Build(rootInfo, resolve, datFont, fontResolve, strings.Resolve);
|
return Build(rootInfo, resolve, datFont, fontResolve, strings.Resolve, layoutId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Inheritance resolution ────────────────────────────────────────────────
|
// ── Inheritance resolution ────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -96,26 +96,92 @@ public sealed class RetailTooltipPresenter : IDisposable
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Enabled { get; set; } = true;
|
public bool Enabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retail <c>UIElement::StartTooltipAtMouse @0x00460D70</c>'s text source, in
|
||||||
|
/// retail's own order: the RUNTIME <c>m_TTText</c> first
|
||||||
|
/// (<c>@0x00460DA3</c> tests <c>StringInfo::IsValid(&m_TTText)</c> and takes
|
||||||
|
/// it verbatim at <c>@0x00460DAA</c>), and only when that is empty does it fall
|
||||||
|
/// back to the AUTHORED <c>P0x49</c> property (<c>@0x00460DDF</c>
|
||||||
|
/// <c>InqProperty(0x49)</c>).
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// <c>UiElement.GetTooltipText()</c> is this port's <c>m_TTText</c>: it is what
|
||||||
|
/// every acdream analog of retail's ~15 game-code <c>UIElement::SetTooltip</c>
|
||||||
|
/// call sites already writes — the Options panel's per-row
|
||||||
|
/// <c>ID_PlayerOption_*_Help</c> strings
|
||||||
|
/// (<c>UIOption_CheckboxBitfield64::CreateChildren @0x00485E65</c>'s
|
||||||
|
/// <c>siTooltip</c> array; acdream <c>CharacterOptionsPageController</c>,
|
||||||
|
/// <c>ChatOptionsPageController</c>, <c>ConfigOptionsPageController</c>,
|
||||||
|
/// <c>UiCheckboxBitfield64</c>), the Configure-Keyboard key captions, and the
|
||||||
|
/// social pages' checkbox help. Reading only <see cref="UiElement.AuthoredTooltipText"/>
|
||||||
|
/// (as this class did before) is why NONE of those showed live: retail's
|
||||||
|
/// in-world panels author the popup LOCATOR (<c>P0x47</c>/<c>P0x48</c>) and the
|
||||||
|
/// <c>P0x4B</c> on-bit but deliberately author NO <c>P0x49</c> text, because the
|
||||||
|
/// text arrives at runtime. Live-DAT measured: the Options toggle row
|
||||||
|
/// (<c>0x2100002B</c>/<c>0x10000218</c>) authors
|
||||||
|
/// <c>P0x47=0x10000397 P0x48=0x21000041 P0x4B=true P0x49=<empty></c>.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
private static string? ResolveTooltipText(UiElement widget, out bool fromRuntime)
|
||||||
|
{
|
||||||
|
string? runtime = widget.GetTooltipText();
|
||||||
|
if (!string.IsNullOrEmpty(runtime))
|
||||||
|
{
|
||||||
|
fromRuntime = true;
|
||||||
|
return runtime;
|
||||||
|
}
|
||||||
|
fromRuntime = false;
|
||||||
|
return widget.AuthoredTooltipText;
|
||||||
|
}
|
||||||
|
|
||||||
private void OnTooltipShow(UiElement widget)
|
private void OnTooltipShow(UiElement widget)
|
||||||
{
|
{
|
||||||
RemovePopup();
|
RemovePopup();
|
||||||
|
|
||||||
if (!Enabled || !widget.AuthoredTooltipEnabled)
|
if (!Enabled)
|
||||||
return;
|
return;
|
||||||
if (string.IsNullOrEmpty(widget.AuthoredTooltipText))
|
|
||||||
|
string? tooltipText = ResolveTooltipText(widget, out bool fromRuntime);
|
||||||
|
if (string.IsNullOrEmpty(tooltipText))
|
||||||
return;
|
return;
|
||||||
if (widget.AuthoredTooltipLayoutDid == 0u || widget.AuthoredTooltipRootElementId == 0u)
|
|
||||||
|
// The P0x4B TooltipOn bit (UIElement::MouseHover @0x0046254C reads
|
||||||
|
// __bitfield164 bit 5). Retail's game-code SetTooltip sites do not rely on
|
||||||
|
// the authored bit — each one SETS it in the same breath as the text:
|
||||||
|
// UIElement_UIItem::UpdateTooltip @0x004E1D5E, gmPaperDollUI::
|
||||||
|
// UpdateItemSlotTooltip @0x004A52F4, gmSpellcastingUI::UpdateEndowmentIcon
|
||||||
|
// @0x004C63AC, SpellCastSubMenu::UpdateFromPlayerModule @0x004C67ED,
|
||||||
|
// gmSpellcastingUI::UpdateCastButtonTooltip @0x004C7000, SpellCastSubMenu::
|
||||||
|
// AddFavorite @0x004C7218, gmRadarUI::DrawObjects @0x004D9617, and
|
||||||
|
// UIElement_Text::RecalculateTruncation @0x00467076 — all `|= 0x20`, each
|
||||||
|
// paired with a `&= ~0x20` on the clearing path. So a widget carrying
|
||||||
|
// runtime tooltip text is tooltip-on by construction; only the AUTHORED-
|
||||||
|
// text path consults the authored bit.
|
||||||
|
if (!fromRuntime && !widget.AuthoredTooltipEnabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (widget.AuthoredTooltipRootElementId == 0u)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// StartTooltipAtMouse @0x00460E6B reads P0x48 and, when it is absent
|
||||||
|
// (@0x00460E7E), substitutes the element's OWN LayoutDesc DID
|
||||||
|
// (this->m_layout->m_DID) before dispatching to StartTooltip. Only when
|
||||||
|
// BOTH are absent (@0x00460E91) does it give up.
|
||||||
|
uint layoutDid = widget.AuthoredTooltipLayoutDid != 0u
|
||||||
|
? widget.AuthoredTooltipLayoutDid
|
||||||
|
: widget.SourceLayoutDid;
|
||||||
|
if (layoutDid == 0u)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ImportedLayout? layout;
|
ImportedLayout? layout;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
layout = _createLayout(widget.AuthoredTooltipLayoutDid, widget.AuthoredTooltipRootElementId);
|
layout = _createLayout(layoutDid, widget.AuthoredTooltipRootElementId);
|
||||||
}
|
}
|
||||||
catch (Exception error)
|
catch (Exception error)
|
||||||
{
|
{
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"[UI] #409 tooltip popup layout=0x{widget.AuthoredTooltipLayoutDid:X8} "
|
$"[UI] #409 tooltip popup layout=0x{layoutDid:X8} "
|
||||||
+ $"root=0x{widget.AuthoredTooltipRootElementId:X8} failed to build: {error.Message}");
|
+ $"root=0x{widget.AuthoredTooltipRootElementId:X8} failed to build: {error.Message}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -150,7 +216,7 @@ public sealed class RetailTooltipPresenter : IDisposable
|
||||||
text.LayoutPolicy = null;
|
text.LayoutPolicy = null;
|
||||||
text.Anchors = AnchorEdges.None;
|
text.Anchors = AnchorEdges.None;
|
||||||
|
|
||||||
ApplyTooltipText(root, text, widget.AuthoredTooltipText!);
|
ApplyTooltipText(root, text, tooltipText!);
|
||||||
|
|
||||||
SetClickThroughRecursive(root);
|
SetClickThroughRecursive(root);
|
||||||
PositionAtMouse(root);
|
PositionAtMouse(root);
|
||||||
|
|
|
||||||
|
|
@ -2506,7 +2506,8 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
_bindings.Assets.ResolveSprite,
|
_bindings.Assets.ResolveSprite,
|
||||||
_bindings.Assets.DefaultFont,
|
_bindings.Assets.DefaultFont,
|
||||||
_bindings.Assets.ResolveFont,
|
_bindings.Assets.ResolveFont,
|
||||||
strings.Resolve).Root;
|
strings.Resolve,
|
||||||
|
templateLayoutId).Root;
|
||||||
},
|
},
|
||||||
resolveString: (tableId, stringId) => strings.Resolve(tableId, stringId),
|
resolveString: (tableId, stringId) => strings.Resolve(tableId, stringId),
|
||||||
new Layout.CharacterOptionsPageController.Bindings(
|
new Layout.CharacterOptionsPageController.Bindings(
|
||||||
|
|
@ -2567,7 +2568,8 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
_bindings.Assets.ResolveSprite,
|
_bindings.Assets.ResolveSprite,
|
||||||
_bindings.Assets.DefaultFont,
|
_bindings.Assets.DefaultFont,
|
||||||
_bindings.Assets.ResolveFont,
|
_bindings.Assets.ResolveFont,
|
||||||
strings.Resolve).Root;
|
strings.Resolve,
|
||||||
|
templateLayoutId).Root;
|
||||||
},
|
},
|
||||||
resolveString: (tableId, stringId) => strings.Resolve(tableId, stringId),
|
resolveString: (tableId, stringId) => strings.Resolve(tableId, stringId),
|
||||||
new Layout.ChatOptionsPageController.Bindings(
|
new Layout.ChatOptionsPageController.Bindings(
|
||||||
|
|
@ -2613,7 +2615,8 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
_bindings.Assets.ResolveSprite,
|
_bindings.Assets.ResolveSprite,
|
||||||
_bindings.Assets.DefaultFont,
|
_bindings.Assets.DefaultFont,
|
||||||
_bindings.Assets.ResolveFont,
|
_bindings.Assets.ResolveFont,
|
||||||
strings.Resolve).Root;
|
strings.Resolve,
|
||||||
|
templateLayoutId).Root;
|
||||||
},
|
},
|
||||||
resolveString: (tableId, stringId) => strings.Resolve(tableId, stringId),
|
resolveString: (tableId, stringId) => strings.Resolve(tableId, stringId),
|
||||||
new Layout.ConfigOptionsPageController.Bindings(
|
new Layout.ConfigOptionsPageController.Bindings(
|
||||||
|
|
@ -2782,7 +2785,8 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
_bindings.Assets.ResolveSprite,
|
_bindings.Assets.ResolveSprite,
|
||||||
_bindings.Assets.DefaultFont,
|
_bindings.Assets.DefaultFont,
|
||||||
_bindings.Assets.ResolveFont,
|
_bindings.Assets.ResolveFont,
|
||||||
strings.Resolve).Root;
|
strings.Resolve,
|
||||||
|
templateLayoutId).Root;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resolveString: (tableId, stringId) => strings.Resolve(tableId, stringId),
|
resolveString: (tableId, stringId) => strings.Resolve(tableId, stringId),
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,20 @@ public abstract class UiElement
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public uint AuthoredTooltipLayoutDid { get; internal set; }
|
public uint AuthoredTooltipLayoutDid { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// #409 (live-failure round): the DID of the <c>LayoutDesc</c> this widget
|
||||||
|
/// was imported from, stamped by <c>LayoutImporter.Import</c>'s dat shell.
|
||||||
|
/// Retail's <c>UIElement::StartTooltipAtMouse @0x00460D70</c> falls back to
|
||||||
|
/// exactly this — <c>this->m_layout->m_DID</c> (<c>@0x00460E7E</c>) — when
|
||||||
|
/// the hovered element authors a tooltip popup ROOT (<c>P0x47</c>) but no
|
||||||
|
/// popup LAYOUT (<c>P0x48</c>), i.e. the popup root lives in the element's
|
||||||
|
/// own layout. Zero when the widget came through the pure
|
||||||
|
/// <c>LayoutImporter.Build</c> layer (which has no dat context) or was
|
||||||
|
/// constructed directly; the presenter then behaves exactly as it did
|
||||||
|
/// before this field existed.
|
||||||
|
/// </summary>
|
||||||
|
public uint SourceLayoutDid { get; internal set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// #409: mirrors <c>ElementInfo.TooltipTextChildElementId</c> (dat
|
/// #409: mirrors <c>ElementInfo.TooltipTextChildElementId</c> (dat
|
||||||
/// property <c>0x4A</c>). Meaningful only when read off a tooltip
|
/// property <c>0x4A</c>). Meaningful only when read off a tooltip
|
||||||
|
|
|
||||||
|
|
@ -434,4 +434,159 @@ public sealed class RetailTooltipPresenterTests
|
||||||
UiElement popup = root.Children.Single(c => !ReferenceEquals(c, target));
|
UiElement popup = root.Children.Single(c => !ReferenceEquals(c, target));
|
||||||
Assert.Equal(40f, popup.Width); // clamped, not the larger natural measured size
|
Assert.Equal(40f, popup.Width); // clamped, not the larger natural measured size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── #409 live-failure round: the RUNTIME text family ────────────────────
|
||||||
|
|
||||||
|
/// <summary>A hover target that carries retail's runtime <c>m_TTText</c>
|
||||||
|
/// (this port's <see cref="UiElement.GetTooltipText"/>) instead of an
|
||||||
|
/// authored <c>P0x49</c> — the exact shape of every Options-panel row, the
|
||||||
|
/// Configure-Keyboard key buttons, and the social pages' checkboxes.</summary>
|
||||||
|
private sealed class RuntimeTextTarget : UiElement
|
||||||
|
{
|
||||||
|
public string? Runtime { get; set; }
|
||||||
|
public override string? GetTooltipText() => Runtime;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RuntimeTextTarget AddRuntimeTextTarget(
|
||||||
|
UiRoot root, string? runtime, bool authoredOn = true,
|
||||||
|
uint layoutDid = PopupLayoutDid, uint sourceLayoutDid = 0u)
|
||||||
|
{
|
||||||
|
var target = new RuntimeTextTarget
|
||||||
|
{
|
||||||
|
Left = 100, Top = 100, Width = 40, Height = 20,
|
||||||
|
Runtime = runtime,
|
||||||
|
AuthoredTooltipEnabled = authoredOn,
|
||||||
|
AuthoredTooltipRootElementId = PopupRootId,
|
||||||
|
AuthoredTooltipLayoutDid = layoutDid,
|
||||||
|
SourceLayoutDid = sourceLayoutDid,
|
||||||
|
};
|
||||||
|
root.AddChild(target);
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HoverAndDwell(UiRoot root)
|
||||||
|
{
|
||||||
|
root.OnMouseMove(110, 110);
|
||||||
|
root.Tick(0.016, 0);
|
||||||
|
root.Tick(0.016, root.TooltipDelayMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RuntimeText_ShowsEvenWithNoAuthoredP0x49()
|
||||||
|
{
|
||||||
|
// THE live-failure root cause. Retail StartTooltipAtMouse @0x00460DA3
|
||||||
|
// takes m_TTText verbatim when it is valid and only falls back to the
|
||||||
|
// authored P0x49 at @0x00460DDF. Live-DAT measured: the Options toggle
|
||||||
|
// row's checkbox (0x2100002B/0x10000219) authors P0x47/P0x48/P0x4B but
|
||||||
|
// NO P0x49 — its help text arrives at runtime, exactly like retail's
|
||||||
|
// UIOption_CheckboxBitfield64::CreateChildren @0x00485E65 siTooltip.
|
||||||
|
var (root, _, requests) = CreateHarness();
|
||||||
|
var target = AddRuntimeTextTarget(
|
||||||
|
root, "When this option is chosen, you will always appear as offline.");
|
||||||
|
int childrenBefore = root.Children.Count;
|
||||||
|
|
||||||
|
HoverAndDwell(root);
|
||||||
|
|
||||||
|
Assert.Single(requests, r => r == (PopupLayoutDid, PopupRootId));
|
||||||
|
Assert.Equal(childrenBefore + 1, root.Children.Count);
|
||||||
|
UiElement popup = root.Children.Single(c => !ReferenceEquals(c, target));
|
||||||
|
UiText text = Assert.IsType<UiText>(FindById(popup, TextChildId));
|
||||||
|
Assert.Contains(
|
||||||
|
"appear as offline",
|
||||||
|
string.Join(' ', text.LinesProvider!().Select(l => l.Text)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RuntimeText_WinsOverTheAuthoredP0x49()
|
||||||
|
{
|
||||||
|
// Precedence, not merge: @0x00460DA3's IsValid(m_TTText) branch skips
|
||||||
|
// the InqProperty(0x49) read entirely.
|
||||||
|
var (root, _, _) = CreateHarness();
|
||||||
|
var target = AddRuntimeTextTarget(root, "runtime wins");
|
||||||
|
target.AuthoredTooltipText = "authored loses";
|
||||||
|
|
||||||
|
HoverAndDwell(root);
|
||||||
|
|
||||||
|
UiElement popup = root.Children.Single(c => !ReferenceEquals(c, target));
|
||||||
|
UiText text = Assert.IsType<UiText>(FindById(popup, TextChildId));
|
||||||
|
string shown = string.Join(' ', text.LinesProvider!().Select(l => l.Text));
|
||||||
|
Assert.Contains("runtime wins", shown);
|
||||||
|
Assert.DoesNotContain("authored loses", shown);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RuntimeText_DoesNotNeedTheAuthoredTooltipOnBit()
|
||||||
|
{
|
||||||
|
// Every game-code SetTooltip site sets the bit itself in the same
|
||||||
|
// breath as the text (UIElement_UIItem::UpdateTooltip @0x004E1D5E and
|
||||||
|
// seven siblings, all `__bitfield164 |= 0x20`), so runtime text is
|
||||||
|
// tooltip-on by construction. The AUTHORED-text path still consults
|
||||||
|
// the authored bit — pinned by the next test.
|
||||||
|
var (root, _, requests) = CreateHarness();
|
||||||
|
AddRuntimeTextTarget(root, "runtime text, authored bit off", authoredOn: false);
|
||||||
|
|
||||||
|
HoverAndDwell(root);
|
||||||
|
|
||||||
|
Assert.Single(requests);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AuthoredText_StillRequiresTheAuthoredTooltipOnBit()
|
||||||
|
{
|
||||||
|
var (root, _, requests) = CreateHarness();
|
||||||
|
var target = AddRuntimeTextTarget(root, runtime: null, authoredOn: false);
|
||||||
|
target.AuthoredTooltipText = "authored, but P0x4B is off";
|
||||||
|
|
||||||
|
HoverAndDwell(root);
|
||||||
|
|
||||||
|
Assert.Empty(requests);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MissingP0x48_FallsBackToTheElementsOwnSourceLayout()
|
||||||
|
{
|
||||||
|
// StartTooltipAtMouse @0x00460E7E: when GetAttribute_DataID(0x48)
|
||||||
|
// yields INVALID, retail substitutes this->m_layout->m_DID before
|
||||||
|
// dispatching, and only gives up when both are absent (@0x00460E91).
|
||||||
|
var (root, _, requests) = CreateHarness();
|
||||||
|
AddRuntimeTextTarget(
|
||||||
|
root, "no P0x48 authored", layoutDid: 0u, sourceLayoutDid: 0x21000099u);
|
||||||
|
|
||||||
|
HoverAndDwell(root);
|
||||||
|
|
||||||
|
Assert.Single(requests, r => r == (0x21000099u, PopupRootId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MissingP0x48_AndNoSourceLayout_ShowsNothing()
|
||||||
|
{
|
||||||
|
var (root, _, requests) = CreateHarness();
|
||||||
|
AddRuntimeTextTarget(root, "nowhere to build the popup", layoutDid: 0u);
|
||||||
|
|
||||||
|
HoverAndDwell(root);
|
||||||
|
|
||||||
|
Assert.Empty(requests);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MissingP0x47_ShowsNothing_EvenWithRuntimeText()
|
||||||
|
{
|
||||||
|
// The popup ROOT id is retail's hard gate (@0x00460E44's
|
||||||
|
// GetAttribute_Enum(0x47) short-circuits the whole function).
|
||||||
|
var (root, _, requests) = CreateHarness();
|
||||||
|
var target = AddRuntimeTextTarget(root, "text but no popup root");
|
||||||
|
target.AuthoredTooltipRootElementId = 0u;
|
||||||
|
|
||||||
|
HoverAndDwell(root);
|
||||||
|
|
||||||
|
Assert.Empty(requests);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UiElement? FindById(UiElement root, uint datElementId)
|
||||||
|
{
|
||||||
|
if (root.DatElementId == datElementId) return root;
|
||||||
|
foreach (UiElement child in root.Children)
|
||||||
|
if (FindById(child, datElementId) is { } found) return found;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,57 @@ public sealed class TooltipLiveDatTests
|
||||||
$"expected at least 200 fully showable elements, found {withProperties.Count(f => f.Showable)}.");
|
$"expected at least 200 fully showable elements, found {withProperties.Count(f => f.Showable)}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// #409 live-failure round, the pin that protects the runtime-text fix.
|
||||||
|
/// The Options panel's authored toggle-row TEMPLATE (LayoutDesc
|
||||||
|
/// <c>0x2100002B</c>, row root <c>0x10000218</c>, checkbox leaf
|
||||||
|
/// <c>0x10000219</c>) carries the full tooltip POPUP LOCATOR
|
||||||
|
/// (<c>P0x47</c>/<c>P0x48</c>) and the <c>P0x4B</c> on-bit, but authors NO
|
||||||
|
/// <c>P0x49</c> text — its help string arrives at runtime, exactly as
|
||||||
|
/// retail's <c>UIOption_CheckboxBitfield64::CreateChildren @0x00485E65</c>
|
||||||
|
/// stamps its <c>siTooltip</c> array. This is why reading only
|
||||||
|
/// <c>AuthoredTooltipText</c> showed nothing anywhere in-world; see
|
||||||
|
/// <c>RetailTooltipPresenter.ResolveTooltipText</c>.
|
||||||
|
///
|
||||||
|
/// <para>The row template is template-list referenced, so
|
||||||
|
/// <see cref="LayoutImporter.ImportInfos"/>'s client-wide walk deliberately
|
||||||
|
/// filters it out (#375) — it has to be imported by its own root id.</para>
|
||||||
|
/// </summary>
|
||||||
|
[InstalledDatFact]
|
||||||
|
public void OptionsToggleRowTemplate_AuthorsThePopupLocatorButNoLiteralText()
|
||||||
|
{
|
||||||
|
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
||||||
|
|
||||||
|
ImportedLayout? row = LayoutImporter.Import(
|
||||||
|
dats, OptionsPanelLayoutId, OptionsToggleRowTemplateId, _ => (0u, 0, 0), null);
|
||||||
|
Assert.NotNull(row);
|
||||||
|
|
||||||
|
UiElement checkbox = Assert.Single(
|
||||||
|
AllWidgets(row!.Root), w => w.DatElementId == OptionsToggleCheckboxId);
|
||||||
|
|
||||||
|
Assert.Equal(0x10000397u, checkbox.AuthoredTooltipRootElementId); // P0x47
|
||||||
|
Assert.Equal(TooltipCatalogLayoutId, checkbox.AuthoredTooltipLayoutDid); // P0x48
|
||||||
|
Assert.True(checkbox.AuthoredTooltipEnabled); // P0x4B
|
||||||
|
Assert.True(string.IsNullOrEmpty(checkbox.AuthoredTooltipText)); // no P0x49
|
||||||
|
// ...and it is a real hover target, so UiRoot.UpdateHover can select it.
|
||||||
|
Assert.False(checkbox.ClickThrough);
|
||||||
|
// StartTooltipAtMouse @0x00460E7E's fallback source, stamped by the
|
||||||
|
// importer's dat shell.
|
||||||
|
Assert.Equal(OptionsPanelLayoutId, checkbox.SourceLayoutDid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private const uint OptionsPanelLayoutId = 0x2100002Bu;
|
||||||
|
private const uint OptionsToggleRowTemplateId = 0x10000218u;
|
||||||
|
private const uint OptionsToggleCheckboxId = 0x10000219u;
|
||||||
|
|
||||||
|
private static IEnumerable<UiElement> AllWidgets(UiElement root)
|
||||||
|
{
|
||||||
|
yield return root;
|
||||||
|
foreach (UiElement child in root.Children)
|
||||||
|
foreach (UiElement descendant in AllWidgets(child))
|
||||||
|
yield return descendant;
|
||||||
|
}
|
||||||
|
|
||||||
private static IEnumerable<ElementInfo> AllDescendants(ElementInfo root)
|
private static IEnumerable<ElementInfo> AllDescendants(ElementInfo root)
|
||||||
{
|
{
|
||||||
yield return root;
|
yield return root;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue