# 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): resolves `m_pDateTimeText=0x100001eb` (`UIElement_Text`, DynamicCast 0xc), `m_pCoordinateText=0x100001ef` (`UIElement_Text`), `m_pPlayerLocationIcon=0x100001ed`, `m_pHouseLocationIcon=0x100001ee`, `m_pMap=0x100001ec`. Reads `m_pMap`'s own int attrs `0x1000004e/4f/50/51` into `m_boxMapMarkerArea` (x0,x1,y0,y1) — the marker-area rect. Reads `m_pMap`'s enum attr `0x47` (**literal small property id, NOT `0x10000047`**) and DataID attr `0x48` into a `QualifiedDataID(id, 0x23)` (category `0x23` = LayoutDesc) → `DBObj::Get`. If that resolves, loops the 53-entry `s_rgLocations` table calling `AddMapNote(this, m_pMap, var_c /*=attr 0x47, the template ELEMENT id*/, eax_10 /*=the resolved LayoutDesc*/, &s_rgLocations[i])` for each. **So `0x47`/`0x48` on `m_pMap` together name a template (LayoutDesc, element) pair for the per-town hotspot widget** — the exact same "authored template" pattern `Layout.RowTemplateResolver` already 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's `UiElement.AuthoredTooltipText` + `AuthoredTooltipEnabled`, served by `RetailTooltipPresenter` (`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 named `gmMapUI::AddMapNote @0x004A1C51` as the last unported `SetTooltip` call 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 the `arg3`/`arg4` reads. A direct capstone disassembly of the raw bytes at `0x004a18b0` in the PDB-paired `acclient.exe` recovers 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))`, then `SetVisible(1)`. Constants read from `.rdata`: `0x79bac8`=10.0, `0x7aac78`=1024.0, `0x7aac70`=-1/2048, `0x7aac68`=2047.0. `w`/`h` are `UIRegion::GetWidth`/`GetHeight` halved by INTEGER (truncating) division, matching retail's `cdq;sub;sar` idiom. Golden case: marker area (6,8)-(247,258), 10x10 icon, position 0.0N/0.0E → (122,128) center — reproduced exactly. Ported at `src/AcDream.App/UI/Layout/MapPageController.cs`'s `PlaceMarker`. 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 actual `arg3`/`arg4` reads) — this formula is the handoff's own already-verified reading and is accepted as-is; the underlying `+x-w/2` / `+y-h/2` centering 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-arms `m_nextUpdate = Timer::cur_time + 5.0` every call (5 s cadence, driven by `ListenToGlobalMessage`'s `arg2==3` tick case). Date/time block: builds `"Date: %s\nTime: %s"` from `GameTime::GetDateTimeString`, only calls `SetText` when the string actually differs (a change-detect, not a re-stamp every 5 s). Coordinate block, gated on `CPlayerSystem::IsOutside()`: - **outside**: `CPlayerSystem::InqPlayerCoords` → sign-based N/S/E/W selection (heavily FPU-mangled — BN elides the actual printf format string behind a `Formatted`/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**: `SetText` to 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.5` transform on both axes → `PlaceMarkerOnMap(m_pHouseLocationIcon, x, y)`. - `CPlayerSystem::InqPlayerCoords @0x00560090` (pc:364615): confirms the `(lcoord - 0x400) * 0.1 + 0.5` display transform per axis, fed by `CPhysicsObj::get_landscape_coord`. Which of that function's two raw outputs maps to which InqPlayerCoords axis is ambiguous in the BN decomp (a `esp+0x10`/`esp+0x24` swap 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** at `src/AcDream.Core/Physics/LandDefs.cs:73` (`LandDefs.GidToLcoord`, issue #106, cross-checked against ACE) — the `edx_2 < 0x100` low-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 @0x004a2350` idMessage `0x1c` handles a GM-only ("`PlayerDesc:: PlayerIsPSR`") click-to-teleport on the map (`lcoord_to_gid` from 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): resolves `m_pTextBox=0x100001e6` (`UIElement_ListBox`, DynamicCast 5). Registers FOUR notice handlers: `0x4dd225` (HouseData), `0x4dd226` (HouseStatus), `0x4dd227` (UpdateRentTime), `0x4dd228` (UpdateRentPayment) — matching wire opcodes `0x0225-0x0228`. - `gmHouseUI::GetHouseLocation @0x004a27b0`: reads `m_pHouseData` at offset `0x74` (an enum, `== 4` is a short-circuit "no location" case) or falls through to `Position::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 from `DisplayHouseData @0x004a3380` and from both `Update` overloads. 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 + `RuntimeHouseState` raw-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`. `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`) and `Layout.SocialPanelController.cs` — copied for a 2-tab `MapHousePanelController`. - `UiTabPanel` (`src/AcDream.App/UI/UiTabPanel.cs`) — same tab-table/ `ActivateTabBehavior` mechanism. - `ToolbarController` + `RetailPanelCatalog` — add panel id 16 to both `Mounted` and `Toolbar` arrays; button un-ghosts automatically once its panel id resolves via `TryGetWindowName`. - `RetailTooltipPresenter` via `UiElement.AuthoredTooltipText`/ `AuthoredTooltipEnabled` — town marker tooltips, closing TS-85's last item. - `Layout.RowTemplateResolver` pattern — the per-town hotspot child is a template-instantiated element exactly like Friends/Squelch/Fellowship rows (template LayoutDesc/element resolved from `m_pMap`'s own `0x48`/ `0x47` attrs). - `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's `Position` field, 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 for `RuntimeHouseState` if the wire groundwork lands. ## Open items carried into slice reports 1. 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). 2. `m_pMap`'s attrs `0x47`/`0x48` (hotspot template element/LayoutDesc) need a live read — the decomp explains their MEANING but not their VALUE. 3. 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. 4. 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. 5. Seven House `Display*` line builders deferred to an ISSUES entry (see above) — ownership of the wire groundwork (parsers, `RuntimeHouseState` raw fields) still lands this session so a future session can pick up the text formatting without re-doing the wire. 6. Toolbar button `0x10000197` (panel id 12 = Social) appears to be a real, currently-ghosted toolbar entry for the Social panel — contradicts `SocialPanelController`'s class doc ("No toolbar button authors this id"). Flagged, not chased — outside Batch C's scope.