using System; using System.Collections.Generic; using System.Linq; using System.Numerics; namespace AcDream.App.UI.Layout; /// /// Binds the House tab of the retail Map/House panel (gmHouseUI, /// class id 0x10000025) — 's /// second page. /// /// /// Retail references: gmHouseUI::PostInit @0x004a2710 resolves ONE /// UIElement_ListBox (m_pTextBox = 0x100001e6) and registers /// four notice handlers for wire opcodes 0x0225-0x0228. /// gmHouseUI::AddHousePanelText @0x004a2810 is /// UIElement_ListBox::AddItemFromTemplateList(this, 0, nullptr) — /// live-DAT-confirmed (MapHousePanelSlotProbeTests) as a single /// UIElement_Text (Type 12) row template at LayoutDesc /// 0x21000025 element 0x100001e7, no scrollbar authored. The /// ListBox itself authors ZERO static child rows — the box starts genuinely /// empty until the first server notice populates it (retail's own /// PostInit never calls Update/DisplayHouseData). /// /// /// /// Scope — see ISSUES #413 for the full ledger. Batch C /// (2026-08-17) shipped the mount (this class) and the wire PARSING /// groundwork (GameEvents.ParseHouseData/ParseHouseStatus/ /// ParseUpdateRentTime/ParseUpdateRentPayment, /// GameEventWiring's four delegate holes, the outbound HouseQuery /// action). The House-tab ownership-text closer session (also 2026-08-17) /// wired to the minimal RuntimeHouseState /// owner and ported DisplayPurchaseTimeText @0x004a3110's expired /// branch — a fresh houseless character's House tab shows the single /// decomp-verified line "You may buy another house immediately.", /// live-connected-gate-verified (screenshot + structural UI-tree dump /// against the real +Acdream character on a local ACE server). The /// other six Display* line builders DisplayHouseData calls /// (owned-house-only content: buy/rent payments and times, location, /// warning text) remain unported — ISSUES #413's surviving scope. The /// night-round review (F2, 2026-08-17) moved the outbound HouseQuery send /// from a House-tab-open trigger to retail's real login-complete edge (see /// 's own doc), so by the time a player opens /// the House tab the data has usually already arrived. /// /// public sealed class HousePageController { public const uint TextBoxId = 0x100001E6u; public sealed record Bindings( Func> Lines, // Fires once when the page transitions to visible. Night-round // review F2 (2026-08-17): NOT wired to SendHouseQuery any more — // retail's HouseQuery (0x021E, CM_House::Event_QueryHouse // @0x006aaa00) is byte-decoded confirmed to fire exactly once at the // client's login-complete edge (tail-called, unconditionally, from // CPlayerSystem::InitializePlayer @0x00563570, right after // AttemptSendLoginCompleteNotification — both guarded by the SAME // once-per-session player_initialized flag), never from House-tab // activation; neither gmHouseUI::PostInit nor gmMapUI::PostInit // sends one on tab-open. See WorldSession.SendHouseQuery's own // production call sites (the graphical/headless first-entry- // completion edges) for where it's actually sent now. This hook // remains available for a genuinely page-shown concern, but no // current caller wires it. Action? OnShown = null, // Batch C House-ownership-text closer (2026-08-17): the ListBox's // OWN row template (LayoutDesc 0x21000025 element 0x100001E7, // live-DAT-confirmed by MapHousePanelSlotProbeTests) is resolved // through the SAME generic (templateLayoutId, templateElementId) -> // UiElement seam MapPageController.Bindings.TemplateResolver already // wires for the Map tab's town hotspots — it performs the identical // LayoutImporter.ImportInfos+Build operation, nothing map-specific // about it. Without this, UiTemplateListBox.AddItemFromTemplateList // always returns null (no resolver = no row), so Refresh silently // produced zero rows regardless of Lines — the gap this session // closes alongside the text composition itself. Func? TemplateResolver = null); private readonly UiTemplateListBox _listBox; private readonly Bindings _bindings; private IReadOnlyList _lastLines = Array.Empty(); private HousePageController(UiTemplateListBox listBox, Bindings bindings) { _listBox = listBox; _bindings = bindings; } public static HousePageController? Bind(UiElement page, Bindings bindings) { ArgumentNullException.ThrowIfNull(page); ArgumentNullException.ThrowIfNull(bindings); if (UiElement.FindDescendant(page, TextBoxId) is not UiTemplateListBox listBox) { Console.WriteLine( $"[D.2b] House tab: ListBox 0x{TextBoxId:X8} not found or not a template list box."); return null; } listBox.TemplateResolver = bindings.TemplateResolver; var controller = new HousePageController(listBox, bindings); controller.Refresh(bindings.Lines()); return controller; } /// Per-frame poll — cheap no-op when the line set hasn't /// changed (reference-content compare via SequenceEqual, mirroring the /// other social-panel pages' revision-gated rebuild discipline). public void Tick() { IReadOnlyList lines = _bindings.Lines(); if (lines.SequenceEqual(_lastLines)) return; Refresh(lines); } public void OnShown() => _bindings.OnShown?.Invoke(); private void Refresh(IReadOnlyList lines) { _lastLines = lines; _listBox.Flush(); foreach (string line in lines) { UiElement? row = _listBox.AddItemFromTemplateList(0); if (row is UiText text) text.LinesProvider = () => [new UiText.Line(line, Vector4.One)]; } } }