diff --git a/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs b/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs index ec368803..90bbc822 100644 --- a/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs +++ b/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs @@ -842,6 +842,17 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory d.Actions.Selection, text => d.Communication.AddText(text, RetailLogTextType.ClientLocal)), Cursor: new RetailUiCursorBindings(cursorFeedback, cursorManager), + // #409 follow-on ("Item 2"): world-object hover tooltip. + // Reuses the SAME world-hover pick cursorFeedback's own + // worldTargetProvider already calls (UIElement_SmartBoxWrapper:: + // FindObject's 3D-raycast fallback — RetailWorldPicker's exact + // port) and the SAME ClientObjectTable name resolver + // ResolveWorldObjectName already uses elsewhere in this file. + WorldTooltip: new WorldTooltipRuntimeBindings( + HoverGuidAtCursor: () => late.Selection.PickAtCursor(includeSelf: true), + ResolveName: guid => d.Inventory.Objects.Get(guid)?.GetAppropriateName(), + Enabled: () => d.Character.Options.GetOptionBit( + CharacterOptionId.ShowTooltips)), Confirmations: new ConfirmationRuntimeBindings( (type, context, accepted) => late.Session.CurrentSession?.SendConfirmationResponse( diff --git a/src/AcDream.App/UI/Layout/RetailTooltipPresenter.cs b/src/AcDream.App/UI/Layout/RetailTooltipPresenter.cs index dd43c8fb..0ceab77f 100644 --- a/src/AcDream.App/UI/Layout/RetailTooltipPresenter.cs +++ b/src/AcDream.App/UI/Layout/RetailTooltipPresenter.cs @@ -173,20 +173,36 @@ public sealed class RetailTooltipPresenter : IDisposable if (layoutDid == 0u) return; + if (TryBuildAndMountPopup(widget.AuthoredTooltipRootElementId, layoutDid, tooltipText!)) + _owner = widget; + } + + /// + /// Shared popup-build/mount body for BOTH tooltip triggers this class + /// owns: the UI-element hover-dwell path above (, + /// per-widget authored P0x47/P0x48) and the world-object + /// hover path below (, the fixed + /// popup-skin pair every game-code SetTooltip caller in this + /// family resolves to). Extracted unchanged from the pre-#411-follow-on + /// OnTooltipShow body — same F4/F5/F8 fixes, same failure + /// handling. + /// + private bool TryBuildAndMountPopup(uint rootElementId, uint layoutDid, string tooltipText) + { ImportedLayout? layout; try { - layout = _createLayout(layoutDid, widget.AuthoredTooltipRootElementId); + layout = _createLayout(layoutDid, rootElementId); } catch (Exception error) { Console.WriteLine( $"[UI] #409 tooltip popup layout=0x{layoutDid:X8} " - + $"root=0x{widget.AuthoredTooltipRootElementId:X8} failed to build: {error.Message}"); - return; + + $"root=0x{rootElementId:X8} failed to build: {error.Message}"); + return false; } if (layout is null) - return; + return false; UiElement root = layout.Root; UiElement? textChild = root.AuthoredTooltipTextChildElementId != 0u @@ -202,7 +218,7 @@ public sealed class RetailTooltipPresenter : IDisposable // show an empty, unsized 30x30 bevel artifact instead of retail's // silent no-op. if (textChild is not UiText text) - return; + return false; // F4: null the per-frame anchor recompute on BOTH the popup root and // its text child before resizing, the same shape the sibling @@ -216,7 +232,7 @@ public sealed class RetailTooltipPresenter : IDisposable text.LayoutPolicy = null; text.Anchors = AnchorEdges.None; - ApplyTooltipText(root, text, tooltipText!); + ApplyTooltipText(root, text, tooltipText); SetClickThroughRecursive(root); PositionAtMouse(root); @@ -224,7 +240,7 @@ public sealed class RetailTooltipPresenter : IDisposable _host.AddChild(root); _host.BringToFront(root); _popupRoot = root; - _owner = widget; + return true; } private void OnTooltipHide(UiElement widget) @@ -240,6 +256,122 @@ public sealed class RetailTooltipPresenter : IDisposable _host.RemoveChild(_popupRoot); _popupRoot = null; _owner = null; + _worldTooltipShowing = false; + } + + // ── World-object hover tooltip (docs/ISSUES.md #409 follow-on) ───────── + // + // Port of UIElement_SmartBoxWrapper::RecvNotice_SmartBoxObjectFound + // @0x004E5AD0's tooltip half (@0x004E5D13-@0x004E5E00). Unlike the + // dwell-timer UI-element path above, this trigger is EDGE-fired: retail + // calls SetTooltip + StartTooltipAtMouse IMMEDIATELY when SmartBox's + // found-object id CHANGES (@0x004E5D74/@0x004E5DFB) — no dwell wait — + // gated per-edge by PlayerModule::ShowTooltips (@0x004E5D21, + // CharacterOptionId.ShowTooltips in this port's CharacterOptionTable). + // The text is ACCWeenieObject::GetObjectName(id, NAME_APPROPRIATE, 0) + // (@0x004E5D3B) — the SAME call UIElement_UIItem::UpdateTooltip uses, + // but WITHOUT that item-cell's separate stack-count "%d %s" prefix + // (RecvNotice_SmartBoxObjectFound's own text-building block has no + // count logic at all — a real, decomp-confirmed asymmetry versus + // UiItemSlot's GetTooltipDisplayName). + // + // The found-object id itself comes from UIElement_SmartBoxWrapper:: + // FindObject @0x004E5430, called every frame from Global_Loop + // @0x004E5620 using the CURRENT mouse position regardless of what has + // input focus. FindObject special-cases m_pElementLastOver casting to + // UIElement_UIItem (SmartBox::set_found_object(itemID) — item cells + // own their own answer, ported as UiItemSlot.GetTooltipText, Item 1); + // otherwise it runs the ordinary 3D raycast even under non-item UI + // chrome. This port narrows that second branch to "no UI element + // hovered at all" (see WorldHoverGuidProvider's own doc) rather than + // reproducing the raycast-under-windows edge case. + // + // UIElement_SmartBoxWrapper is registered class 0x10000030 + // (Register @0x0047A47E) but an exhaustive live-DAT sweep + // (TooltipLiveDatTests.SmartBoxWrapper_HasNoAuthoredElementDesc_ + // AnywhereInstalled) found ZERO elements of that type anywhere + // installed — unlike the UIItem catalog's 49 standalone template + // prototypes, the 3D-viewport wrapper is evidently constructed + // directly by gmGamePlayUI's own mode setup rather than from a + // walkable authored ElementDesc, so its own P0x47/P0x48 cannot be + // read from the DAT. This port therefore REUSES the item catalog's + // confirmed uniform popup-locator pair (WorldPopupRootElementId/ + // WorldPopupLayoutDid below) — the SAME "generic runtime-text" skin + // every other game-code SetTooltip caller in this family draws from — + // as the best-evidenced inference for the unrecoverable constant. + + /// Same popup skin every UIItem prototype resolves to + /// ('s own ItemTooltipRootElementId) — + /// see this section's own doc note on why the exact value cannot be + /// read off an authored UIElement_SmartBoxWrapper ElementDesc. + private const uint WorldPopupRootElementId = 0x10000395u; + private const uint WorldPopupLayoutDid = 0x21000041u; + + private uint _worldHoverGuid; + private bool _worldTooltipShowing; + + /// + /// The world-hover pick (retail's SmartBox::find_object via + /// UIElement_SmartBoxWrapper::FindObject @0x004E5430's fallback + /// branch) — a per-frame "what's under the cursor right now" query, + /// distinct from click-driven SelectionState. Queried only when + /// finds no UI element under the cursor + /// (mirrors FindObject's m_pElementLastOver check, narrowed + /// per this section's own doc note). Null/unset disables the whole + /// world-hover path (no world tooltip, matching a client build that + /// never wires it). + /// + public Func? WorldHoverGuidProvider { get; set; } + + /// + /// ACCWeenieObject::GetObjectName(id, NAME_APPROPRIATE, 0) — + /// resolve the found guid's display name. Returning null/empty shows no + /// tooltip for that guid (matching retail's own non-empty-string guard + /// at @0x004E5D48). + /// + public Func? WorldHoverNameResolver { get; set; } + + /// + /// PlayerModule::ShowTooltips (CharacterOptionId.ShowTooltips) — + /// read once per found-object edge, exactly where retail reads it + /// (@0x004E5D21). A mid-hover option toggle takes effect at the + /// NEXT found-object change, matching retail's own edge-only + /// re-evaluation rather than a live per-frame re-check. + /// + public Func? WorldTooltipsEnabled { get; set; } + + private void UpdateWorldHoverTooltip() + { + if (WorldHoverGuidProvider is null) + return; + + uint found = _host.Pick(_host.MouseX, _host.MouseY) is null + ? WorldHoverGuidProvider() ?? 0u + : 0u; + + if (found == _worldHoverGuid) + return; // no change -> RecvNotice_SmartBoxObjectFound never re-fires + _worldHoverGuid = found; + + if (found == 0u) + { + if (_worldTooltipShowing) + RemovePopup(); + return; + } + + if (WorldTooltipsEnabled?.Invoke() != true) + return; + + string? text = WorldHoverNameResolver?.Invoke(found); + if (string.IsNullOrEmpty(text)) + return; + + // A UI-element popup cannot be showing here: UiRoot's own hover + // (queried above) is null whenever this branch runs, so its dwell + // timer never arms and OnTooltipShow never fires concurrently. + if (TryBuildAndMountPopup(WorldPopupRootElementId, WorldPopupLayoutDid, text)) + _worldTooltipShowing = true; } /// Force-hides whatever tooltip is currently showing, if any. @@ -371,6 +503,8 @@ public sealed class RetailTooltipPresenter : IDisposable { if (_popupRoot is not null) _host.BringToFront(_popupRoot); + + UpdateWorldHoverTooltip(); } public void Dispose() diff --git a/src/AcDream.App/UI/RetailUiRuntime.cs b/src/AcDream.App/UI/RetailUiRuntime.cs index 15961ae3..64304dfd 100644 --- a/src/AcDream.App/UI/RetailUiRuntime.cs +++ b/src/AcDream.App/UI/RetailUiRuntime.cs @@ -333,6 +333,17 @@ public sealed record RetailUiCursorBindings( CursorFeedbackController Feedback, RetailCursorManager Manager); +/// +/// #409 follow-on (docs/ISSUES.md "Item 2"): world-object hover tooltip +/// bindings for 's world-hover half — see +/// that class's own doc note on UIElement_SmartBoxWrapper:: +/// RecvNotice_SmartBoxObjectFound @0x004E5AD0. +/// +public sealed record WorldTooltipRuntimeBindings( + Func HoverGuidAtCursor, + Func ResolveName, + Func Enabled); + public sealed record ConfirmationRuntimeBindings( Action SendResponse); @@ -435,6 +446,7 @@ public sealed record RetailUiRuntimeBindings( ExternalContainerRuntimeBindings ExternalContainer, VendorRuntimeBindings Vendor, RetailUiCursorBindings Cursor, + WorldTooltipRuntimeBindings WorldTooltip, ConfirmationRuntimeBindings Confirmations, AppraisalRuntimeBindings Appraisal, OptionsRuntimeBindings Options, @@ -3366,7 +3378,15 @@ public sealed class RetailUiRuntime : IDisposable } } - TooltipPresenter = new RetailTooltipPresenter(Host.Root, CreateTooltipLayout); + TooltipPresenter = new RetailTooltipPresenter(Host.Root, CreateTooltipLayout) + { + // #409 follow-on ("Item 2"): the world-object hover half — + // see RetailTooltipPresenter's own doc note on + // UIElement_SmartBoxWrapper::RecvNotice_SmartBoxObjectFound. + WorldHoverGuidProvider = () => _bindings.WorldTooltip.HoverGuidAtCursor(), + WorldHoverNameResolver = _bindings.WorldTooltip.ResolveName, + WorldTooltipsEnabled = () => _bindings.WorldTooltip.Enabled(), + }; if (_bindings.Chat.Store is { } store) { diff --git a/tests/AcDream.App.Tests/UI/Layout/RetailTooltipPresenterTests.cs b/tests/AcDream.App.Tests/UI/Layout/RetailTooltipPresenterTests.cs index 18b69384..4095519f 100644 --- a/tests/AcDream.App.Tests/UI/Layout/RetailTooltipPresenterTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/RetailTooltipPresenterTests.cs @@ -589,4 +589,148 @@ public sealed class RetailTooltipPresenterTests if (FindById(child, datElementId) is { } found) return found; return null; } + + // ── World-object hover tooltip (docs/ISSUES.md #409 follow-on) ───────── + // Port of UIElement_SmartBoxWrapper::RecvNotice_SmartBoxObjectFound + // @0x004E5AD0: edge-fired (no dwell wait), gated by PlayerModule:: + // ShowTooltips, uses the fixed popup-skin pair every game-code + // SetTooltip caller in this family shares (see RetailTooltipPresenter's + // own doc note on why UIElement_SmartBoxWrapper's own P0x47/P0x48 + // cannot be read from the installed DAT). + + private const uint WorldFoundGuid = 0x80000123u; + + [Fact] + public void WorldHover_ShowsImmediately_NoDwellWait() + { + var (root, presenter, requests) = CreateHarness(); + presenter.WorldHoverGuidProvider = () => WorldFoundGuid; + presenter.WorldHoverNameResolver = guid => guid == WorldFoundGuid ? "A Drudge" : null; + presenter.WorldTooltipsEnabled = () => true; + int childrenBefore = root.Children.Count; + + // A single Tick — no root.Tick dwell timer involved at all, unlike + // every UI-element case above. + presenter.Tick(); + + Assert.Equal(childrenBefore + 1, root.Children.Count); + Assert.Single(requests, r => r == (0x21000041u, 0x10000395u)); + } + + [Fact] + public void WorldHover_HidesWhenTheFoundGuidClears() + { + var (root, presenter, _) = CreateHarness(); + uint? found = WorldFoundGuid; + presenter.WorldHoverGuidProvider = () => found; + presenter.WorldHoverNameResolver = _ => "A Drudge"; + presenter.WorldTooltipsEnabled = () => true; + int childrenBefore = root.Children.Count; + + presenter.Tick(); + Assert.Equal(childrenBefore + 1, root.Children.Count); + + found = null; + presenter.Tick(); + + Assert.Equal(childrenBefore, root.Children.Count); + } + + [Fact] + public void WorldHover_ShowTooltipsOff_ShowsNothing() + { + // PlayerModule::ShowTooltips @0x004E5D21 gates the whole block — + // UpdateCursorState (the found-cursor swap) is NOT gated by it, but + // that is a separate mechanism this presenter does not own. + var (root, presenter, requests) = CreateHarness(); + presenter.WorldHoverGuidProvider = () => WorldFoundGuid; + presenter.WorldHoverNameResolver = _ => "A Drudge"; + presenter.WorldTooltipsEnabled = () => false; + int childrenBefore = root.Children.Count; + + presenter.Tick(); + + Assert.Empty(requests); + Assert.Equal(childrenBefore, root.Children.Count); + } + + [Fact] + public void WorldHover_NoNameResolved_ShowsNothing() + { + var (_, presenter, requests) = CreateHarness(); + presenter.WorldHoverGuidProvider = () => WorldFoundGuid; + presenter.WorldHoverNameResolver = _ => null; + presenter.WorldTooltipsEnabled = () => true; + + presenter.Tick(); + + Assert.Empty(requests); + } + + [Fact] + public void WorldHover_SuppressedWhileHoveringAUiElement() + { + // FindObject @0x004E5430: m_pElementLastOver != null routes through + // the UI-item special case or falls through to the 3D raycast — + // either way the found-object pipeline here must not also fire for + // whatever the mouse is currently over. This port narrows that to + // "no UI element hovered at all" (see the class's own doc note). + var (root, presenter, requests) = CreateHarness(); + var uiElement = new HoverTarget { Left = 100, Top = 100, Width = 40, Height = 20 }; + root.AddChild(uiElement); + root.OnMouseMove(110, 110); + + presenter.WorldHoverGuidProvider = () => WorldFoundGuid; + presenter.WorldHoverNameResolver = _ => "A Drudge"; + presenter.WorldTooltipsEnabled = () => true; + + presenter.Tick(); + + Assert.Empty(requests); + } + + [Fact] + public void WorldHover_ReEvaluatesGateAndTextOnlyOnTheFoundGuidEdge() + { + // RecvNotice_SmartBoxObjectFound only re-runs when SmartBox:: + // set_found_object's target actually changes — a per-frame poll of + // the SAME found id must not re-read ShowTooltips or re-resolve the + // name every tick. + var (_, presenter, requests) = CreateHarness(); + int gateReads = 0, nameReads = 0; + presenter.WorldHoverGuidProvider = () => WorldFoundGuid; + presenter.WorldHoverNameResolver = _ => { nameReads++; return "A Drudge"; }; + presenter.WorldTooltipsEnabled = () => { gateReads++; return true; }; + + presenter.Tick(); + presenter.Tick(); + presenter.Tick(); + + Assert.Equal(1, gateReads); + Assert.Equal(1, nameReads); + Assert.Single(requests); + } + + [Fact] + public void WorldHover_TextIsPlainAppropriateName_NoStackCountPrefix() + { + // RecvNotice_SmartBoxObjectFound's own text-building block + // (@0x004E5D3B-@0x004E5D74) has no "%d %s" stack-count logic — + // unlike UIElement_UIItem::UpdateTooltip's item-cell tooltip. The + // resolver contract here is plain GetAppropriateName, not + // GetTooltipDisplayName; this pin just documents the caller's + // resolver is free to return whatever plain text it wants and the + // presenter applies it verbatim (no separate count formatting is + // ever added by this class). + var (root, presenter, _) = CreateHarness(); + presenter.WorldHoverGuidProvider = () => WorldFoundGuid; + presenter.WorldHoverNameResolver = _ => "Iron Bars"; + presenter.WorldTooltipsEnabled = () => true; + + presenter.Tick(); + + UiElement popup = Assert.Single(root.Children); + UiElement? textChild = FindById(popup, TextChildId); + Assert.IsType(textChild); + } } diff --git a/tests/AcDream.App.Tests/UI/Layout/TooltipLiveDatTests.cs b/tests/AcDream.App.Tests/UI/Layout/TooltipLiveDatTests.cs index 19c7a2fa..326b618b 100644 --- a/tests/AcDream.App.Tests/UI/Layout/TooltipLiveDatTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/TooltipLiveDatTests.cs @@ -217,6 +217,125 @@ public sealed class TooltipLiveDatTests 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;