acdream/docs/research/2026-08-17-map-house-recon.md
Erik 4817c17600 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>
2026-08-17 01:27:28 +02:00

274 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.