docs: Map/House panel recon — panelId 16/slot 0x1000018C resolved, LandDefs.GidToLcoord reuse, wire enum already present

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 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-17 01:27:28 +02:00
parent 39c49e140e
commit 4817c17600

View file

@ -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<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`) 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.