feat(ui): House tab ownership text — DisplayPurchaseTimeText + RuntimeHouseState
Derived the mechanism from the decomp before writing code: neither gmHouseUI::PostInit @0x004a2710 nor gmMapUI::PostInit @0x004a1c70 sends a HouseQuery, and six of gmHouseUI's seven Display* builders early-return on m_pHouseData == 0. The only text a houseless character's House tab shows is gmHouseUI::DisplayPurchaseTimeText @0x004a3110's expired branch (it doesn't gate on m_pHouseData) — the local player's PropertyInt.HousePurchaseTimestamp plus HouseSystem::HasPurchaseWaitPeriodExpired renders exactly "You may buy another house immediately." for a fresh character. Exhaustive search of the 2013 EoR decomp, ACE, and the live DAT found zero support for a second "You do not currently own a house." line the task brief described — this commit ports what the decomp actually shows. Ships: - RuntimeHouseState: a minimal (no disposal, no construction-transaction Fault() point) Runtime owner per ISSUES #413's own sizing note, wired through GameEventWiring's existing HouseData/HouseStatus delegate holes, LiveSessionEventRouter, and GameRuntime.HouseOwner. Participates in RuntimeGenerationReset (new House stage) since a fresh login must not show a stale character's house state. - HousePageController.Bindings.Lines/OnShown wired to real data; OnShown fires WorldSession.SendHouseQuery() on tab-open (AD-107: an acdream trigger, not a ported retail call site — filed in the divergence register). - Fixed a real bug found along the way: HousePageController.Bind never wired UiTemplateListBox.TemplateResolver, so no row could ever render regardless of Lines content. Now reuses the Map tab's generic hotspot resolver. Live-verified against a real local ACE server and the +Acdream character (--session-config auto-select + a UI automation script): screenshot and structural UI-tree dump both confirm the House tab renders exactly "You may buy another house immediately." Graceful logout confirmed both launches. ISSUES #413 narrowed to its one remaining piece: the six owned-house-only Display* builders (DisplayBuyPayment/RentPayment/BuyTime/RentTimes/ Location/WarningText), unexercisable without a test character that owns a house. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
eb6f3bd8c8
commit
06512f0957
13 changed files with 601 additions and 99 deletions
161
docs/ISSUES.md
161
docs/ISSUES.md
|
|
@ -24,62 +24,72 @@ What does NOT go here:
|
|||
- Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending.
|
||||
- Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed.
|
||||
|
||||
## #413 — House tab shows no content (owned-house display, seven Display* line builders unported)
|
||||
## #413 — House tab shows no content (owned-house display, six Display* line builders unported)
|
||||
|
||||
**Status:** OPEN (filed 2026-08-17, Batch C — overnight hover/UI round, Map/House
|
||||
toolbar panel).
|
||||
**Status:** NARROWED 2026-08-17 (House-tab ownership-text closer session).
|
||||
Items 1 and 2 below are DONE; item 3 (six owned-house-only builders) remains
|
||||
OPEN and is the entire remaining scope.
|
||||
|
||||
**What's shipped.** The Map/House panel (host `0x2100006E` slot `0x1000018C`,
|
||||
`RetailPanelCatalog.MapHouse = 16`) is fully mounted with a working Map tab
|
||||
(date/time, coordinates, player/house markers, 53 town hotspots with retail
|
||||
tooltips) and a House tab that mounts correctly with its authored ListBox
|
||||
(`0x100001E6`) and row template — but the ListBox is genuinely EMPTY, matching
|
||||
retail's own `gmHouseUI::PostInit @0x004a2710` (it never calls `Update`/
|
||||
`DisplayHouseData`; the box only populates after a server notice arrives).
|
||||
The full wire *parsing* groundwork also shipped: outbound HouseQuery
|
||||
(`0x021E`, `WorldSession.SendHouseQuery`) and inbound parsers for all four
|
||||
House opcodes (`GameEvents.ParseHouseData`/`ParseHouseStatus`/
|
||||
`ParseUpdateRentTime`/`ParseUpdateRentPayment`, wired into
|
||||
`GameEventWiring.WireAll` as optional delegate holes) — all tested
|
||||
(`HouseEventsTests`, `GameEventWiringTests.WireAll_HouseFamily_ReachesTheirCallbacks`).
|
||||
**What's shipped (this session, on top of Batch C's mount + parser
|
||||
groundwork).**
|
||||
|
||||
**What's missing — three pieces, all deliberately deferred (the task's own
|
||||
pre-authorized fallback: "land the default-content tab + the enum/parser
|
||||
groundwork, and file the remainder as a precise ISSUES entry"):**
|
||||
1. **`RuntimeHouseState`** (`src/AcDream.Runtime/Gameplay/RuntimeHouseState.cs`)
|
||||
— the minimal owner ISSUES originally called for ("a lighter read-only
|
||||
mirror... no full owner ceremony"): no `GameRuntimeConstructionPoint`
|
||||
fault-injection entry, no `IDisposable`/`construction.Own`, since it holds
|
||||
no live-object side effects. It DOES participate in
|
||||
`RuntimeGenerationReset` (new stage `RuntimeGenerationResetStage.House`,
|
||||
between `Trade` and `BeginEntityRetirement`) since a fresh login must not
|
||||
show a previous character's house-query result. Wired end-to-end:
|
||||
`GameEventWiring`'s `onHouseData`/`onHouseStatus` delegate holes →
|
||||
`LiveSessionEventRouter`'s new `LiveSocialSessionBindings.House` →
|
||||
`GameRuntime.HouseOwner` → `MapHouseRuntimeBindings.HouseLines`/
|
||||
`HouseShown` → `HousePageController`.
|
||||
|
||||
1. **A `RuntimeHouseState` owner.** The wire delegate holes exist but nothing
|
||||
consumes them yet — no session-scoped state class holds the parsed
|
||||
`HouseData`/`HouseStatus`/rent fields, and `HousePageController.Bindings.Lines`/
|
||||
`OnShown` are unwired defaults (`() => Array.Empty<string>()`, no-op).
|
||||
Sizing note: a FULL `GameRuntime`-integrated owner (the
|
||||
`RuntimeTradeState` precedent — construction-transaction `Fault()`
|
||||
injection point, `GameRuntimeConstructionPoint` enum entry, disposal
|
||||
ordering, convergence tracking at 2-3 sites) is a substantial standalone
|
||||
undertaking; judged disproportionate to add alongside the completed Map
|
||||
tab in one session. A lighter read-only mirror (closer to
|
||||
`FriendsState`/`SquelchState`'s weight, no full owner ceremony) may be
|
||||
the right shape — evaluate against the codebase's "single canonical
|
||||
owner" architecture before choosing.
|
||||
2. **`gmHouseUI::DisplayPurchaseTimeText @0x004a3110`'s expired branch** —
|
||||
ported faithfully in `RuntimeHouseState.Recompute`: local player
|
||||
`PropertyInt.HousePurchaseTimestamp` (199 decimal) via
|
||||
`ClientObjectTable`, `HouseSystem::HasPurchaseWaitPeriodExpired(timestamp)
|
||||
= (nowEpoch - timestamp) > 0x278d00` (2,592,000 s = 30 days), and the two
|
||||
literal strings gated on `m_pHouseData == 0`. A fresh `+Acdream`-shaped
|
||||
character (no `HousePurchaseTimestamp` ever set) shows **exactly one
|
||||
line**: "You may buy another house immediately." — matching this issue's
|
||||
OWN original acceptance-test wording below, byte-verified against
|
||||
`data_7ab7f0` in the decomp. The not-expired `strftime`-formatted branch
|
||||
stays unported (its format string is BN-unrecoverable) — renders no
|
||||
line, not a guess.
|
||||
|
||||
2. **`gmHouseUI::DisplayPurchaseTimeText @0x004a3110`'s port** — the ONE
|
||||
builder decomp-confirmed simple enough to land (no `m_pHouseData` early
|
||||
return; reads the LOCAL PLAYER's own `PropertyInt.HousePurchaseTimestamp`
|
||||
(`= 199` decimal, confirmed already in `src/AcDream.Core/Properties/PropertyInt.cs:356`)
|
||||
and `HouseSystem::HasPurchaseWaitPeriodExpired(timestamp) =
|
||||
(Timer::get_real_time() - timestamp) > 0x278d00` — clean, no FPU noise,
|
||||
`0x278d00` = 2,592,000 s = 30 days, the house-abandon cooldown). Two fully
|
||||
recovered literal strings for the expired case: `"You may buy another
|
||||
house immediately."` (no house owned) / `"...after you abandon this
|
||||
one."` (owns a house) — these are what a fresh test character (no
|
||||
`HousePurchaseTimestamp` set, i.e. `0`) would show once queried, since
|
||||
`HasPurchaseWaitPeriodExpired(0)` is trivially true. The NOT-expired
|
||||
branch's future-dated wait message uses a `strftime` format string
|
||||
(`data_7ab7ec`) and a suffix (`". This restriction does not appl…"`)
|
||||
that BN truncates and this session did not attempt to recover further —
|
||||
port the expired branch first, mark the not-expired branch's suffix text
|
||||
as inferred-pending-verification if ported later.
|
||||
**Corrects a framing this session's task brief carried in from outside
|
||||
this doc**: the brief described retail as ALSO showing a preceding line
|
||||
"You do not currently own a house." No such string, in that or any close
|
||||
wording, exists anywhere in the 2013 EoR `acclient_2013_pseudo_c.txt`
|
||||
dump, in any `gmHouseUI`/`gmMapUI` method, in ACE's `GameEventHouseStatus`
|
||||
writer, or in the live-DAT House ListBox/page (re-confirmed empty this
|
||||
session — `MapHousePanelSlotProbeTests`, zero rows, zero sibling
|
||||
content). The closest strings found are UNRELATED generic command-error
|
||||
chat text ("You do not own a house!", WeenieError `0x45E`/`0x45F`; "You
|
||||
must own a house to use this command.", WeenieError `0x47F`), routed
|
||||
through the GENERIC WeenieError-to-chat dispatcher, never through
|
||||
`gmHouseUI`. This ISSUES entry's OWN pre-existing "Acceptance test once
|
||||
closed" line below (written before this session, by the same research
|
||||
pass that produced the recon doc) already named the single-line
|
||||
"You may buy another house immediately." text as the target — this
|
||||
session's mechanism derivation independently reached the same
|
||||
conclusion and is now the shipped, tested behavior.
|
||||
|
||||
3. **The other six `Display*` line builders** (`DisplayBuyPayment`,
|
||||
**Also fixed in the same pass: `HousePageController.Bind` never wired
|
||||
`UiTemplateListBox.TemplateResolver`.** Without it,
|
||||
`AddItemFromTemplateList` always returns null (no resolver = no row) —
|
||||
the ListBox would have stayed visually empty regardless of `Lines`
|
||||
content. `HousePageController.Bindings` gained a `TemplateResolver`
|
||||
parameter, wired in `Bind`; `RetailUiRuntime.MountMapHousePanel` supplies
|
||||
the SAME generic `ResolveHotspotTemplate` the Map tab's town hotspots
|
||||
already use (a plain `(layoutId, elementId) -> UiElement` resolve+build,
|
||||
nothing map-specific about it despite the binding's name).
|
||||
|
||||
**What remains open — item 3, the entire surviving scope:**
|
||||
|
||||
3. **The six owned-house-only `Display*` line builders** (`DisplayBuyPayment`,
|
||||
`DisplayRentPayment`, `DisplayBuyTime`, `DisplayRentTimes`,
|
||||
`DisplayLocation`, `DisplayWarningText` — all called from
|
||||
`gmHouseUI::DisplayHouseData @0x004a3380`). Each is dozens-to-a-few-hundred
|
||||
|
|
@ -87,25 +97,46 @@ groundwork, and file the remainder as a precise ISSUES entry"):**
|
|||
chains, `HousePaymentList` iteration, `IsPaidInFull`/
|
||||
`ConstructRentWarningMessage`-style formatting) — genuinely sized as its
|
||||
own session, and only exercisable once a test character actually owns a
|
||||
house (not true of `+Acdream` today). `DisplayLocation` is the exception:
|
||||
its own logic is clean (`GetHouseLocation` → `LandDefs::gid_to_lcoord` →
|
||||
the SAME `(v-0x400)*0.1+0.5` transform the Map tab already ports via
|
||||
`RadarCoordinates`) but its output STRING format is BN-mangled the same
|
||||
way the Map tab's coordinate readout was — reuse whatever resolution
|
||||
that gets if/when #413's map coordinate format string is independently
|
||||
recovered.
|
||||
house (not true of `+Acdream` today; `RuntimeHouseState.ApplyHouseData`
|
||||
is wired and tested against a synthetic `GameEvents.HouseData`, but has
|
||||
never been exercised against a real ACE-owned house). `DisplayLocation`
|
||||
is the exception: its own logic is clean (`GetHouseLocation` →
|
||||
`LandDefs::gid_to_lcoord` → the SAME `(v-0x400)*0.1+0.5` transform the
|
||||
Map tab already ports via `RadarCoordinates`) but its output STRING
|
||||
format is BN-mangled the same way the Map tab's coordinate readout was —
|
||||
reuse whatever resolution that gets if/when #413's map coordinate format
|
||||
string is independently recovered.
|
||||
|
||||
**Reference:** `docs/research/2026-08-17-map-house-recon.md` (the full
|
||||
citation set: addresses, ACE cross-references, the two ACE writer stubs for
|
||||
UpdateRentTime/UpdateRentPayment). `src/AcDream.App/UI/Layout/HousePageController.cs`,
|
||||
**Reference:** `docs/research/2026-08-17-map-house-recon.md` (the recon);
|
||||
`src/AcDream.Runtime/Gameplay/RuntimeHouseState.cs` (this session's owner,
|
||||
full citation set in its own class doc); `docs/architecture/retail-divergence-register.md`
|
||||
AD-107 (the HouseQuery-on-tab-open trigger adaptation);
|
||||
`src/AcDream.App/UI/Layout/HousePageController.cs`,
|
||||
`src/AcDream.Core.Net/Messages/GameEvents.cs` (House parsers),
|
||||
`src/AcDream.Core.Net/GameEventWiring.cs` (delegate holes).
|
||||
|
||||
**Acceptance test once closed:** the House tab, on a fresh `+Acdream` connect
|
||||
with no owned house, shows "You may buy another house immediately." after
|
||||
the tab is opened (client sends `HouseQuery`, ACE replies `HouseStatus`,
|
||||
`RuntimeHouseState` clears `m_pHouseData`-equivalent, `DisplayPurchaseTimeText`'s
|
||||
expired/no-house branch fires).
|
||||
**Acceptance test — CLOSED for the houseless case, LIVE-VERIFIED, still the
|
||||
target for the owned-house case.** The House tab, on a fresh `+Acdream`
|
||||
connect with no owned house, shows "You may buy another house
|
||||
immediately." after the tab is opened (client sends `HouseQuery`, ACE
|
||||
replies `HouseStatus`, `RuntimeHouseState.ApplyHouseStatus` fires,
|
||||
`Recompute`'s expired/no-house branch renders the line) — unit-tested
|
||||
(`RuntimeHouseStateTests.HouseStatus_FreshCharacterWithNoTimestamp_ShowsBuyImmediatelyLine`),
|
||||
fixture-tested end-to-end through the real row template
|
||||
(`MapHousePanelControllerTests.Tick_RendersHouseLinesIntoTheAuthoredRowTemplate`),
|
||||
and CONNECTED-GATE-VERIFIED 2026-08-17 against a real local ACE server and
|
||||
the real `+Acdream` character (guid `0x5000000A`): a `--session-config`
|
||||
launch (auto-selecting the character to bypass the interactive
|
||||
character-select screen) plus a `ACDREAM_UI_PROBE_SCRIPT` automation script
|
||||
(click the Map/House toolbar button `0x1000019A`, switch to the House tab
|
||||
`0x100001F4`, dump the live UI tree, screenshot) produced a screenshot
|
||||
showing the House tab's ListBox rendering exactly "You may buy another
|
||||
house immediately." and a structural UI dump confirming the House page
|
||||
(`0x100001F7`), its ListBox (`0x100001E6`), and its ONE rendered row
|
||||
(`0x100001E7`, the authored template) all visible and correctly placed. Both
|
||||
launches ended with an ACE-confirmed graceful logout
|
||||
(`[session] graceful logout confirmed`). Still owed: the owned-house case
|
||||
once item 3 lands.
|
||||
|
||||
## #412 — Options panel Config tab content escapes the window frame (footer mid-panel, rows drawing below the window's bottom edge)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue