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;
///
/// #409 (client-wide retail tooltip system) installed-DAT acceptance gate.
/// Opt in with ACDREAM_PROBE_LIVE_MOUNT=1; ACDREAM_DAT_DIR can
/// override the ordinary Documents/Asheron's Call location. Pins retail's
/// tooltip popup LayoutDesc 0x21000041 structure and a client-wide
/// sweep landmark, mirroring 's
/// pattern.
///
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");
/// 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).
private const uint TooltipCatalogLayoutId = 0x21000041u;
/// The four popup "skin" root element ids live-DAT-probed inside
/// . Each is a 30x30 four-piece bevel
/// frame around the SAME text child id ().
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(textChild);
}
///
/// 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
/// can show end to end.
///
[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);
}
///
/// Landmark client-wide sweep (mirrors
/// '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.
///
[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())
{
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)}.");
}
///
/// #409 live-failure round, the pin that protects the runtime-text fix.
/// The Options panel's authored toggle-row TEMPLATE (LayoutDesc
/// 0x2100002B, row root 0x10000218, checkbox leaf
/// 0x10000219) carries the full tooltip POPUP LOCATOR
/// (P0x47/P0x48) and the P0x4B on-bit, but authors NO
/// P0x49 text — its help string arrives at runtime, exactly as
/// retail's UIOption_CheckboxBitfield64::CreateChildren @0x00485E65
/// stamps its siTooltip array. This is why reading only
/// AuthoredTooltipText showed nothing anywhere in-world; see
/// RetailTooltipPresenter.ResolveTooltipText.
///
/// The row template is template-list referenced, so
/// 's client-wide walk deliberately
/// filters it out (#375) — it has to be imported by its own root id.
///
[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;
/// Retail's UIElement_UIItem registered class id
/// (UIElement_UIItem::Register @0x0047A488:
/// RegisterElementClass(0x10000032, ...)). The catalog also holds a
/// handful of type-3 (plain UIRegion) housekeeping elements used only as
/// BaseElement bases for the real prototypes below — never selected by any
/// list's own cell-template attribute, so they are excluded from this
/// scan.
private const uint UiItemElementType = 0x10000032u;
///
/// Item-tooltip investigation (docs/ISSUES.md #409 follow-on): pins the
/// finding that justifies UiItemSlot hardcoding a single popup-
/// locator pair rather than reading it per prototype. Every standalone
/// UIItem prototype (type 0x10000032) in the shared cell-template
/// catalog (, LayoutDesc
/// 0x21000037) resolves the SAME P0x47/P0x48 pair through
/// catalog inheritance — 0x10000395 within 0x21000041, one of
/// the four popup skins
/// already pins. None author literal P0x49 text or the P0x4B
/// on-bit — matching retail's runtime-text game-code sites
/// (UIElement_UIItem::UpdateTooltip @0x004E1CB0 sets both the text
/// and the on-bit itself; see 's
/// fromRuntime bypass).
///
[InstalledDatFact]
public void UiItemCatalog_EveryPrototype_SharesTheSamePopupLocator()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
ElementInfo? catalog = LayoutImporter.ImportInfos(dats, ItemListCellTemplate.CatalogLayoutId);
Assert.NotNull(catalog);
Assert.True(catalog!.Children.Count >= 40,
$"expected the shared UIItem catalog to hold dozens of prototypes, found {catalog.Children.Count}.");
var prototypes = catalog.Children.Where(c => c.Type == UiItemElementType).ToList();
Assert.True(prototypes.Count >= 30,
$"expected the shared UIItem catalog to hold dozens of type-0x10000032 prototypes, found {prototypes.Count}.");
foreach (ElementInfo prototype in prototypes)
{
Assert.True(prototype.TooltipRootElementId == UiItemTooltipRootElementId,
$"prototype 0x{prototype.Id:X8} authors P0x47=0x{prototype.TooltipRootElementId:X8}, expected 0x{UiItemTooltipRootElementId:X8}.");
Assert.True(prototype.TooltipLayoutDid == TooltipCatalogLayoutId,
$"prototype 0x{prototype.Id:X8} authors P0x48=0x{prototype.TooltipLayoutDid:X8}, expected 0x{TooltipCatalogLayoutId:X8}.");
Assert.False(prototype.TooltipText.HasValue,
$"prototype 0x{prototype.Id:X8} unexpectedly authors literal P0x49 tooltip text.");
}
// Concrete owning lists select DIFFERENT prototype ids (attribute
// 0x1000000E) for their own cell shape, but every one of those
// prototypes still resolves the same popup locator above — so
// UiItemSlot's hardcoded pair is correct regardless of which list
// spawned the cell.
ElementInfo? inventoryTree = LayoutImporter.ImportInfos(dats, 0x21000023u);
ElementInfo? contentsGrid = inventoryTree is null
? null : AllDescendants(inventoryTree).FirstOrDefault(x => x.Id == 0x100001C6u);
Assert.NotNull(contentsGrid);
Assert.True(contentsGrid!.TryGetEffectiveProperty(0x1000000Eu, out UiPropertyValue protoProp));
Assert.NotEqual(0u, (uint)protoProp.UnsignedValue);
ElementInfo? toolbarTree = LayoutImporter.ImportInfos(dats, 0x21000016u);
ElementInfo? toolbarSlot = toolbarTree is null
? null : AllDescendants(toolbarTree).FirstOrDefault(x => x.Id == 0x100001A7u);
Assert.NotNull(toolbarSlot);
Assert.True(toolbarSlot!.TryGetEffectiveProperty(0x1000000Eu, out UiPropertyValue toolbarProtoProp));
Assert.NotEqual(0u, (uint)toolbarProtoProp.UnsignedValue);
// Different lists really do select different prototypes.
Assert.NotEqual((uint)protoProp.UnsignedValue, (uint)toolbarProtoProp.UnsignedValue);
}
///
/// World-object tooltip investigation (docs/ISSUES.md #409 follow-on).
/// UIElement_SmartBoxWrapper (retail's registered class
/// 0x10000030, UIElement_SmartBoxWrapper::Register @0x0047A47E)
/// is the caller of the world-hover tooltip's own
/// UIElement::SetTooltip/StartTooltipAtMouse pair
/// (RecvNotice_SmartBoxObjectFound @0x004E5AD0, calls at
/// @0x004E5D74/@0x004E5DFB) — but this exhaustive sweep of
/// every installed LayoutDesc found ZERO elements of that type
/// anywhere. Unlike the UIItem catalog (49 standalone template
/// prototypes, all authoring the SAME popup locator), the 3D-viewport
/// wrapper is evidently constructed directly by game code
/// (gmGamePlayUI's own mode setup) rather than from a walkable
/// authored ElementDesc, so its own P0x47/P0x48
/// cannot be read from the DAT the way every other tooltip trigger's
/// can. 's world-hover popup therefore
/// REUSES the item-catalog's confirmed uniform pair
/// (P0x47=0x10000395/P0x48=0x21000041) — the SAME "generic
/// runtime-text" skin every other game-code SetTooltip caller in
/// this family (items, the Options checkboxes, the radar) draws from —
/// as the best-evidenced inference rather than leaving world-object
/// tooltips unimplemented over one unrecoverable hex constant.
///
[InstalledDatFact]
public void SmartBoxWrapper_HasNoAuthoredElementDesc_AnywhereInstalled()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
int found = 0;
foreach (uint layoutId in dats.GetAllIdsOfType())
{
ElementInfo? tree;
try { tree = LayoutImporter.ImportInfos(dats, layoutId); }
catch { continue; }
if (tree is null) continue;
found += AllDescendants(tree).Count(e => e.Type == SmartBoxWrapperElementType);
}
Assert.Equal(0, found);
}
/// Retail's UIElement_SmartBoxWrapper registered class id.
private const uint SmartBoxWrapperElementType = 0x10000030u;
/// The item-cell popup-locator P0x47, hardcoded onto every
/// — see that class's own doc comment.
private const uint UiItemTooltipRootElementId = 0x10000395u;
private static IEnumerable AllWidgets(UiElement root)
{
yield return root;
foreach (UiElement child in root.Children)
foreach (UiElement descendant in AllWidgets(child))
yield return descendant;
}
private static IEnumerable AllDescendants(ElementInfo root)
{
yield return root;
foreach (ElementInfo child in root.Children)
foreach (ElementInfo descendant in AllDescendants(child))
yield return descendant;
}
}