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>
17 KiB
Map/House toolbar panel — recon (Batch C, overnight hover/UI round)
Combines the recon handed to this session (address-level findings, verified) with additional desk verification done before implementation: several facts the handoff marked "UNKNOWN" or "likely" were already resolved elsewhere in the repo, and are corrected here.
Panel identity — RESOLVED (corrects the handoff's guess)
The handoff guessed the panel id was "likely one of RetailPanelCatalog's
unused ids {1,2,6,14}". That guess is wrong — the answer was already on
disk from the FA campaign's own full 16-slot gmPanelUI::SetupChildren
dump (docs/research/2026-08-11-fa-panel-structure.md:927-933, itself
byte-verified against the live installed DATs at FA3):
0x1000018C = 16 gmMapUI+gmHouseUI pages
0x10000559 = 25 gmJournalUI/gmPageListUI/gmContractsUI (NOT this batch)
So: host 0x2100006E, slot 0x1000018C, RetailPanelCatalog id 16.
gmPanelUI::SetupChildren @0x004bc9e0 (pc:195832) confirms the full 16-slot
enumeration is exhaustive — 0x10000186 inside the contiguous id run
resolves to nothing (IMPORT NULL), consistent with the FA doc's flag.
Toolbar button: cross-referenced the committed fixture
tests/AcDream.App.Tests/UI/Layout/fixtures/toolbar_21000016.json (last
regenerated 2026-08-11, Campaign OP slice OP5 — same DAT install this
session uses) for each of ToolbarController.PanelButtonIds' own
P0x10000029 value:
| Button element | P0x10000029 |
Panel |
|---|---|---|
0x1000055A |
25 | Journal (ghosted, out of scope) |
0x10000197 |
12 | Social — currently ghosted despite FA docs claiming "no toolbar button authors this id"; flagged, not chased (out of scope for this batch) |
0x10000198 |
13 | Magic (registered) |
0x10000199 |
11 | Character (registered) |
0x100001B1 |
7 | Inventory (registered) |
0x1000019A |
16 | Map/House — THE button this batch un-ghosts |
0x1000019B |
10 | Options (registered) |
Slice 1's live probe re-confirms both facts (slot table + button) against the live DAT rather than trusting the committed fixture at face value.
gmMapUI — decompiled, byte-exact
Retail source: docs/research/named-retail/acclient_2013_pseudo_c.txt.
gmMapUI::PostInit @0x004a1c70(pc:171993): resolvesm_pDateTimeText=0x100001eb(UIElement_Text, DynamicCast 0xc),m_pCoordinateText=0x100001ef(UIElement_Text),m_pPlayerLocationIcon=0x100001ed,m_pHouseLocationIcon=0x100001ee,m_pMap=0x100001ec. Readsm_pMap's own int attrs0x1000004e/4f/50/51intom_boxMapMarkerArea(x0,x1,y0,y1) — the marker-area rect. Readsm_pMap's enum attr0x47(literal small property id, NOT0x10000047) and DataID attr0x48into aQualifiedDataID(id, 0x23)(category0x23= LayoutDesc) →DBObj::Get. If that resolves, loops the 53-entrys_rgLocationstable callingAddMapNote(this, m_pMap, var_c /*=attr 0x47, the template ELEMENT id*/, eax_10 /*=the resolved LayoutDesc*/, &s_rgLocations[i])for each. So0x47/0x48onm_pMaptogether name a template (LayoutDesc, element) pair for the per-town hotspot widget — the exact same "authored template" patternLayout.RowTemplateResolveralready serves for Friends/Squelch/ Fellowship rows. Slice 1's probe reads the live values.gmMapUI::AddMapNote @0x004a1bb0(pc:171967):CreateChildElement(mgr, m_pMap, layoutDesc, templateElementId)→MoveTo(info.X, info.Y)→ResizeTo(info.Width, info.Height)→SetTooltip(child, info.Name)(a literal wide string,StringInfo::SetLiteralValue— not a DAT string table lookup). acdream'sUiElement.AuthoredTooltipText+AuthoredTooltipEnabled, served byRetailTooltipPresenter(src/AcDream.App/UI/Layout/RetailTooltipPresenter.cs), is the exact seam — set both fields on the built child and the existing tooltip pipeline does the rest. This closes the one remaining item of register row TS-85 (docs/architecture/retail-divergence-register.md), which explicitly namedgmMapUI::AddMapNote @0x004A1C51as the last unportedSetTooltipcall site.gmMapUI::PlaceMarkerOnMap @0x004a18b0(pc:171827): CORRECTED 2026-08-17 (night-round review, finding F1) — the "accepted as-is" reading below was WRONG. The BN pseudo-C's operand-less_ftol2()calls are not just "argument-passing mangled" — they swallow the ENTIRE FPU chain (constants, multiplies, the Y-axis FSUBR flip), not merely thearg3/arg4reads. A direct capstone disassembly of the raw bytes at0x004a18b0in the PDB-pairedacclient.exerecovers the true formula: retail projects the AC display coordinate (x/y, range ≈ ±102.4) onto the marker-area rect via a fixed-point-style transform, not a raw pixel offset: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)), thenSetVisible(1). Constants read from.rdata:0x79bac8=10.0,0x7aac78=1024.0,0x7aac70=-1/2048,0x7aac68=2047.0.w/hareUIRegion::GetWidth/GetHeighthalved by INTEGER (truncating) division, matching retail'scdq;sub;saridiom. Golden case: marker area (6,8)-(247,258), 10x10 icon, position 0.0N/0.0E → (122,128) center — reproduced exactly. Ported atsrc/AcDream.App/UI/Layout/MapPageController.cs'sPlaceMarker. The ORIGINAL (wrong) note, kept for the historical record of how the mistake happened: "MoveTo(m_x0 + (int)x - width/2, m_y0 + (int)y - height/2)... the x87/FPU argument-passing is BN-mangled in the raw decomp (the_ftol2()placeholder swallows the actualarg3/arg4reads) — this formula is the handoff's own already-verified reading and is accepted as-is; the underlying+x-w/2/+y-h/2centering pattern is unambiguous from the surrounding integer math." It was not unambiguous — the BN elision hid a whole coordinate-projection transform behind what looked like a plain pixel add.gmMapUI::Update @0x004a1eb0(pc:172084): re-armsm_nextUpdate = Timer::cur_time + 5.0every call (5 s cadence, driven byListenToGlobalMessage'sarg2==3tick case). Date/time block: builds"Date: %s\nTime: %s"fromGameTime::GetDateTimeString, only callsSetTextwhen the string actually differs (a change-detect, not a re-stamp every 5 s). Coordinate block, gated onCPlayerSystem::IsOutside():- outside:
CPlayerSystem::InqPlayerCoords→ sign-based N/S/E/W selection (heavily FPU-mangled — BN elides the actual printf format string behind aFormatted/vtable-slot placeholder it cannot resolve; accepted as genuinely unrecoverable from this dump, matching the handoff's own UNKNOWN #3) →SetText(change-detected) →PlaceMarkerOnMap(m_pPlayerLocationIcon, x, y). - inside:
SetTextto a fixed narrow-string constant (also BN-mangled/unrecovered — treated as "empty/blank", matching retail's known behavior of clearing the readout) →m_pPlayerLocationIcon-> SetVisible(0). - House marker (independent of the outside/inside branch, gated on
m_pHouseLocationIcon != 0):Position::IsValid(&m_HousePosition)→ if invalid,SetVisible(0); if valid,Position::get_outside_cell_id(&m_HousePosition)→LandDefs::gid_to_lcoord→ the identical(v - 0x400) * 0.1 + 0.5transform on both axes →PlaceMarkerOnMap(m_pHouseLocationIcon, x, y).
- outside:
CPlayerSystem::InqPlayerCoords @0x00560090(pc:364615): confirms the(lcoord - 0x400) * 0.1 + 0.5display transform per axis, fed byCPhysicsObj::get_landscape_coord. Which of that function's two raw outputs maps to which InqPlayerCoords axis is ambiguous in the BN decomp (aesp+0x10/esp+0x24swap that can't be resolved without disassembly); not chased — see "Precision decision" below.LandDefs::gid_to_lcoord @0x00497a90(pc:163500): clean, no FPU noise. Already ported atsrc/AcDream.Core/Physics/LandDefs.cs:73(LandDefs.GidToLcoord, issue #106, cross-checked against ACE) — theedx_2 < 0x100low-word check that looked suspicious in the raw BN text is exactly the existing port's own documented finding (low = cellId & 0xFFFF; if (low >= 0x100) return false— a 16-bit sub-register access BN renders as a full-width compare). No re-port needed, per the WorldBuilder-inventory doctrine ("read the inventory FIRST... re-porting when we already have a tested port is how bugs slip in") extended here to the equivalent Core-physics precedent.- Also present but out of scope:
gmMapUI::ListenToElementMessage @0x004a2350idMessage0x1chandles a GM-only ("PlayerDesc:: PlayerIsPSR") click-to-teleport on the map (lcoord_to_gidfrom the click pixel →Position→ presumably a teleport notice). Not part of any assigned slice; flagged for a future issue if wanted, not filed given no immediate need.
Precision decision (player marker)
InqPlayerCoords' raw inputs come from CPhysicsObj::get_landscape_coord,
which is not itself ported and whose BN decomp is FPU-mangled beyond safe
recovery tonight. At this map's scale (53-entry pixel-rect town table
covering the FULL Dereth landmass, lcoord range [0, 0x7F8)=2040 mapped
to ~0.1 map-units/cell, i.e. ~0.8 map-units per landblock, well under a
pixel) integer landcell precision is visually indistinguishable from
sub-cell precision. The port therefore computes the player marker exactly
like the (byte-exact, unambiguous) house marker: current outdoor cell id →
LandDefs.GidToLcoord → the same (v-0x400)*0.1+0.5 transform. This is a
reasoned substitution of an ALREADY-VERIFIED equivalent primitive, not a
guess — flagged here and in a divergence-register row for the one case
where it could matter (crossing a cell boundary at the exact map-rendering
threshold), not for the sub-pixel precision itself.
Wire byte layout — Position (used by HouseData.Position)
references/ACE/Source/ACE.Server/Network/Structure/AllegianceHierarchy.cs:192-212
writes Cell(uint32) + Pos.XYZ(float×3) + Rotation.WXYZ(float×4) = 32
bytes. acdream already has this exact shape as
AcDream.Core.Net.Messages.CreateObject.ServerPosition (CreateObject.cs:479,
parsed at CreateObject.cs:611-619) — reused rather than re-defined.
House — gmHouseUI, decompiled
gmHouseUI::PostInit @0x004a2710(pc:172581): resolvesm_pTextBox=0x100001e6(UIElement_ListBox, DynamicCast 5). Registers FOUR notice handlers:0x4dd225(HouseData),0x4dd226(HouseStatus),0x4dd227(UpdateRentTime),0x4dd228(UpdateRentPayment) — matching wire opcodes0x0225-0x0228.gmHouseUI::GetHouseLocation @0x004a27b0: readsm_pHouseDataat offset0x74(an enum,== 4is a short-circuit "no location" case) or falls through toPosition::IsValid(&m_pHouseData->m_pos /*+0x2c*/)→LandDefs::gid_to_lcoord(Position::get_outside_cell_id(...)).- Seven
Display*line builders (DisplayBuyPayment,DisplayRentPayment,DisplayBuyTime,DisplayRentTimes,DisplayLocation,DisplayWarningText,DisplayPurchaseTimeText), all called in sequence fromDisplayHouseData @0x004a3380and from bothUpdateoverloads. Each is dozens-to-a-few-hundred lines of heavily FPU/string-mangled BN pseudo-C (PStringBase sprintf chains, HousePaymentList iteration,IsPaidInFull/ConstructRentWarningMessage-style formatting). Sized as genuinely disproportionate for tonight's batch — this matches the task brief's own pre-authorized fallback ("if the whole owned-house wire half balloons beyond reach tonight, land the default-content tab + the enum/parser groundwork, and file the remainder as a precise ISSUES entry"). Decision: land the mount (default authored content, zero wire) and the wire groundwork (enum route registration + parsers +RuntimeHouseStateraw-field owner), and file the seven line builders as an ISSUES entry rather than porting them tonight.
Wire — GameEventType already has all four ids (corrects the handoff)
The handoff claimed "0x0227/0x0228 absent from the enum". Checked
src/AcDream.Core.Net/Messages/GameEventType.cs:73-76 directly — all
four are already defined:
HouseData = 0x0225
HouseStatus = 0x0226
UpdateRentTime = 0x0227
UpdateRentPayment= 0x0228
What's actually missing (confirmed by grepping src/ for every
GameEventType.House*/UpdateRent* reference: zero hits) is routing —
no parser, no GameEventWiring registration, no consumer. ACE's own
writers for two of the four are themselves stubs worth knowing about
before treating any live capture as ground truth:
GameEventHouseUpdateRentTime.cs always writes a hardcoded rentTime = 0u; GameEventHouseUpdateRentPayment.cs always writes an empty
List<HousePayment>. GameEventHouseData/GameEventHouseStatus write
real data (HouseData/(uint)WeenieError).
Marker table — s_rgLocations[0x35] (53 entries), verbatim
docs/research/named-retail/acclient_2013_pseudo_c.txt:977225-977651.
Struct gmMapUI::LocationRolloverInfo { uint X,Y,Width,Height; wchar_t* Name; } (acclient.h:55686). Values are direct pixel rects passed to
MoveTo/ResizeTo on m_pMap — no coordinate transform (unlike the
player/house markers). Ported verbatim into a static C# array (see
MapLocations.cs below) — town list cross-checked as the complete classic
Dereth town set (Holtburg, Arwic, Yaraq, Shoushi, Rithwic, Samsur, Zaikhal,
Xarabydun, Yanshi, Nanto, Kara, Lin, Mayoi, Baishi, Sawato, Tou-Tou,
Al-Jalima, Al-Arqas, Qalaba'r, Silyun, Bandit Castle, Fort Tethana, Glenden
Wood, Cragstone, Dryreach, Eastham, Lytelthorpe, MacNiall's Freehold,
Linvak Tukal, Uziz, Wai Jhou, Timaru, Sanamar, Stonehold, Redspire,
Bluespire, Greenspire, Neydisa, Mt Esper-Crater Village, Plateau Village,
Fiun Outpost, Danby's Outpost, Candeth Keep, Khayyaban, Kryst, Hebian-to,
Oolutanga's Refuge, Ulgrim's Island, Ayan Baqur, Aerlinthe Island,
Singularity Caul Island).
Seams reused (no new infrastructure needed)
- Panel mount recipe:
RetailUiRuntime.MountSocialPanel(src/AcDream.App/UI/RetailUiRuntime.cs:3033-3257) andLayout.SocialPanelController.cs— copied for a 2-tabMapHousePanelController. UiTabPanel(src/AcDream.App/UI/UiTabPanel.cs) — same tab-table/ActivateTabBehaviormechanism.ToolbarController+RetailPanelCatalog— add panel id 16 to bothMountedandToolbararrays; button un-ghosts automatically once its panel id resolves viaTryGetWindowName.RetailTooltipPresenterviaUiElement.AuthoredTooltipText/AuthoredTooltipEnabled— town marker tooltips, closing TS-85's last item.Layout.RowTemplateResolverpattern — the per-town hotspot child is a template-instantiated element exactly like Friends/Squelch/Fellowship rows (template LayoutDesc/element resolved fromm_pMap's own0x48/0x47attrs).LandDefs.GidToLcoord/LcoordToGid(src/AcDream.Core/Physics/LandDefs.cs) — coordinate math, already ported and tested (issue #106).ServerPosition(src/AcDream.Core.Net/Messages/CreateObject.cs:479) — House wire'sPositionfield, already parsed elsewhere.WorldTimeService.CurrentCalendar/DerethDateTime— calendar data; only a formatter matching retail's"Date: %s\nTime: %s"shape is new.RuntimeTradeState(src/AcDream.Runtime/Gameplay/RuntimeTradeState.cs) — J4.x-style session-scoped owner pattern, template forRuntimeHouseStateif the wire groundwork lands.
Open items carried into slice reports
- Live probe (slice 1) must re-confirm the desk-verified slot/panelId/ button facts above against the ACTUAL live DAT install, not just trust the committed fixture + FA doc (which are consistent with each other but both need the live cross-check the task mandates).
m_pMap's attrs0x47/0x48(hotspot template element/LayoutDesc) need a live read — the decomp explains their MEANING but not their VALUE.- House ListBox
0x100001e6's authored default content (the "You do not currently own a house" text) needs a live read to confirm it's baked into the LayoutDesc rather than something the client synthesizes. - Coordinate/N-S-E-W format string for the outside coordinate readout is
unrecoverable from this decomp dump (BN elides it behind an
unresolved vtable-slot placeholder) — implemented per the task's
pre-authorized fallback (sign-based N/S/E/W, one-decimal magnitude,
"42.1N, 33.6E"shape), marked inferred-pending-verification in code. - Seven House
Display*line builders deferred to an ISSUES entry (see above) — ownership of the wire groundwork (parsers,RuntimeHouseStateraw fields) still lands this session so a future session can pick up the text formatting without re-doing the wire. - Toolbar button
0x10000197(panel id 12 = Social) appears to be a real, currently-ghosted toolbar entry for the Social panel — contradictsSocialPanelController's class doc ("No toolbar button authors this id"). Flagged, not chased — outside Batch C's scope.