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>
169 lines
7.3 KiB
C#
169 lines
7.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)>();
|
|
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)
|
|
withProperties.Add((layoutId, e.Id, e.TooltipText.HasValue));
|
|
}
|
|
}
|
|
|
|
Console.WriteLine($"[409-DAT] {withProperties.Count} elements author >=1 tooltip property "
|
|
+ $"({withProperties.Count(f => f.HasText)} with literal StringInfo text).");
|
|
|
|
// #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 434 total / 243 with literal text at
|
|
// filing time — assert comfortably below both 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)}.");
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|