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:
parent
38c580ff6d
commit
e316e190cb
6 changed files with 289 additions and 42 deletions
|
|
@ -58,7 +58,15 @@ public sealed class MapPageController
|
|||
// branch — the house icon starts/stays hidden) so this page works
|
||||
// standalone before that lands.
|
||||
Func<CreateObject.ServerPosition?> HousePosition,
|
||||
Func<uint, uint, UiElement?> TemplateResolver);
|
||||
Func<uint, uint, UiElement?> TemplateResolver,
|
||||
// Builds one UiElement subtree from an ALREADY-RESOLVED ElementInfo
|
||||
// (production: LayoutImporter.Build under the DAT lock — the same
|
||||
// build half RowTemplateResolver uses, without the import half).
|
||||
// Used for m_pMap's two button-swallowed icon children, whose
|
||||
// ElementInfos only exist inside the full panel-slot resolve tree —
|
||||
// a cold ImportInfos(hostLayout, iconId) re-import CANNOT find them
|
||||
// (register row AD-108's live-DAT finding; see Bind's own doc).
|
||||
Func<ElementInfo, UiElement?> IconBuilder);
|
||||
|
||||
private readonly UiElement? _dateTimeText;
|
||||
private readonly UiElement? _map;
|
||||
|
|
@ -108,13 +116,20 @@ public sealed class MapPageController
|
|||
/// siblings. <see cref="UiButton.ConsumesDatChildren"/> swallows a
|
||||
/// button's dat children as skin/label parts, so they never appear in
|
||||
/// the normally-built tree — <see cref="UiElement.FindDescendant"/>
|
||||
/// against the page root always returns null for them. They're
|
||||
/// resolved the SAME way the town hotspot template is: re-imported
|
||||
/// standalone via <paramref name="bindings"/>'s
|
||||
/// <see cref="Bindings.TemplateResolver"/> against the panel's own host
|
||||
/// LayoutDesc, then attached under <c>m_pMap</c> directly — their
|
||||
/// authored local position is irrelevant since <see cref="PlaceMarker"/>
|
||||
/// overwrites it every refresh.
|
||||
/// against the page root always returns null for them. Their
|
||||
/// <see cref="ElementInfo"/>s, however, DO survive: <paramref name="pageInfo"/>
|
||||
/// is a subtree of the panel's full slot resolve
|
||||
/// (<c>ImportInfos(0x2100006E, 0x1000018C)</c>), the only pathway that
|
||||
/// materializes them at all — a cold
|
||||
/// <c>ImportInfos(hostLayoutId, iconElementId)</c> starting from the
|
||||
/// icon id returns null on the live DAT because the raw LayoutDesc
|
||||
/// <c>Elements</c>-table walk never reaches them (register row AD-108's
|
||||
/// live-DAT finding, 2026-08-17). So the icons are resolved by finding
|
||||
/// their infos under <c>m_pMap</c>'s own already-resolved info and
|
||||
/// BUILDING each via <see cref="Bindings.IconBuilder"/>, then attached
|
||||
/// under <c>m_pMap</c> directly — their authored local position is
|
||||
/// irrelevant since <see cref="PlaceMarker"/> overwrites it every
|
||||
/// refresh.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static MapPageController? Bind(UiElement page, ElementInfo pageInfo, Bindings bindings)
|
||||
|
|
@ -141,8 +156,8 @@ public sealed class MapPageController
|
|||
markerArea = (x0, x1, y0, y1);
|
||||
}
|
||||
|
||||
UiElement? playerIcon = ResolveSwallowedIcon(map, bindings.TemplateResolver, PlayerIconId);
|
||||
UiElement? houseIcon = ResolveSwallowedIcon(map, bindings.TemplateResolver, HouseIconId);
|
||||
UiElement? playerIcon = ResolveSwallowedIcon(map, mapInfo, bindings.IconBuilder, PlayerIconId);
|
||||
UiElement? houseIcon = ResolveSwallowedIcon(map, mapInfo, bindings.IconBuilder, HouseIconId);
|
||||
|
||||
var controller = new MapPageController(
|
||||
UiElement.FindDescendant(page, DateTimeTextId),
|
||||
|
|
@ -170,23 +185,60 @@ public sealed class MapPageController
|
|||
return controller;
|
||||
}
|
||||
|
||||
/// <summary>Re-resolves one of <c>m_pMap</c>'s button-swallowed nested
|
||||
/// icon children standalone (see <see cref="Bind"/>'s own doc) and
|
||||
/// attaches it under <paramref name="map"/>. Starts hidden — the first
|
||||
/// <summary>Resolves one of <c>m_pMap</c>'s button-swallowed nested
|
||||
/// icon children by finding its <see cref="ElementInfo"/> under
|
||||
/// <paramref name="mapInfo"/> — the panel-slot resolve tree, the ONLY
|
||||
/// place these infos exist (see <see cref="Bind"/>'s own doc + register
|
||||
/// row AD-108) — building it via <paramref name="iconBuilder"/>, and
|
||||
/// attaching it under <paramref name="map"/>. Starts hidden — the first
|
||||
/// <see cref="Refresh"/> call (from <see cref="Bind"/>) decides real
|
||||
/// visibility.</summary>
|
||||
private static UiElement? ResolveSwallowedIcon(
|
||||
UiElement map, Func<uint, uint, UiElement?> templateResolver, uint iconElementId)
|
||||
UiElement map, ElementInfo? mapInfo, Func<ElementInfo, UiElement?> iconBuilder, uint iconElementId)
|
||||
{
|
||||
UiElement? icon = templateResolver(MapHousePanelController.HostLayoutId, iconElementId);
|
||||
// If the normal build walk ever stops swallowing m_pMap's dat
|
||||
// children (a future UiButton.ConsumesDatChildren policy change),
|
||||
// the icon already exists in the built tree — use it rather than
|
||||
// building a second, permanently-static copy behind the live
|
||||
// marker. Retail's own PostInit is exactly this find-the-child.
|
||||
UiElement? existing = UiElement.FindDescendant(map, iconElementId);
|
||||
if (existing is not null)
|
||||
return PrepareIcon(existing);
|
||||
|
||||
ElementInfo? iconInfo = mapInfo is null ? null : FindInfo(mapInfo, iconElementId);
|
||||
if (iconInfo is null)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[D.2b] Map tab: icon 0x{iconElementId:X8} not authored under m_pMap's resolved "
|
||||
+ "info tree — it will not be shown.");
|
||||
return null;
|
||||
}
|
||||
|
||||
UiElement? icon = iconBuilder(iconInfo);
|
||||
if (icon is null)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[D.2b] Map tab: icon 0x{iconElementId:X8} did not resolve — it will not be shown.");
|
||||
$"[D.2b] Map tab: icon 0x{iconElementId:X8} did not build — it will not be shown.");
|
||||
return null;
|
||||
}
|
||||
map.AddChild(PrepareIcon(icon));
|
||||
return icon;
|
||||
}
|
||||
|
||||
/// <summary>Marks one marker icon as runtime-positioned. <see cref="PlaceMarker"/>
|
||||
/// owns the element's position outright (retail's <c>gmMapUI::Update</c>
|
||||
/// re-places both markers every tick) — but acdream re-runs the authored
|
||||
/// layout pass per frame, so the compatibility anchor capture (and any
|
||||
/// imported raw-edge <see cref="UiElement.LayoutPolicy"/>, which the
|
||||
/// <see cref="UiElement.Anchors"/> setter clears) would re-assert the
|
||||
/// authored (0,0) rect every frame, silently overwriting PlaceMarker's
|
||||
/// writes — the F1 live finding: a visible green ring pinned to m_pMap's
|
||||
/// top-left corner regardless of the player's true position. Starts
|
||||
/// hidden — the first <see cref="Refresh"/> decides real visibility.</summary>
|
||||
private static UiElement PrepareIcon(UiElement icon)
|
||||
{
|
||||
icon.Anchors = AnchorEdges.None;
|
||||
icon.Visible = false;
|
||||
map.AddChild(icon);
|
||||
return icon;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3329,6 +3329,25 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
return hotspotTemplate.Resolve(templateLayoutId, templateElementId);
|
||||
}
|
||||
|
||||
// m_pMap's two button-swallowed icon children (player/house markers)
|
||||
// only exist as ElementInfos INSIDE rootInfo's own panel-slot resolve
|
||||
// tree — a cold ImportInfos(hostLayout, iconId) re-import returns
|
||||
// null on the live DAT (register row AD-108). MapPageController.Bind
|
||||
// locates each icon's info under m_pMap and calls this seam to build
|
||||
// it: the build half of RowTemplateResolver's shape, no import half.
|
||||
// Monitor re-entrancy on DatLock is established for this mount path
|
||||
// (Bind itself runs under the lock below, same as
|
||||
// ResolveHotspotTemplate's own re-entrant take).
|
||||
UiElement? BuildSwallowedIcon(ElementInfo iconInfo)
|
||||
{
|
||||
lock (_bindings.Assets.DatLock)
|
||||
return LayoutImporter.Build(
|
||||
iconInfo,
|
||||
_bindings.Assets.ResolveSprite,
|
||||
_bindings.Assets.DefaultFont,
|
||||
_bindings.Assets.ResolveFont).Root;
|
||||
}
|
||||
|
||||
MapHouseRuntimeBindings mh = _bindings.MapHouse;
|
||||
var callbacks = new Layout.MapHousePanelController.Callbacks(
|
||||
Toggle: () => ToggleWindow(WindowNames.MapHouse),
|
||||
|
|
@ -3336,7 +3355,8 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
CurrentCalendar: mh.CurrentCalendar,
|
||||
PlayerCellId: mh.PlayerCellId,
|
||||
HousePosition: mh.HousePosition ?? (static () => null),
|
||||
TemplateResolver: ResolveHotspotTemplate),
|
||||
TemplateResolver: ResolveHotspotTemplate,
|
||||
IconBuilder: BuildSwallowedIcon),
|
||||
House: new Layout.HousePageController.Bindings(
|
||||
Lines: mh.HouseLines ?? (static () => Array.Empty<string>()),
|
||||
OnShown: mh.HouseShown,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue