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:
Erik 2026-08-17 03:38:16 +02:00
parent eb6f3bd8c8
commit 06512f0957
13 changed files with 601 additions and 99 deletions

View file

@ -60,15 +60,24 @@ public enum RuntimeGenerationResetStage
/// beside its fellowship/allegiance precedents.
/// </summary>
Trade = 14,
BeginEntityRetirement = 15,
RetireEntities = 16,
DrainHostProjection = 17,
CompleteCanonicalEntities = 18,
CompleteHostProjection = 19,
ChatIdentity = 20,
PlayerSnapshots = 21,
PlayerIdentity = 22,
Complete = 23,
/// <summary>
/// Batch C (Map/House toolbar panel, 2026-08-17): the House tab's
/// query result is session-scoped like fellowship/allegiance/trade
/// above — a fresh login must not show a previous character's house
/// data. See <see cref="RuntimeHouseState"/>'s class doc for why this
/// owner is lighter-weight than its three siblings (no disposal, no
/// construction-transaction Fault() point).
/// </summary>
House = 15,
BeginEntityRetirement = 16,
RetireEntities = 17,
DrainHostProjection = 18,
CompleteCanonicalEntities = 19,
CompleteHostProjection = 20,
ChatIdentity = 21,
PlayerSnapshots = 22,
PlayerIdentity = 23,
Complete = 24,
}
public readonly record struct RuntimeGenerationResetSnapshot(
@ -119,6 +128,7 @@ public sealed class RuntimeGenerationReset
private readonly RuntimeFellowshipState _fellowship;
private readonly RuntimeAllegianceState _allegiance;
private readonly RuntimeTradeState _trade;
private readonly RuntimeHouseState _house;
private ResetState? _state;
private RuntimeGenerationToken _lastCompletedGeneration;
private bool _hasCompletedGeneration;
@ -136,7 +146,8 @@ public sealed class RuntimeGenerationReset
RuntimeLocalPlayerIdentityState identity,
RuntimeFellowshipState fellowship,
RuntimeAllegianceState allegiance,
RuntimeTradeState trade)
RuntimeTradeState trade,
RuntimeHouseState house)
{
_transit = transit ?? throw new ArgumentNullException(nameof(transit));
_communication = communication
@ -157,6 +168,7 @@ public sealed class RuntimeGenerationReset
_allegiance = allegiance
?? throw new ArgumentNullException(nameof(allegiance));
_trade = trade ?? throw new ArgumentNullException(nameof(trade));
_house = house ?? throw new ArgumentNullException(nameof(house));
}
public RuntimeGenerationToken? ActiveRetiringGeneration =>
@ -333,6 +345,9 @@ public sealed class RuntimeGenerationReset
case RuntimeGenerationResetStage.Trade:
Advance(state, _trade.Clear);
break;
case RuntimeGenerationResetStage.House:
Advance(state, _house.ResetSession);
break;
case RuntimeGenerationResetStage.BeginEntityRetirement:
_ = _entityObjects.BeginSessionClear();
state.Retirements = _entityObjects