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>
235 lines
11 KiB
C#
235 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using AcDream.App.UI;
|
|
using AcDream.App.UI.Layout;
|
|
using AcDream.Content;
|
|
using DatReaderWriter;
|
|
using DatReaderWriter.DBObjs;
|
|
using DatReaderWriter.Options;
|
|
|
|
namespace AcDream.App.Tests.UI.Layout;
|
|
|
|
/// <summary>
|
|
/// #409 (client-wide retail tooltip system) installed-DAT acceptance gate.
|
|
/// Opt in with <c>ACDREAM_PROBE_LIVE_MOUNT=1</c>; <c>ACDREAM_DAT_DIR</c> can
|
|
/// override the ordinary Documents/Asheron's Call location. Pins retail's
|
|
/// tooltip popup LayoutDesc <c>0x21000041</c> structure and a client-wide
|
|
/// sweep landmark, mirroring <see cref="CharacterManagementLiveDatTests"/>'s
|
|
/// pattern.
|
|
/// </summary>
|
|
public sealed class TooltipLiveDatTests
|
|
{
|
|
private static string DatDirectory =>
|
|
System.Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR")
|
|
?? Path.Combine(
|
|
System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile),
|
|
"Documents",
|
|
"Asheron's Call");
|
|
|
|
/// <summary>The tooltip popup catalog LayoutDesc — every probed
|
|
/// tooltip-bearing element's P0x48 resolves here (with one exception,
|
|
/// 0x21000026, an alternate skin used by a handful of elements —
|
|
/// RetailTooltipPresenter is data-driven off each element's OWN P0x48
|
|
/// so neither this constant nor 0x21000026 is hardcoded in production
|
|
/// code; it is cited here only to pin the probe's own finding).</summary>
|
|
private const uint TooltipCatalogLayoutId = 0x21000041u;
|
|
|
|
/// <summary>The four popup "skin" root element ids live-DAT-probed inside
|
|
/// <see cref="TooltipCatalogLayoutId"/>. Each is a 30x30 four-piece bevel
|
|
/// frame around the SAME text child id (<see cref="TooltipTextChildId"/>).</summary>
|
|
private static readonly uint[] PopupSkinRootIds =
|
|
[0x10000487u, 0x10000395u, 0x10000397u, 0x10000398u];
|
|
|
|
private const uint TooltipTextChildId = 0x10000396u;
|
|
|
|
[InstalledDatFact]
|
|
public void TooltipCatalog_EveryPopupSkin_SharesTheSameTextChild()
|
|
{
|
|
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
|
|
|
ElementInfo? root = LayoutImporter.ImportInfos(dats, TooltipCatalogLayoutId);
|
|
Assert.NotNull(root);
|
|
Assert.Equal(0u, root!.Id);
|
|
Assert.Equal(800f, root.Width);
|
|
Assert.Equal(600f, root.Height);
|
|
|
|
foreach (uint skinRootId in PopupSkinRootIds)
|
|
{
|
|
ElementInfo skin = Assert.Single(root.Children, c => c.Id == skinRootId);
|
|
// UIElementManager::StartTooltip @0x0045DE90's own fallback read:
|
|
// GetAttribute_Enum(tooltipRootElement, 0x4a, &textChildId) off the
|
|
// freshly-instantiated POPUP root itself.
|
|
Assert.Equal(TooltipTextChildId, skin.TooltipTextChildElementId);
|
|
|
|
ElementInfo textChild = Assert.Single(
|
|
AllDescendants(skin), e => e.Id == TooltipTextChildId);
|
|
Assert.Equal(12u, textChild.Type); // UIElement_Text
|
|
}
|
|
}
|
|
|
|
[InstalledDatFact]
|
|
public void TooltipCatalog_ImportsThroughLayoutImporter_WithTextChildResolvable()
|
|
{
|
|
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
|
|
|
ImportedLayout? popup = LayoutImporter.Import(
|
|
dats,
|
|
TooltipCatalogLayoutId,
|
|
0x10000487u,
|
|
_ => (0u, 0, 0),
|
|
null);
|
|
Assert.NotNull(popup);
|
|
Assert.Equal(30f, popup!.Root.Width);
|
|
Assert.Equal(30f, popup.Root.Height);
|
|
Assert.Equal(TooltipTextChildId, popup.Root.AuthoredTooltipTextChildElementId);
|
|
|
|
UiElement? textChild = popup.FindElement(TooltipTextChildId);
|
|
Assert.IsType<UiText>(textChild);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A known tooltip-bearing element (the Appearance page's left rotate
|
|
/// button, live-DAT-probed) authors all five trigger properties with
|
|
/// literal StringInfo text — the "core" case
|
|
/// <see cref="RetailTooltipPresenter"/> can show end to end.
|
|
/// </summary>
|
|
[InstalledDatFact]
|
|
public void KnownElement_AuthorsAllFiveTooltipProperties_WithResolvableText()
|
|
{
|
|
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
|
|
|
ElementInfo? tree = LayoutImporter.ImportInfos(dats, 0x21000005u);
|
|
Assert.NotNull(tree);
|
|
ElementInfo rotateLeft = Assert.Single(AllDescendants(tree!), e => e.Id == 0x100005A4u);
|
|
|
|
Assert.True(rotateLeft.TooltipEnabled);
|
|
Assert.Equal(0x10000487u, rotateLeft.TooltipRootElementId);
|
|
Assert.Equal(TooltipCatalogLayoutId, rotateLeft.TooltipLayoutDid);
|
|
Assert.NotNull(rotateLeft.TooltipText);
|
|
|
|
var strings = new DatStringResolver(dats);
|
|
string? resolved = DatWidgetFactory.ResolveTooltipText(rotateLeft, strings.Resolve);
|
|
Assert.Equal("Rotate left.", resolved);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Landmark client-wide sweep (mirrors
|
|
/// <see cref="LayoutImporterMediaBearingChildSweepTests"/>'s own shape):
|
|
/// every installed LayoutDesc, counting elements authoring at least one
|
|
/// of the five tooltip-trigger properties. Asserts landmarks + a floor,
|
|
/// not a brittle exact total, so the gate survives a future DAT
|
|
/// revision.
|
|
/// </summary>
|
|
[InstalledDatFact]
|
|
public void ClientWideSweep_FindsKnownLandmarksAndAFloorCount()
|
|
{
|
|
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
|
|
|
var withProperties = new List<(uint LayoutId, uint ElementId, bool HasText, bool Showable)>();
|
|
foreach (uint layoutId in dats.GetAllIdsOfType<LayoutDesc>())
|
|
{
|
|
ElementInfo? tree;
|
|
try { tree = LayoutImporter.ImportInfos(dats, layoutId); }
|
|
catch { continue; }
|
|
if (tree is null) continue;
|
|
|
|
foreach (ElementInfo e in AllDescendants(tree))
|
|
{
|
|
bool any = e.TooltipRootElementId != 0 || e.TooltipLayoutDid != 0
|
|
|| e.TooltipText.HasValue || e.TooltipEnabled
|
|
|| e.TooltipDelaySeconds.HasValue;
|
|
if (any)
|
|
{
|
|
// F9 (2026-08-16 review round): "with literal text" alone
|
|
// over-counts what RetailTooltipPresenter.OnTooltipShow
|
|
// actually shows — that gate is a FULL AND across
|
|
// TooltipEnabled (P0x4B) AND non-null text (P0x49) AND
|
|
// both popup-locator ids (P0x47/P0x48), not just text
|
|
// presence. Measure the real intersection instead of
|
|
// assuming "243 with text" == "243 showable".
|
|
bool showable = e.TooltipEnabled && e.TooltipText.HasValue
|
|
&& e.TooltipLayoutDid != 0 && e.TooltipRootElementId != 0;
|
|
withProperties.Add((layoutId, e.Id, e.TooltipText.HasValue, showable));
|
|
}
|
|
}
|
|
}
|
|
|
|
Console.WriteLine($"[409-DAT] {withProperties.Count} elements author >=1 tooltip property "
|
|
+ $"({withProperties.Count(f => f.HasText)} with literal StringInfo text, "
|
|
+ $"{withProperties.Count(f => f.Showable)} pass the full OnTooltipShow gate).");
|
|
|
|
// #409 investigation landmark (main game UI, Appearance rotate button).
|
|
Assert.Contains(withProperties, f => f.LayoutId == 0x21000005u && f.ElementId == 0x100005A4u);
|
|
// Floor: the live-DAT sweep found 430 total / 243 with literal text /
|
|
// 243 fully showable (2026-08-16, both post-F9 measurements) —
|
|
// assert comfortably below all three so a future content patch that
|
|
// only ADDS tooltip authoring cannot flake this gate.
|
|
Assert.True(withProperties.Count >= 400,
|
|
$"expected at least 400 tooltip-property-authoring elements, found {withProperties.Count}.");
|
|
Assert.True(withProperties.Count(f => f.HasText) >= 200,
|
|
$"expected at least 200 elements with literal tooltip text, found {withProperties.Count(f => f.HasText)}.");
|
|
Assert.True(withProperties.Count(f => f.Showable) >= 200,
|
|
$"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)
|
|
{
|
|
yield return root;
|
|
foreach (ElementInfo child in root.Children)
|
|
foreach (ElementInfo descendant in AllDescendants(child))
|
|
yield return descendant;
|
|
}
|
|
}
|