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);
}
}