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

@ -216,6 +216,22 @@ public sealed record OptionsRuntimeBindings(
Func<AudioSettings> LoadAudio,
Action<AudioSettings> SaveAudio);
/// <summary>
/// Campaign FA slice FA3: the social panel's (Friends/Allegiance/
/// Fellowship/Squelch) own bindings — the FA2 owners' typed snapshot
/// readers plus the two J4.1 social-list owners, direct instances (not the
/// <see cref="AcDream.Runtime.IRuntimeSocialView"/> view seam bots use — that
/// view has no full-collection enumeration, only per-id lookup + counts;
/// the panel's read-only rows need the actual entries, same access shape
/// <see cref="AcDream.App.Composition.InteractionRetainedUiDependencies.Communication"/>
/// already exposes for every other Communication-owned consumer).
/// </summary>
public sealed record SocialRuntimeBindings(
Func<AcDream.Runtime.RuntimeFellowshipSnapshot> FellowshipSnapshot,
Func<AcDream.Runtime.RuntimeAllegianceSnapshot> AllegianceSnapshot,
AcDream.Core.Social.FriendsState Friends,
AcDream.Core.Social.SquelchState Squelch);
public sealed record InventoryRuntimeBindings(
ClientObjectTable Objects,
Func<uint> PlayerGuid,
@ -318,6 +334,7 @@ public sealed record RetailUiRuntimeBindings(
ConfirmationRuntimeBindings Confirmations,
AppraisalRuntimeBindings Appraisal,
OptionsRuntimeBindings Options,
SocialRuntimeBindings Social,
StackSplitQuantityState StackSplitQuantity,
BufferedUiRegistry? Plugins,
RetailUiPersistenceBindings? Persistence,
@ -402,6 +419,7 @@ public sealed class RetailUiRuntime : IDisposable
MountIndicators();
MountJumpPowerbar();
MountDialogFactory();
MountSocialPanel();
MountCharacter();
MountPlugins();
MountInventory();
@ -495,6 +513,7 @@ public sealed class RetailUiRuntime : IDisposable
public ExternalContainerController? ExternalContainerController { get; private set; }
public VendorUiController? VendorController { get; private set; }
public OptionsPanelController? OptionsPanelController { get; private set; }
public SocialPanelController? SocialPanelController { get; private set; }
public static RetailUiRuntime Mount(RetailUiRuntimeBindings bindings)
{
@ -537,6 +556,7 @@ public sealed class RetailUiRuntime : IDisposable
JumpPowerbarController?.Tick();
SelectedObjectController?.Tick(deltaSeconds);
ExternalContainerController?.Tick();
SocialPanelController?.Tick();
_itemCooldownController?.Tick();
DialogFactory?.Tick();
Host.Tick(deltaSeconds);
@ -559,9 +579,46 @@ public sealed class RetailUiRuntime : IDisposable
OpenSpellbook(SpellbookWindowPage.Components);
return true;
}
// Campaign FA slice FA3: F3/F4 — keyboard-only open paths (lane A
// §6.1: neither action authors a toolbar button). Both share the
// one social panel (RetailPanelCatalog.SocialPanel) and switch to
// their own tab; the panel participates in the SAME gmPanelUI
// one-active-panel exclusivity every sibling panel gets from
// RetailPanelUiController.RegisterMainPanel.
if (action == AcDream.UI.Abstractions.Input.InputAction.ToggleAllegiancePanel)
{
OpenSocialPanel(showAllegiance: true);
return true;
}
if (action == AcDream.UI.Abstractions.Input.InputAction.ToggleFellowshipPanel)
{
OpenSocialPanel(showAllegiance: false);
return true;
}
return ToolbarInputController?.Handle(action) == true;
}
/// <summary>Shared F3/F4 handler — same "toggle closes on a repeat press
/// of the SAME tab, otherwise show + switch" shape as <see cref="OpenSpellbook"/>.</summary>
private void OpenSocialPanel(bool showAllegiance)
{
bool visible = Host.IsWindowVisible(WindowNames.SocialPanel);
bool onTargetTab = showAllegiance
? SocialPanelController?.IsShowingAllegiance == true
: SocialPanelController?.IsShowingFellowship == true;
if (visible && onTargetTab)
{
CloseWindow(WindowNames.SocialPanel);
return;
}
if (showAllegiance)
SocialPanelController?.ShowAllegiance();
else
SocialPanelController?.ShowFellowship();
_panelUi.SetPanelVisibility(RetailPanelCatalog.SocialPanel, visible: true);
}
private void OpenSpellbook(SpellbookWindowPage page)
{
bool visible = Host.IsWindowVisible(WindowNames.Spellbook);
@ -2526,6 +2583,118 @@ public sealed class RetailUiRuntime : IDisposable
Console.WriteLine("[D.6] retail jump bar from gmFloatyPowerBarUI LayoutDesc 0x21000072.");
}
/// <summary>
/// Campaign FA slice FA3: retail's four-tab social panel — Friends /
/// Allegiance / Fellowship / Squelch, host <c>0x2100006E</c> slot
/// <c>0x1000018F</c>, <see cref="RetailPanelCatalog.SocialPanel"/> (12).
/// Same five-step shape as <see cref="MountOptionsPanel"/>: Import →
/// build callbacks → <see cref="Layout.SocialPanelController.Bind"/> →
/// <see cref="Layout.SocialPanelController.ActivateTabs"/> →
/// <see cref="RetailWindowFrame.Mount"/> → <see cref="RetailPanelUiController.RegisterMainPanel"/>.
/// </summary>
private void MountSocialPanel()
{
ElementInfo? rootInfo;
ImportedLayout? layout;
lock (_bindings.Assets.DatLock)
{
rootInfo = LayoutImporter.ImportInfos(
_bindings.Assets.Dats,
Layout.SocialPanelController.HostLayoutId,
Layout.SocialPanelController.SlotElementId);
var resolver = new DatStringResolver(_bindings.Assets.Dats);
layout = rootInfo is null
? null
: LayoutImporter.Build(
rootInfo,
_bindings.Assets.ResolveSprite,
_bindings.Assets.DefaultFont,
_bindings.Assets.ResolveFont,
resolver.Resolve);
}
if (rootInfo is null || layout is null)
{
Console.WriteLine("[UI] social panel: LayoutDesc 0x2100006E slot 0x1000018F not found.");
return;
}
// The Friends/Squelch row templates (0x2100005D/0x21000060) are
// resolved lazily, per row — the SAME "second dat-lock scope per
// resolve" shape the Character/Chat/Config tab row templates use
// (MountOptionsPanel, above), since each row's own template is a
// live DAT read.
UiElement? TemplateResolver(uint templateLayoutId, uint templateElementId)
{
lock (_bindings.Assets.DatLock)
{
var strings = new DatStringResolver(_bindings.Assets.Dats);
ElementInfo? info = LayoutImporter.ImportInfos(
_bindings.Assets.Dats, templateLayoutId, templateElementId);
return info is null
? null
: LayoutImporter.Build(
info,
_bindings.Assets.ResolveSprite,
_bindings.Assets.DefaultFont,
_bindings.Assets.ResolveFont,
strings.Resolve).Root;
}
}
var callbacks = new Layout.SocialPanelController.Callbacks(
Toggle: () => ToggleWindow(WindowNames.SocialPanel),
FellowshipSnapshot: _bindings.Social.FellowshipSnapshot,
AllegianceSnapshot: _bindings.Social.AllegianceSnapshot,
Friends: _bindings.Social.Friends,
Squelch: _bindings.Social.Squelch,
TemplateResolver: TemplateResolver);
Layout.SocialPanelController? controller =
Layout.SocialPanelController.Bind(layout, callbacks);
if (controller is null)
{
Console.WriteLine("[UI] social panel: required root did not build as UiTabPanel.");
return;
}
controller.ActivateTabs();
SocialPanelController = controller;
RetailWindowHandle handle = RetailWindowFrame.Mount(
Host.Root,
controller.Root,
_bindings.Assets.ResolveSprite,
new RetailWindowFrame.Options
{
WindowName = WindowNames.SocialPanel,
Chrome = RetailWindowChrome.NineSlice,
Left = 200f,
Top = 140f,
Visible = false,
// Same shared main-panel geometry policy every gmPanelUI
// sibling uses (Options' own MUST-FIX 2 correction, above).
ResizeX = false,
ResizeY = true,
ResizableEdges = ResizeEdges.Bottom,
ConstrainDragToParent = true,
ConstrainResizeToParent = true,
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top
| AnchorEdges.Right | AnchorEdges.Bottom,
ContentClickThrough = false,
DrawChromeCenter = !AuthorsFullPanelCenter(rootInfo),
Controller = controller,
});
_panelUi.RegisterMainPanel(
RetailPanelCatalog.SocialPanel,
WindowNames.SocialPanel,
handle,
rootInfo.TryGetEffectiveBool(
RetailPanelUiController.RestorePreviousPropertyId,
out bool restorePrevious)
&& restorePrevious);
Console.WriteLine("[UI] retail social panel from LayoutDesc importer (0x2100006E slot 0x1000018F).");
}
private void MountDialogFactory()
{
uint layoutId;