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>
157 lines
7.5 KiB
C#
157 lines
7.5 KiB
C#
using System.IO;
|
|
using AcDream.App.UI;
|
|
using AcDream.App.UI.Layout;
|
|
using AcDream.Core.Net.Messages;
|
|
using DatReaderWriter;
|
|
using DatReaderWriter.Options;
|
|
|
|
namespace AcDream.App.Tests.UI.Layout;
|
|
|
|
/// <summary>
|
|
/// Live-DAT mount pin for the Map tab's player/house marker icons — THE test
|
|
/// that would have caught the Batch C icon-resolution gap (register row
|
|
/// AD-108) at commit time.
|
|
///
|
|
/// <para>
|
|
/// The fixture-based <see cref="MapHousePanelControllerTests"/> could not
|
|
/// catch it: their fake resolvers answer ANY id, so the production resolve
|
|
/// mechanism itself was never exercised against real data. This test
|
|
/// reproduces <see cref="RetailUiRuntime.MountMapHousePanel"/>'s exact
|
|
/// recipe against the INSTALLED DATs (sprite/font resolution stubbed —
|
|
/// structure only, same as every committed-fixture build): the panel-slot
|
|
/// <c>LayoutImporter.ImportInfos(dats, hostLayoutId, slotElementId)</c>
|
|
/// import, <see cref="LayoutImporter.Build"/>, real
|
|
/// import-then-build hotspot/template resolution, and the
|
|
/// <see cref="MapPageController.Bindings.IconBuilder"/> build seam —
|
|
/// then asserts the two icons actually materialize as built elements.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// Gated like every other installed-DAT family here:
|
|
/// <c>[InstalledDatFact]</c>, opt in with
|
|
/// <c>ACDREAM_PROBE_LIVE_MOUNT=1</c> (the App suite's live-DAT baseline
|
|
/// mode); <c>ACDREAM_DAT_DIR</c> overrides the ordinary
|
|
/// Documents/Asheron's Call location.
|
|
/// </para>
|
|
/// </summary>
|
|
public sealed class MapHousePanelLiveDatMountTests
|
|
{
|
|
private static string DatDirectory =>
|
|
Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR")
|
|
?? Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
|
"Documents",
|
|
"Asheron's Call");
|
|
|
|
[InstalledDatFact]
|
|
public void MountRecipe_ResolvesPlayerAndHouseIcons_UnderTheMapWidget()
|
|
{
|
|
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
|
|
|
// ── AD-108's structural facts, pinned ────────────────────────────
|
|
// 1) A COLD standalone re-import starting from the icon's own id
|
|
// returns null: the raw LayoutDesc Elements-table walk never
|
|
// reaches m_pMap's nested children. This is WHY the IconBuilder
|
|
// seam exists — if a future DAT regeneration makes this resolve,
|
|
// this pin flags that the seam could be revisited.
|
|
Assert.Null(LayoutImporter.ImportInfos(
|
|
dats,
|
|
MapHousePanelController.HostLayoutId,
|
|
MapPageController.PlayerIconId));
|
|
Assert.Null(LayoutImporter.ImportInfos(
|
|
dats,
|
|
MapHousePanelController.HostLayoutId,
|
|
MapPageController.HouseIconId));
|
|
|
|
// 2) The full panel-slot resolve — what MountMapHousePanel actually
|
|
// imports — DOES materialize both icons, nested under m_pMap.
|
|
ElementInfo? rootInfo = LayoutImporter.ImportInfos(
|
|
dats,
|
|
MapHousePanelController.HostLayoutId,
|
|
MapHousePanelController.SlotElementId);
|
|
Assert.NotNull(rootInfo);
|
|
|
|
// ── The production mount recipe (sprites/fonts stubbed) ──────────
|
|
ImportedLayout layout = LayoutImporter.Build(rootInfo!, static _ => (0u, 0, 0), null);
|
|
|
|
UiElement? ResolveTemplate(uint layoutId, uint elementId)
|
|
{
|
|
ElementInfo? info = LayoutImporter.ImportInfos(dats, layoutId, elementId);
|
|
return info is null
|
|
? null
|
|
: LayoutImporter.Build(info, static _ => (0u, 0, 0), null).Root;
|
|
}
|
|
|
|
UiElement? BuildIcon(ElementInfo info)
|
|
=> LayoutImporter.Build(info, static _ => (0u, 0, 0), null).Root;
|
|
|
|
// Mutable cell: Bind sees "no position yet" (0 — the real mount-time
|
|
// state, the panel mounts before the session enters world), then the
|
|
// player lands outdoors and the 5 s cadence re-refreshes.
|
|
uint playerCell = 0u;
|
|
var callbacks = new MapHousePanelController.Callbacks(
|
|
Toggle: static () => { },
|
|
Map: new MapPageController.Bindings(
|
|
CurrentCalendar: static () => default,
|
|
PlayerCellId: () => playerCell,
|
|
HousePosition: static () => (CreateObject.ServerPosition?)null,
|
|
TemplateResolver: ResolveTemplate,
|
|
IconBuilder: BuildIcon),
|
|
House: new HousePageController.Bindings(
|
|
Lines: static () => Array.Empty<string>(),
|
|
TemplateResolver: ResolveTemplate));
|
|
|
|
MapHousePanelController? controller =
|
|
MapHousePanelController.Bind(rootInfo!, layout, callbacks);
|
|
Assert.NotNull(controller);
|
|
|
|
// ── Pin 1: both icons resolve as BUILT elements ──────────────────
|
|
UiElement? map = UiElement.FindDescendant(
|
|
controller!.Root, MapPageController.MapWidgetId);
|
|
Assert.NotNull(map);
|
|
|
|
UiElement? playerIcon = UiElement.FindDescendant(
|
|
controller.Root, MapPageController.PlayerIconId);
|
|
UiElement? houseIcon = UiElement.FindDescendant(
|
|
controller.Root, MapPageController.HouseIconId);
|
|
Assert.NotNull(playerIcon);
|
|
Assert.NotNull(houseIcon);
|
|
|
|
// Attached directly under m_pMap (PlaceMarker positions them in its
|
|
// local space), with real authored extents — PlaceMarker's centering
|
|
// divides the icon's own Width/Height, so a zero-sized build would
|
|
// silently mis-center every marker.
|
|
Assert.Same(map, playerIcon!.Parent);
|
|
Assert.Same(map, houseIcon!.Parent);
|
|
Assert.True(playerIcon.Width > 0 && playerIcon.Height > 0,
|
|
$"player icon built with degenerate extent {playerIcon.Width}x{playerIcon.Height}");
|
|
Assert.True(houseIcon.Width > 0 && houseIcon.Height > 0,
|
|
$"house icon built with degenerate extent {houseIcon.Width}x{houseIcon.Height}");
|
|
|
|
// ── Pin 2: PlaceMarker's writes survive the per-frame layout pass ─
|
|
// The F1 live finding's second half: the client re-runs the authored
|
|
// layout pass (parent → child.ApplyAnchor) every frame. Pre-fix the
|
|
// icon's compatibility anchor captured the authored (0,0) rect while
|
|
// the panel sat indoors/hidden, then re-asserted it every frame —
|
|
// a visible ring pinned to m_pMap's top-left corner regardless of
|
|
// the player's position. Reproduce that exact frame order here.
|
|
playerIcon.ApplyAnchor(map!.Width, map.Height); // frame while cell unknown
|
|
houseIcon.ApplyAnchor(map.Width, map.Height);
|
|
Assert.False(playerIcon.Visible);
|
|
|
|
playerCell = 0x11CE0001u; // Arwic (independently pinned:
|
|
// display coords -88.3 / 62.9)
|
|
controller.Tick(MapPageController.RefreshIntervalSeconds + 0.01);
|
|
Assert.True(playerIcon.Visible);
|
|
|
|
(float expectedLeft, float expectedTop) = MapPageController.ComputeMarkerPosition(
|
|
markerX0: 6, markerX1: 247, markerY0: 8, markerY1: 258,
|
|
(int)playerIcon.Width, (int)playerIcon.Height, -88.30000000000001, 62.900000000000006);
|
|
Assert.Equal(expectedLeft, playerIcon.Left);
|
|
Assert.Equal(expectedTop, playerIcon.Top);
|
|
|
|
playerIcon.ApplyAnchor(map.Width, map.Height); // the next frame's pass
|
|
Assert.Equal(expectedLeft, playerIcon.Left);
|
|
Assert.Equal(expectedTop, playerIcon.Top);
|
|
}
|
|
}
|