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; } }