fix(ui): Map tab player/house icon resolution — build the button-swallowed icons from the panel-slot resolve tree, detached from the per-frame layout pass

Two mechanisms, both live-verified (register row AD-108 updated to match):

1. RESOLUTION. The player/house icons (0x100001ED/0x100001EE) are authored
   as nested dat children of m_pMap (0x100001EC), itself a Type-1 button
   whose UiButton.ConsumesDatChildren swallows them at build. The old
   ResolveSwallowedIcon re-imported them standalone via
   ImportInfos(hostLayout, iconId) — which returns null on the live DAT:
   FindDesc walks the raw top-level Elements table (one entry for
   0x2100006E) and never reaches them. Their ElementInfos only materialize
   inside the full panel-slot resolve (ImportInfos(0x2100006E, 0x1000018C))
   that MountMapHousePanel already imports — the pageInfo Bind already
   receives. The fix finds each icon's info under m_pMap's own resolved
   info subtree and BUILDS it through the new Bindings.IconBuilder seam
   (production: LayoutImporter.Build under the DAT lock — the build half
   of RowTemplateResolver's shape). An icon the normal walk DID build is
   preferred (FindDescendant first), so a future ConsumesDatChildren
   policy change cannot double-build.

2. POSITION. Found by this fix's own F1 live verification: the resolved
   ring rendered pinned to m_pMap's top-left. PlaceMarker owns marker
   position outright (retail's gmMapUI::Update re-places every tick;
   retail's UpdateForParentSizeChange runs only on real parent resize),
   but acdream re-runs ApplyAnchor per frame and the icon's compatibility
   anchor had captured the authored (0,0) rect while the panel was still
   hidden, re-asserting it over PlaceMarker's writes every frame.
   PrepareIcon now sets Anchors=None (clearing any imported LayoutPolicy),
   the established runtime-positioned-element convention.

Live numeric gate (session character +Acdream, cell 0xF07E003F):
independent computation (gid_to_lcoord -> display (90.8E, 0.5S) ->
byte-decoded PlaceMarkerOnMap formula, 17x16 icon, marker area
(6,8)-(247,258)) predicts local pixel (226,125); the connected client's
UI-tree dump shows the icon at screen (1166,195) under m_pMap (940,70) =
local (226,125) — exact match in both panel-open dumps. Coordinate text
"0.5S,90.8E", Holtburg town-marker tooltip (real-mouse hover), and the
House tab's "You may buy another house immediately." sentence all
confirmed on screen; ACE-confirmed graceful logout.

New pin: MapHousePanelLiveDatMountTests ([InstalledDatFact]) reproduces
the production mount recipe against the installed DATs — the test that
would have caught this at Batch C: pins the cold-import null, the
panel-slot resolution of both icons with non-degenerate extents, AND
that PlaceMarker's writes survive the per-frame ApplyAnchor pass.

Gates: Release build green; App suite (live-DAT mode) 5479/3 skips
(baseline 5478 + the new pin); Runtime 1744/0; full solution green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-17 05:58:17 +02:00
parent 38c580ff6d
commit e316e190cb
6 changed files with 289 additions and 42 deletions

View file

@ -17,17 +17,14 @@ namespace AcDream.App.Tests.UI.Layout;
/// </summary>
public sealed class MapHousePanelControllerTests
{
/// <summary>Serves BOTH the town-hotspot template (any (layoutId,
/// elementId) pair not the player/house icon ids) and the two icons
/// <see cref="MapPageController.Bind"/> re-resolves standalone
/// (<c>m_pMap</c>'s own button-swallowed children). Returns a
/// <summary>Serves the town-hotspot template. Returns a
/// <see cref="UiButton"/> — matching the live template's own authored
/// Type 1 (<c>MapHousePanelSlotProbeTests</c>: "hotspot template
/// type=1") — so <see cref="MapPageController.BuildTownMarkers"/>'s
/// <c>marker is UiButton</c> tooltip-text branch is actually exercised
/// by these tests. A real <see cref="RowTemplateResolver"/> would set
/// <c>DatElementId</c> the same way <see cref="LayoutImporter.Build"/>
/// does, so tests that need to find these icons back by id after the
/// does, so tests that need to find these markers back by id after the
/// fact need it too.</summary>
private static UiElement? FakeHotspotTemplate(uint layoutId, uint elementId)
=> new UiButton(new ElementInfo(), static _ => (0u, 0, 0))
@ -37,6 +34,22 @@ public sealed class MapHousePanelControllerTests
DatElementId = elementId,
};
/// <summary>The <see cref="MapPageController.Bindings.IconBuilder"/>
/// seam: builds m_pMap's two button-swallowed icon children from their
/// OWN <see cref="ElementInfo"/>s inside the panel-slot resolve tree
/// (register row AD-108 — a standalone re-import cannot find them on
/// the live DAT, so the icons are found under the already-resolved
/// <c>pageInfo</c> and built through this seam instead). Mirrors
/// production's <c>LayoutImporter.Build(info, ...).Root</c>, which sets
/// <c>DatElementId</c> from the info's own id.</summary>
private static UiElement? FakeIconBuilder(ElementInfo info)
=> new UiButton(new ElementInfo(), static _ => (0u, 0, 0))
{
Width = 10f,
Height = 10f,
DatElementId = info.Id,
};
/// <summary>The House ListBox's own row template resolves to a
/// <see cref="UiText"/> in the live DAT (<c>MapHousePanelSlotProbeTests</c>:
/// "row template type=12" — <c>UIElement_Text</c>), unlike the Map tab's
@ -59,7 +72,8 @@ public sealed class MapHousePanelControllerTests
CurrentCalendar: currentCalendar ?? (static () => default),
PlayerCellId: playerCellId ?? (static () => 0u),
HousePosition: housePosition ?? (static () => null),
TemplateResolver: FakeHotspotTemplate),
TemplateResolver: FakeHotspotTemplate,
IconBuilder: FakeIconBuilder),
House: new HousePageController.Bindings(
Lines: houseLines ?? (static () => Array.Empty<string>()),
OnShown: () => calls.Add("house-shown"),
@ -158,10 +172,10 @@ public sealed class MapHousePanelControllerTests
UiElement? map = UiElement.FindDescendant(controller!.Root, MapPageController.MapWidgetId);
Assert.NotNull(map);
// m_pMap's own children are the player/house icons (re-resolved
// standalone — see MapPageController.Bind's doc on why m_pMap being
// a Button swallows its authored nested children) PLUS the 53 town
// hotspots.
// m_pMap's own children are the player/house icons (found under the
// panel-slot resolve tree and rebuilt via Bindings.IconBuilder — see
// MapPageController.Bind's doc on why m_pMap being a Button swallows
// its authored nested children) PLUS the 53 town hotspots.
var townMarkers = map!.Children
.Where(c => c.DatElementId != MapPageController.PlayerIconId
&& c.DatElementId != MapPageController.HouseIconId)