acdream/tests/AcDream.App.Tests/UI/Layout/TooltipLiveDatTests.cs
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

184 lines
8.3 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)}.");
}
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;
}
}