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>
F11: logs when a Map tab town-marker's template resolves to something
other than a UiButton — that path previously silently skipped the
TooltipText write with no diagnostic, leaving a mounted-but-empty
tooltip popup indistinguishable from "no template configured".
F13: RetailSkillFormula.FormatFormula now reads Attribute1Multiplier/
Attribute2Multiplier/AdditiveBonus/Divisor through the SAME unsigned
reinterpretation TryCalculate already uses (this class's own doc
comment already stated the invariant; FormatFormula just didn't follow
it). A high-bit-set value would previously both mis-gate hasAttr1/
hasAttr2 and print a negative number, out of sync with what
TryCalculate actually computes with for the same formula. Added
regression tests, empirically verified to fail without the fix.
F14: documented the RefreshHouseMarker gap rather than guessing at the
byte-decode — Position::get_outside_cell_id @0x004527b0 is itself
BN-mangled (its `(eax_2 - eax_2) & objcell_id` return is the same
decompiler-obscures-a-real-conditional artifact class this round hit
elsewhere) and depends on LandDefs::adjust_to_outside, a genuinely
larger port than this round's other findings. HousePosition is wired
() => null in production today (ISSUES #413's remaining scope), so
this method is currently unreachable; left a TODO citing the retail
call chain for whenever that lands.
F15: fixed RefreshCoordinatesAndPlayerMarker's gate to AND-on-both-
present, matching gmMapUI::Update @0x004a2078's exact
`if (m_pCoordinateText != 0 && m_pPlayerLocationIcon != 0)` condition.
The prior `_coordinateText is null && _playerIcon is null` check only
skipped when BOTH were absent (proceeding whenever EITHER was
present), letting coordinate text and the player marker update
independently instead of as the single gated unit retail treats them
as. Added a regression test (player-icon template resolution failure
must also skip the coordinate-text write), empirically verified to
fail without the fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The prior PlaceMarker() reading ("center at markerX0+x") was wrong.
Binary Ninja elides gmMapUI::PlaceMarkerOnMap @0x004a18b0's entire FPU
chain to bare, operand-less _ftol2() calls, so the pseudo-C
under-specifies the function. A capstone disassembly of the raw bytes
in the PDB-paired acclient.exe recovers the real formula: retail
projects the AC display coordinate (range ~-102.4..102.4) onto the
marker-area rect via a fixed-point-style transform, not a raw pixel
add:
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))
Constants read directly from .rdata: 0x79bac8=10.0, 0x7aac78=1024.0,
0x7aac70=-1/2048, 0x7aac68=2047.0. The Y axis's FSUBR is retail's
north-up flip. w/h halve with truncating integer division (matching
retail's cdq;sub;sar idiom), not float division.
Extracted the pure math into MapPageController.ComputeMarkerPosition
so it's directly testable, and retargeted MapPageControllerTests to
GOLDEN PIXEL values computed independently from the formula (never
from the port's own output): the reviewer's canonical (0,0)->(122,128)
case, a far-west and far-north case, and a real town-table entry
(Arwic's landblock, cross-checked against RadarCoordinates). Applies
to the green ring, house pin, and all 53 static town hotspots, which
all resolve through the same PlaceMarker call.
Corrected the recon doc's "accepted as-is" note, which had mistaken
"the FPU argument-passing is BN-mangled" for a narrow issue instead of
the whole-formula elision it actually was.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mounts host 0x2100006E slot 0x1000018C (RetailPanelCatalog.MapHouse = 16)
as a two-tab UiTabPanel (Map default, House second) through the OP3/FA3
recipe (LayoutImporter.Build -> Bind -> ActivateTabBehavior). Toolbar
button 0x1000019A un-ghosts (added to both RetailPanelCatalog.Mounted and
.Toolbar). Combined into one commit because MapHousePanelController.Bind
depends on both MapPageController and HousePageController existing —
splitting them would mean landing dead code first.
Map tab (gmMapUI, MapPageController):
- Calendar formatter matching gmMapUI::Update's "Date: %s\nTime: %s"
shape, reusing WorldTimeService.CurrentCalendar (new
Func<DerethDateTime.Calendar> dependency threaded through
InteractionRetainedUiDependencies/GameWindow — a stable long-lived
service, not routed through the deferred-binding machinery Radar's
per-session state needs). MonthName enum values already match retail
display text; HourName's "AndHalf" suffix is rewritten to "-and-Half".
- Coordinate math + marker placement reuse RadarCoordinates/
LandDefs.GidToLcoord verbatim (both already byte-exact ports of
CPlayerSystem::InqPlayerCoords/LandDefs::gid_to_lcoord) — no re-port.
PlaceMarkerOnMap's centering math (m_x0 + x - w/2) ported from
gmMapUI::PlaceMarkerOnMap @0x004a18b0. Indoor gating clears the
coordinate text and hides the player marker, matching
gmMapUI::Update's else branch.
- 53-town s_rgLocations table ported verbatim into MapLocations.cs.
Markers built once at bind time via the panel's own RowTemplateResolver
against m_pMap's authored hotspot-template attrs (0x47/0x48), with
literal-string tooltips through AuthoredTooltipText/Enabled
(RetailTooltipPresenter) — closes divergence-register row TS-85's last
item, gmMapUI::AddMapNote @0x004A1C51.
- Structural finding: m_pMap (0x100001EC) is itself authored as a Type-1
BUTTON (the GM click-to-teleport hook at
gmMapUI::ListenToElementMessage), and the player/house icons
(0x100001ED/EE) are its own NESTED children, not siblings —
UiButton.ConsumesDatChildren swallows them from the normally-built
tree. Both are re-resolved standalone through the same template
resolver the town hotspots use and reattached under m_pMap.
House tab (gmHouseUI, HousePageController): mounts the ListBox
(0x100001E6) with its authored row template, wired to an empty Lines()
source by default — genuinely empty until Slice 4's wire lands, matching
retail's own PostInit (no Update call, no static content).
21 new tests (7 MapHousePanelControllerTests, 14 MapPageControllerTests):
tab table pairing, close button, town-hotspot count/tooltips, calendar
formatter golden values (Frostfell 27/119 P.Y., every HourName incl.
AndHalf), player/house marker placement and indoor-gating reproduced
against the real fixture via already-tested RadarCoordinates (no
re-derivation). Fixture map_house_2100006E_1000018C.json captured via
the shared RetailLayoutFixtureGenerator (other 34 fixtures deliberately
NOT regenerated — out of scope for this batch, would touch unrelated
panels' schema drift).
Full solution builds clean; App suite 5391/0 failed/71 skipped (non-live;
one earlier flaky streaming failure unrelated to this change, confirmed
pre-existing on the branch before these commits).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>