using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Core.Ui; using AcDream.Core.World; namespace AcDream.App.Tests.UI.Layout; /// /// Unit coverage for 's pure math: the /// calendar formatter ("Date: %s\nTime: %s", /// gmMapUI::Update @0x004a1eb0) and the 53-town static table /// (verbatim port of s_rgLocations). Marker-placement math itself is /// / — both /// already unit-tested elsewhere; this file only proves the wiring /// reproduces their output through the real fixture (no re-derivation). /// public sealed class MapPageControllerTests { // ── Calendar formatter ─────────────────────────────────────────────── [Fact] public void FormatDateTime_OrdinaryHour_NoAndHalfSuffix() { var calendar = new DerethDateTime.Calendar( 119, DerethDateTime.MonthName.Frostfell, 27, DerethDateTime.HourName.Dawnsong); string text = MapPageController.FormatDateTime(calendar); Assert.Equal("Date: Frostfell 27, 119 P.Y.\nTime: Dawnsong", text); } [Fact] public void FormatDateTime_AndHalfHour_RewritesSuffixWithHyphens() { var calendar = new DerethDateTime.Calendar( 10, DerethDateTime.MonthName.Morningthaw, 1, DerethDateTime.HourName.MorntideAndHalf); string text = MapPageController.FormatDateTime(calendar); Assert.Equal("Date: Morningthaw 1, 10 P.Y.\nTime: Morntide-and-Half", text); } [Theory] [InlineData(DerethDateTime.HourName.Darktide, "Darktide")] [InlineData(DerethDateTime.HourName.DarktideAndHalf, "Darktide-and-Half")] [InlineData(DerethDateTime.HourName.Gloaming, "Gloaming")] [InlineData(DerethDateTime.HourName.GloamingAndHalf, "Gloaming-and-Half")] [InlineData(DerethDateTime.HourName.WarmtideAndHalf, "Warmtide-and-Half")] public void FormatDateTime_EveryHourName_MatchesExpectedDisplayText( DerethDateTime.HourName hour, string expectedHourText) { var calendar = new DerethDateTime.Calendar( 10, DerethDateTime.MonthName.Morningthaw, 1, hour); string text = MapPageController.FormatDateTime(calendar); Assert.EndsWith($"Time: {expectedHourText}", text); } // ── Town table ──────────────────────────────────────────────────────── [Fact] public void MapLocations_Has53Entries() { Assert.Equal(53, MapLocations.All.Length); } [Fact] public void MapLocations_AllNamesAreUnique() { var names = new HashSet(StringComparer.Ordinal); foreach (MapLocation loc in MapLocations.All) Assert.True(names.Add(loc.Name), $"duplicate town name: {loc.Name}"); } [Fact] public void MapLocations_Holtburg_MatchesDecompiledByteValues() { // s_rgLocations[0x13] (pc:977379): X=0xa4 Y=0x4d W=9 H=8. MapLocation holtburg = Assert.Single(MapLocations.All, l => l.Name == "Holtburg"); Assert.Equal(0xa4, holtburg.X); Assert.Equal(0x4d, holtburg.Y); Assert.Equal(9, holtburg.Width); Assert.Equal(8, holtburg.Height); } [Fact] public void MapLocations_EveryRectIsWithinTheMapWidgetsAuthoredExtent() { // m_pMap's own authored size (MapHousePanelSlotProbeTests: markerArea // (6,8)-(247,258) — the widest observed extent). Town rects are // independent of the marker-area rect but should still land inside // a sane 0..300 canvas — a coarse sanity check that the verbatim // port didn't transpose a digit. foreach (MapLocation loc in MapLocations.All) { Assert.InRange(loc.X, 0, 260); Assert.InRange(loc.Y, 0, 260); Assert.InRange(loc.Width, 1, 20); Assert.InRange(loc.Height, 1, 20); } } // ── Marker placement math (GOLDEN PIXEL values, byte-decoded formula) ── // // gmMapUI::PlaceMarkerOnMap @0x004a18b0. Binary Ninja elides the entire // FPU chain to bare, operand-less _ftol2() calls; the formula below was // recovered by disassembling the raw bytes of the PDB-paired // acclient.exe directly (capstone) — see MapPageController.ComputeMarkerPosition's // doc comment and docs/research/2026-08-17-map-house-recon.md's // corrected PlaceMarkerOnMap entry. Every expected value here is a // LITERAL computed independently from the formula (by hand / an // external script), never by calling the port itself — that is the // whole point of a golden-value test. // // X = m_x0 - w/2 - (int)((m_x1-m_x0+1) * (x*10+1024) * (-1/2048)) // Y = m_y0 - h/2 - (int)((m_y1-m_y0+1) * (2047-(y*10+1024)) * (-1/2048)) // // Marker area used throughout: (6,8)-(247,258) — the live-fixture value // (MapHousePanelSlotProbeTests). Icon: 10x10 (matches the town hotspot // template's "plain 10x10 hotspot dot" and this test file's own fixture // resolver). [Theory] // Canonical case: dead center of Dereth (0.0N/0.0E) -> the marker // area's own true center pixel. [InlineData(0.0, 0.0, 122, 128)] // Far west (x very negative): pixel X moves toward the marker area's // left edge (m_x0=6), well below the center-case 122. [InlineData(-100.0, 0.0, 3, 128)] // Far north (y very positive): pixel Y moves toward the marker area's // top edge (m_y0=8) — the FSUBR north-up flip means +Y in-game means // SMALLER pixel Y, not larger. [InlineData(0.0, 100.0, 122, 5)] // A real town-table entry: Arwic's landblock cell 0x11CE0001 fed // through the ALREADY-VERIFIED RadarCoordinates.TryFromCell (a // different, independently-tested subsystem) to get x=-88.3/y=62.9, // then through the formula above to get the expected pixel. [InlineData(-88.30000000000001, 62.900000000000006, 17, 51)] public void ComputeMarkerPosition_MatchesByteDecodedFormula_GoldenPixels( double x, double y, int expectedLeft, int expectedTop) { (float left, float top) = MapPageController.ComputeMarkerPosition( markerX0: 6, markerX1: 247, markerY0: 8, markerY1: 258, iconWidth: 10, iconHeight: 10, x: x, y: y); Assert.Equal(expectedLeft, left); Assert.Equal(expectedTop, top); } [Fact] public void ComputeMarkerPosition_ArwicCell_MatchesRadarCoordinates() { // Cross-check that the golden (x,y) literal used above for the // "town-table entry" case really is what RadarCoordinates.TryFromCell // produces for Arwic's landblock, so the golden test above can't // silently drift from the coordinate subsystem it's chained to. const uint cellId = 0x11CE0001u; Assert.True(RadarCoordinates.TryFromCell(cellId, out RadarCoordinates coords)); Assert.Equal(-88.30000000000001, coords.X, precision: 12); Assert.Equal(62.900000000000006, coords.Y, precision: 12); } // ── Marker placement wiring (real fixture, no re-derivation) ──────────── [Fact] public void Bind_PlayerMarker_OutdoorCell_ReproducesRadarCoordinatesPlacement() { // Arwic's landblock cell id (0x11CE0001 — an arbitrary real outdoor // cell, picked only because RadarCoordinates.TryFromCell already // proves gid-to-lcoord conformance elsewhere; this test proves the // WIRING, not the formula — the formula itself is golden-tested // above). const uint cellId = 0x11CE0001u; Assert.True(RadarCoordinates.TryFromCell(cellId, out _)); ElementInfo rootInfo = FixtureLoader.LoadMapHouseHostInfos(); ImportedLayout layout = FixtureLoader.LoadMapHouseHost(); var callbacks = new MapHousePanelController.Callbacks( Toggle: () => { }, Map: new MapPageController.Bindings( CurrentCalendar: static () => default, PlayerCellId: () => cellId, HousePosition: static () => (CreateObject.ServerPosition?)null, TemplateResolver: (_, e) => new UiText { Width = 10f, Height = 10f, DatElementId = e }), House: new HousePageController.Bindings(Lines: static () => Array.Empty())); MapHousePanelController? controller = MapHousePanelController.Bind(rootInfo, layout, callbacks); Assert.NotNull(controller); UiElement? playerIcon = UiElement.FindDescendant(controller!.Root, MapPageController.PlayerIconId); Assert.NotNull(playerIcon); Assert.True(playerIcon!.Visible); // markerArea from the live fixture (MapHousePanelSlotProbeTests): // (6,8)-(247,258). GOLDEN pixel value computed independently above // (ComputeMarkerPosition_MatchesByteDecodedFormula_GoldenPixels' // Arwic case) — (17,51) for this exact (x,y). Assert.Equal(17f, playerIcon.Left); Assert.Equal(51f, playerIcon.Top); } [Fact] public void Bind_PlayerMarker_IndoorCell_HidesIconAndClearsCoordinateText() { // Envcell low word (>= 0x100) fails RadarCoordinates.TryFromCell — // the indoor branch (gmMapUI::Update's else: SetVisible(0)). const uint indoorCellId = 0x0012_0100u; Assert.False(RadarCoordinates.TryFromCell(indoorCellId, out _)); ElementInfo rootInfo = FixtureLoader.LoadMapHouseHostInfos(); ImportedLayout layout = FixtureLoader.LoadMapHouseHost(); var callbacks = new MapHousePanelController.Callbacks( Toggle: () => { }, Map: new MapPageController.Bindings( CurrentCalendar: static () => default, PlayerCellId: () => indoorCellId, HousePosition: static () => (CreateObject.ServerPosition?)null, TemplateResolver: (_, e) => new UiText { Width = 10f, Height = 10f, DatElementId = e }), House: new HousePageController.Bindings(Lines: static () => Array.Empty())); MapHousePanelController? controller = MapHousePanelController.Bind(rootInfo, layout, callbacks); Assert.NotNull(controller); UiElement? playerIcon = UiElement.FindDescendant(controller!.Root, MapPageController.PlayerIconId); Assert.NotNull(playerIcon); Assert.False(playerIcon!.Visible); } [Fact] public void Bind_HouseMarker_NullPosition_StaysHidden() { ElementInfo rootInfo = FixtureLoader.LoadMapHouseHostInfos(); ImportedLayout layout = FixtureLoader.LoadMapHouseHost(); var callbacks = new MapHousePanelController.Callbacks( Toggle: () => { }, Map: new MapPageController.Bindings( CurrentCalendar: static () => default, PlayerCellId: static () => 0u, HousePosition: static () => (CreateObject.ServerPosition?)null, TemplateResolver: (_, e) => new UiText { Width = 10f, Height = 10f, DatElementId = e }), House: new HousePageController.Bindings(Lines: static () => Array.Empty())); MapHousePanelController? controller = MapHousePanelController.Bind(rootInfo, layout, callbacks); Assert.NotNull(controller); UiElement? houseIcon = UiElement.FindDescendant(controller!.Root, MapPageController.HouseIconId); Assert.NotNull(houseIcon); Assert.False(houseIcon!.Visible); } }