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

16 KiB
Raw Blame History

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 actualarg3/arg4reads) — 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_lcoordthe 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.