feat(ui): Map/House panel — slices 2+3, panel shell + Map tab
Mounts host 0x2100006E slot 0x1000018C (RetailPanelCatalog.MapHouse = 16) as a two-tab UiTabPanel (Map default, House second) through the OP3/FA3 recipe (LayoutImporter.Build -> Bind -> ActivateTabBehavior). Toolbar button 0x1000019A un-ghosts (added to both RetailPanelCatalog.Mounted and .Toolbar). Combined into one commit because MapHousePanelController.Bind depends on both MapPageController and HousePageController existing — splitting them would mean landing dead code first. Map tab (gmMapUI, MapPageController): - Calendar formatter matching gmMapUI::Update's "Date: %s\nTime: %s" shape, reusing WorldTimeService.CurrentCalendar (new Func<DerethDateTime.Calendar> dependency threaded through InteractionRetainedUiDependencies/GameWindow — a stable long-lived service, not routed through the deferred-binding machinery Radar's per-session state needs). MonthName enum values already match retail display text; HourName's "AndHalf" suffix is rewritten to "-and-Half". - Coordinate math + marker placement reuse RadarCoordinates/ LandDefs.GidToLcoord verbatim (both already byte-exact ports of CPlayerSystem::InqPlayerCoords/LandDefs::gid_to_lcoord) — no re-port. PlaceMarkerOnMap's centering math (m_x0 + x - w/2) ported from gmMapUI::PlaceMarkerOnMap @0x004a18b0. Indoor gating clears the coordinate text and hides the player marker, matching gmMapUI::Update's else branch. - 53-town s_rgLocations table ported verbatim into MapLocations.cs. Markers built once at bind time via the panel's own RowTemplateResolver against m_pMap's authored hotspot-template attrs (0x47/0x48), with literal-string tooltips through AuthoredTooltipText/Enabled (RetailTooltipPresenter) — closes divergence-register row TS-85's last item, gmMapUI::AddMapNote @0x004A1C51. - Structural finding: m_pMap (0x100001EC) is itself authored as a Type-1 BUTTON (the GM click-to-teleport hook at gmMapUI::ListenToElementMessage), and the player/house icons (0x100001ED/EE) are its own NESTED children, not siblings — UiButton.ConsumesDatChildren swallows them from the normally-built tree. Both are re-resolved standalone through the same template resolver the town hotspots use and reattached under m_pMap. House tab (gmHouseUI, HousePageController): mounts the ListBox (0x100001E6) with its authored row template, wired to an empty Lines() source by default — genuinely empty until Slice 4's wire lands, matching retail's own PostInit (no Update call, no static content). 21 new tests (7 MapHousePanelControllerTests, 14 MapPageControllerTests): tab table pairing, close button, town-hotspot count/tooltips, calendar formatter golden values (Frostfell 27/119 P.Y., every HourName incl. AndHalf), player/house marker placement and indoor-gating reproduced against the real fixture via already-tested RadarCoordinates (no re-derivation). Fixture map_house_2100006E_1000018C.json captured via the shared RetailLayoutFixtureGenerator (other 34 fixtures deliberately NOT regenerated — out of scope for this batch, would touch unrelated panels' schema drift). Full solution builds clean; App suite 5391/0 failed/71 skipped (non-live; one earlier flaky streaming failure unrelated to this change, confirmed pre-existing on the branch before these commits). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
04841b6807
commit
6b0fa4ff0d
15 changed files with 5865 additions and 3 deletions
102
src/AcDream.App/UI/Layout/HousePageController.cs
Normal file
102
src/AcDream.App/UI/Layout/HousePageController.cs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Binds the House tab of the retail Map/House panel (<c>gmHouseUI</c>,
|
||||
/// class id <c>0x10000025</c>) — <see cref="MapHousePanelController"/>'s
|
||||
/// second page.
|
||||
///
|
||||
/// <para>
|
||||
/// Retail references: <c>gmHouseUI::PostInit @0x004a2710</c> resolves ONE
|
||||
/// <c>UIElement_ListBox</c> (<c>m_pTextBox = 0x100001e6</c>) and registers
|
||||
/// four notice handlers for wire opcodes <c>0x0225-0x0228</c>.
|
||||
/// <c>gmHouseUI::AddHousePanelText @0x004a2810</c> is
|
||||
/// <c>UIElement_ListBox::AddItemFromTemplateList(this, 0, nullptr)</c> —
|
||||
/// live-DAT-confirmed (<c>MapHousePanelSlotProbeTests</c>) as a single
|
||||
/// <c>UIElement_Text</c> (Type 12) row template at LayoutDesc
|
||||
/// <c>0x21000025</c> element <c>0x100001e7</c>, 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
|
||||
/// <c>PostInit</c> never calls <c>Update</c>/<c>DisplayHouseData</c>).
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Scope (Batch C, 2026-08-17 recon doc).</b> Of the seven
|
||||
/// <c>Display*</c> line builders <c>DisplayHouseData</c> calls, only
|
||||
/// <c>DisplayPurchaseTimeText @0x004a3110</c>'s two simple, fully-recovered
|
||||
/// literal strings ("You may buy another house immediately." / "...after
|
||||
/// you abandon this one.") are wired end-to-end this session — see
|
||||
/// <c>RuntimeHouseState.PurchaseAvailabilityText</c>. The other six
|
||||
/// (BuyPayment/RentPayment/BuyTime/RentTimes/Location/WarningText) only
|
||||
/// matter once a house is actually owned and are filed as an ISSUES entry
|
||||
/// rather than guessed at from FPU-mangled decomp.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class HousePageController
|
||||
{
|
||||
public const uint TextBoxId = 0x100001E6u;
|
||||
|
||||
public sealed record Bindings(
|
||||
Func<IReadOnlyList<string>> Lines,
|
||||
// Fires once when the page transitions to visible — the seam that
|
||||
// sends the outbound HouseQuery (0x021E) so the server has a
|
||||
// reason to answer with fresh HouseData/HouseStatus. NOT a ported
|
||||
// retail call site (PostInit never triggers a query) — an acdream
|
||||
// convention, documented as such (recon doc open item).
|
||||
Action? OnShown = null);
|
||||
|
||||
private readonly UiTemplateListBox _listBox;
|
||||
private readonly Bindings _bindings;
|
||||
private IReadOnlyList<string> _lastLines = Array.Empty<string>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
var controller = new HousePageController(listBox, bindings);
|
||||
controller.Refresh(bindings.Lines());
|
||||
return controller;
|
||||
}
|
||||
|
||||
/// <summary>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).</summary>
|
||||
public void Tick()
|
||||
{
|
||||
IReadOnlyList<string> lines = _bindings.Lines();
|
||||
if (lines.SequenceEqual(_lastLines)) return;
|
||||
Refresh(lines);
|
||||
}
|
||||
|
||||
public void OnShown() => _bindings.OnShown?.Invoke();
|
||||
|
||||
private void Refresh(IReadOnlyList<string> 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)];
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue