feat(ui): FA3 -- mount the social panel shell (Friends/Allegiance/Fellowship/Squelch)

Campaign FA slice FA3: retail's four-tab social panel (LayoutDesc
0x2100006E slot 0x1000018F, RetailPanelCatalog id 12), built on the OP3
OptionsPanelController recipe -- Type-8 tab host, ActivateTabBehavior,
per-page scoped controllers.

- SocialPanelController mounts the tab host and wires the close button;
  F3 (ToggleAllegiancePanel) and F4 (ToggleFellowshipPanel) open the
  panel and switch to their own tab, sharing the same gmPanelUI
  one-active-panel exclusivity every sibling main panel already has.
- The live-DAT tab table CORRECTS the coordinator addendum's x-order
  guess: button 0x1000028C ("Allegiance") pairs with page 0x10000291 and
  is the authored DEFAULT entry, not Friends -- each button's own page
  id and its own P0x57 (matching the F3/F4 ActionMap ids on the
  Allegiance/Fellowship pages specifically) both corroborate the real
  pairing. See SocialPanelController's class doc for the full table.
- SocialFellowshipPageController swaps the two authored empty/full
  frames (0x1000026B/0x10000275) on RuntimeFellowshipState's
  IsInFellowship -- both frames' full containment (name box, create
  button, checkboxes vs. roster list, six buttons) was confirmed by the
  live-mount probe, so a single Visible toggle per frame is the whole
  swap (closes lane-A unknown U6).
- SocialAllegiancePageController hides the monarch/patron blocks and
  blanks their name text to a literal space when
  RuntimeAllegianceState.Snapshot.HasProfile is false, using SCOPED
  FindDescendant lookups (the panel authors 0x10000492 twice, once per
  block).
- SocialFriendsPageController/SocialSquelchPageController bind their
  ListBoxes read-only to RuntimeCommunicationState's existing J4.1
  Friends/Squelch owners (names only), rebuilding on revision change.
  Their action buttons are honest INERT (D1) -- register row AD-79.
- UiTemplateListBox gains Flush() (lane A/D's "Gap found" prerequisite)
  so a poll-and-rebuild list can shrink between refreshes.
- RetailPanelCatalog.SocialPanel = 12, byte-verified from the live slot's
  own P0x10000029; listed in Mounted only (no toolbar button -- lane A
  §6.1: the open path is keyboard-only).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-12 02:58:09 +02:00
parent b560f415cd
commit 0a9ca2f1f9
12 changed files with 854 additions and 1 deletions

View file

@ -0,0 +1,94 @@
using System;
using System.Numerics;
using AcDream.Core.Social;
namespace AcDream.App.UI.Layout;
/// <summary>
/// Campaign FA slice FA3, D1: the Squelch page's read-only roster — names
/// only, bound directly to the J4.1 <see cref="SquelchState"/> owner
/// (<c>RuntimeCommunicationState.Squelch</c>). Lists both squelched
/// characters (<see cref="SquelchDatabase.Characters"/>, keyed by the
/// character's own display name) and squelched accounts
/// (<see cref="SquelchDatabase.Accounts"/>, whose dictionary KEY is the
/// account name itself). Rebuilds on <see cref="SquelchState.Revision"/>
/// change; polled every frame from <see cref="SocialPanelController.Tick"/>.
///
/// <para>
/// <b>D1 — inert actions.</b> Same disposition as
/// <see cref="SocialFriendsPageController"/>: the three buttons
/// (<c>0x10000547</c>/<c>0x1000054B</c>/<c>0x1000054C</c>) are left
/// AUTHORED and CLICKABLE with no handler. See this commit's single
/// register row covering the whole D1 Friends/Squelch inert-actions scope.
/// </para>
///
/// <para>
/// Row template <c>0x21000060</c>/<c>0x10000541</c> (FA3 live-mount probe);
/// see <see cref="SocialPanelRowText"/> for the name-text resolution note
/// (<c>gmSquelchUI</c> is outside this campaign's decompiled scope).
/// </para>
/// </summary>
public sealed class SocialSquelchPageController
{
private const uint ListBoxId = 0x1000053Eu;
private readonly UiTemplateListBox _listBox;
private readonly SquelchState _squelch;
private long _lastRevision = long.MinValue;
private SocialSquelchPageController(UiTemplateListBox listBox, SquelchState squelch)
{
_listBox = listBox;
_squelch = squelch;
}
public static SocialSquelchPageController? Bind(
UiElement pageRoot,
SquelchState squelch,
Func<uint, uint, UiElement?> templateResolver)
{
ArgumentNullException.ThrowIfNull(pageRoot);
ArgumentNullException.ThrowIfNull(squelch);
ArgumentNullException.ThrowIfNull(templateResolver);
if (UiElement.FindDescendant(pageRoot, ListBoxId) is not UiTemplateListBox listBox)
{
Console.WriteLine(
$"[D.2b] SocialSquelchPageController: ListBox 0x{ListBoxId:X8} not "
+ "found — Squelch page will not populate.");
return null;
}
listBox.TemplateResolver = templateResolver;
var controller = new SocialSquelchPageController(listBox, squelch);
controller.Refresh();
return controller;
}
public void Tick()
{
long revision = _squelch.Revision;
if (revision == _lastRevision) return;
Refresh();
}
private void Refresh()
{
_lastRevision = _squelch.Revision;
_listBox.Flush();
SquelchDatabase database = _squelch.Snapshot();
foreach (SquelchInfo character in database.Characters.Values)
AddRow(character.Name);
foreach (string accountName in database.Accounts.Keys)
AddRow(accountName);
}
private void AddRow(string name)
{
UiElement? row = _listBox.AddItemFromTemplateList(0);
if (row is null) return;
if (SocialPanelRowText.FindDeepest(row) is { } text)
text.LinesProvider = () => [new UiText.Line(name, Vector4.One)];
}
}