From 4817c17600b89e513f1463f8afaa0710b7a97b2f Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 17 Aug 2026 01:27:28 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20Map/House=20panel=20recon=20=E2=80=94?= =?UTF-8?q?=20panelId=2016/slot=200x1000018C=20resolved,=20LandDefs.GidToL?= =?UTF-8?q?coord=20reuse,=20wire=20enum=20already=20present?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Saves the overnight-round recon (embedded findings + this session's desk verification) for auditability before implementation starts. Corrects two handoff claims: the panel id is 16 (already resolved by the existing FA campaign's full 16-slot gmPanelUI::SetupChildren dump, not a guess from {1,2,6,14}), and GameEventType already defines all four House opcodes (0x0225-0x0228) — what's missing is routing, not the enum. Identifies that LandDefs.GidToLcoord/LcoordToGid (src/AcDream.Core/Physics/LandDefs.cs) is an existing tested port of LandDefs::gid_to_lcoord, reusable for both the Map tab's coordinate math and the House location display — no re-port needed. Cites the toolbar button (0x1000019A, panel id 16), the 53-entry s_rgLocations marker table verbatim, the ServerPosition wire struct reuse for HouseData.Position, and the AuthoredTooltipText/RetailTooltipPresenter seam that will close register row TS-85's last item (gmMapUI::AddMapNote). Co-Authored-By: Claude Fable 5 --- docs/research/2026-08-17-map-house-recon.md | 274 ++++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 docs/research/2026-08-17-map-house-recon.md diff --git a/docs/research/2026-08-17-map-house-recon.md b/docs/research/2026-08-17-map-house-recon.md new file mode 100644 index 00000000..9ca33f17 --- /dev/null +++ b/docs/research/2026-08-17-map-house-recon.md @@ -0,0 +1,274 @@ +# 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): `MoveTo(m_x0 + (int)x + - width/2, m_y0 + (int)y - height/2)`, `SetVisible(1)`. 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. +- `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.