fix(ui): night-round review — F1 real PlaceMarkerOnMap formula

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>
This commit is contained in:
Erik 2026-08-17 04:19:29 +02:00
parent 06512f0957
commit 6ee3d88863
3 changed files with 148 additions and 17 deletions

View file

@ -354,17 +354,65 @@ public sealed class MapPageController
}
/// <summary>
/// <c>gmMapUI::PlaceMarkerOnMap @0x004a18b0</c>: center the icon at
/// <c>(markerAreaX0 + x, markerAreaY0 + y)</c>, then show it.
/// <c>gmMapUI::PlaceMarkerOnMap @0x004a18b0</c>, ported from a direct
/// byte-read of the PDB-paired <c>acclient.exe</c> (Binary Ninja elides
/// the whole FPU chain to bare, operand-less <c>_ftol2()</c> calls —
/// see <c>docs/research/named-retail/acclient_2013_pseudo_c.txt</c>
/// lines 171827-171855 — so the pseudo-C alone under-specifies this
/// function; capstone disassembly of the raw machine code at that VA
/// is the ground truth here, not the BN text). The prior "center at
/// markerX0+x" reading was WRONG — retail projects the AC display
/// coordinate (<paramref name="x"/>/<paramref name="y"/>, range
/// approximately ±102.4) onto the marker-area rect's pixel span via a
/// fixed-point-style transform, not a raw pixel add:
/// <code>
/// 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) )
/// </code>
/// Constants read straight from the binary's .rdata: <c>0x79bac8</c> =
/// 10.0, <c>0x7aac78</c> = 1024.0, <c>0x7aac70</c> = -1/2048 (exactly
/// -0.00048828125), <c>0x7aac68</c> = 2047.0. The Y axis's FSUBR
/// (reversed subtract) is retail's north-up flip — Y increases upward
/// on the AC coordinate system but downward in screen pixels.
/// <c>w</c>/<c>h</c> are the icon's own <c>UIRegion::GetWidth</c>/
/// <c>GetHeight</c> (@0x0069efe0/@0x0069eff0), halved with INTEGER
/// (truncating) division to match retail's <c>cdq;sub;sar</c> idiom —
/// not float division, which would drift by half a pixel on
/// odd-sized icons. Golden case (marker area (6,8)-(247,258), 10x10
/// icon, position 0.0N/0.0E) reproduces exactly to (122,128) center.
/// </summary>
private void PlaceMarker(UiElement? icon, double x, double y)
{
if (icon is null) return;
icon.Left = _markerX0 + (float)x - icon.Width / 2f;
icon.Top = _markerY0 + (float)y - icon.Height / 2f;
(float left, float top) = ComputeMarkerPosition(
_markerX0, _markerX1, _markerY0, _markerY1,
(int)icon.Width, (int)icon.Height, x, y);
icon.Left = left;
icon.Top = top;
icon.Visible = true;
}
/// <summary>
/// The pure <c>PlaceMarkerOnMap</c> math, split out from <see cref="PlaceMarker"/>
/// so tests can assert byte-decoded GOLDEN PIXEL values directly against
/// the formula instead of round-tripping through the port's own output.
/// </summary>
internal static (float Left, float Top) ComputeMarkerPosition(
int markerX0, int markerX1, int markerY0, int markerY1,
int iconWidth, int iconHeight, double x, double y)
{
int halfWidth = iconWidth / 2;
int halfHeight = iconHeight / 2;
int extentX = markerX1 - markerX0 + 1;
int extentY = markerY1 - markerY0 + 1;
int xOffset = (int)(extentX * (x * 10.0 + 1024.0) * (-1.0 / 2048.0));
int yOffset = (int)(extentY * (2047.0 - (y * 10.0 + 1024.0)) * (-1.0 / 2048.0));
return (markerX0 - halfWidth - xOffset, markerY0 - halfHeight - yOffset);
}
private static ElementInfo? FindInfo(ElementInfo info, uint id)
{
if (info.Id == id) return info;