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:
parent
39c49e140e
commit
4817c17600
1 changed files with 274 additions and 0 deletions
274
docs/research/2026-08-17-map-house-recon.md
Normal file
274
docs/research/2026-08-17-map-house-recon.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue