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; /// /// 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. /// /// /// The fixture-based 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 's exact /// recipe against the INSTALLED DATs (sprite/font resolution stubbed — /// structure only, same as every committed-fixture build): the panel-slot /// LayoutImporter.ImportInfos(dats, hostLayoutId, slotElementId) /// import, , real /// import-then-build hotspot/template resolution, and the /// build seam — /// then asserts the two icons actually materialize as built elements. /// /// /// /// Gated like every other installed-DAT family here: /// [InstalledDatFact], opt in with /// ACDREAM_PROBE_LIVE_MOUNT=1 (the App suite's live-DAT baseline /// mode); ACDREAM_DAT_DIR overrides the ordinary /// Documents/Asheron's Call location. /// /// 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(), 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); } }