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)];
}
}
}