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:
parent
b560f415cd
commit
0a9ca2f1f9
12 changed files with 854 additions and 1 deletions
78
src/AcDream.App/UI/Layout/SocialFellowshipPageController.cs
Normal file
78
src/AcDream.App/UI/Layout/SocialFellowshipPageController.cs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
using System;
|
||||
using AcDream.Runtime;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign FA slice FA3: the Fellowship page's empty-state frame swap —
|
||||
/// the ONLY behavior this SHELL slice owns for this page (roster rows,
|
||||
/// vitals, create dialog, and the option row un-dims are FA4 scope).
|
||||
///
|
||||
/// <para>
|
||||
/// Retail <c>gmFellowshipUI::Update @0x0048F440</c>
|
||||
/// (docs/research/2026-08-11-fa-panel-structure.md §4.5):
|
||||
/// <c>m_pNotInAFellowshipFrame</c> (<c>0x1000026B</c>) shows and
|
||||
/// <c>m_pInAFellowshipFrame</c> (<c>0x10000275</c>) hides when
|
||||
/// <c>m_pFellowship == null</c>, and vice-versa. Both are AUTHORED sibling
|
||||
/// containers of the fellowship page (<c>0x10000292</c>) — confirmed by the
|
||||
/// FA3 live-mount probe: <c>0x1000026B</c> holds the name-entry box, Create
|
||||
/// button, and all three visible option checkboxes; <c>0x10000275</c> holds
|
||||
/// the roster list and all six member-management buttons. A single
|
||||
/// <see cref="UiElement.Visible"/> toggle on each container is therefore the
|
||||
/// WHOLE empty-state swap; no per-child hiding is needed (closes lane-A
|
||||
/// unknown U6).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class SocialFellowshipPageController
|
||||
{
|
||||
private readonly UiElement _notInFellowshipFrame;
|
||||
private readonly UiElement _inFellowshipFrame;
|
||||
private readonly Func<RuntimeFellowshipSnapshot> _snapshot;
|
||||
|
||||
private SocialFellowshipPageController(
|
||||
UiElement notInFellowshipFrame,
|
||||
UiElement inFellowshipFrame,
|
||||
Func<RuntimeFellowshipSnapshot> snapshot)
|
||||
{
|
||||
_notInFellowshipFrame = notInFellowshipFrame;
|
||||
_inFellowshipFrame = inFellowshipFrame;
|
||||
_snapshot = snapshot;
|
||||
}
|
||||
|
||||
public static SocialFellowshipPageController? Bind(
|
||||
UiElement pageRoot,
|
||||
Func<RuntimeFellowshipSnapshot> snapshot)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(pageRoot);
|
||||
ArgumentNullException.ThrowIfNull(snapshot);
|
||||
|
||||
if (UiElement.FindDescendant(pageRoot, 0x1000026Bu) is not { } notIn
|
||||
|| UiElement.FindDescendant(pageRoot, 0x10000275u) is not { } inFellowship)
|
||||
{
|
||||
Console.WriteLine(
|
||||
"[D.2b] SocialFellowshipPageController: empty/full frame pair "
|
||||
+ "(0x1000026B/0x10000275) not found — fellowship page will not "
|
||||
+ "swap its empty state.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var controller = new SocialFellowshipPageController(notIn, inFellowship, snapshot);
|
||||
controller.Tick();
|
||||
return controller;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-reads the live snapshot and applies retail's frame swap. Cheap (two
|
||||
/// bool writes) — called every frame from
|
||||
/// <see cref="SocialPanelController.Tick"/> rather than gated on
|
||||
/// page-shown/revision, since <see cref="UiElement.Visible"/> has no
|
||||
/// data-driven provider mechanism of its own (unlike <see cref="UiText.LinesProvider"/>,
|
||||
/// which is polled by the render loop already).
|
||||
/// </summary>
|
||||
public void Tick()
|
||||
{
|
||||
bool inFellowship = _snapshot().IsInFellowship;
|
||||
_notInFellowshipFrame.Visible = !inFellowship;
|
||||
_inFellowshipFrame.Visible = inFellowship;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue