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;