feat(ui): FA4 -- fellowship page fully live

Roster: SocialFellowshipPageController now builds one row per fellow
from the authored template (0x21000030/0x10000281, live-DAT verified),
diffing the member GUID set on each revision-gated Tick -- an unchanged
set updates every row's bound widgets in place (no ListBox mutation, so
scroll position is untouched by construction); only a real join/leave/
disband triggers a rebuild, via UiTemplateListBox.FlushPreservingScroll
(FA3 carry-forward 1, both the widget-level fix and the controller-level
diff). Health/stamina/mana meters bind Fill+Label; the leader's name
tints gold (lane A's row template has no dedicated leader marker, so
this is a flagged adaptation, not a ported mechanism). Row-click
selection (SelectFellow) drives Dismiss/Leader targeting and the world
selection (SelectionChangeSource.Social).

D4: SocialPanelController now tracks "is the social window shown AND is
Fellowship the active tab" via UiTabPanel.ActivePageChanged +
OnShown/OnHidden, and calls SetPageVisible on every transition, which
sends 0x00A6 (idempotent, no-op while disconnected) -- the prerequisite
ACE gates its 0x02C0 vitals stream on.

Create flow: the inline name field (0x1000026F, an authored Editable
UiField -- live-DAT verified) gates the Create button's enabled state
exactly like retail (empty name = disabled = the whole refusal
mechanism, no separate error text); FellowshipShareXP's live value is
read at click time.

Actions + confirmations: Recruit/Dismiss/Quit/Disband/AssignLeader/
SetOpen all route through DeferredGameRuntimeStateCommands (new
Fellowship* methods) rather than a raw WorldSession send, so Quit
correctly picks up RuntimeFellowshipState's leader hand-off rule.
Button enable states port gmFellowshipUI::UpdateButtons verbatim. The
Open/Close button's caption swaps between the two DAT-resolved strings
cached once at Bind (never per-tick -- DatCollection is not safe to
touch unprotected from the render loop). RetailUiRuntime intercepts a
type-4 confirmation request before it reaches the generic
GameplayConfirmationController: IgnoreFellowshipRequests auto-declines,
FellowshipAutoAcceptRequests auto-accepts, neither set falls through to
the existing dialog machinery unchanged (D6).

D5 display: the per-fellow stats line uses retail's byte-decoded
even-split percentage table verbatim (1.0/.../.3111111/.28, default
0.0); the proportional branch omits the percentage rather than
inventing a formula (no acdream ExperienceToRaiseLevel table exists
yet). Both StringInfo variable substitution (row/stats/vitals text) and
ACCharGenData::FormatName (create-flow name canonicalization) are
unported prerequisites, so row text renders as plain numeric composites
-- register rows AD-80/AD-81 (docs commit).

D7: un-dims IgnoreFellowshipRequests/FellowshipAutoAcceptRequests
(consumed by the D6 auto-decline/accept) and FellowshipShareXP/
FellowshipShareLoot (consumed by Create + the page's own second
checkbox surface) on the Character tab -- 4 of 35 store-only rows
promoted to Live (31 remain dimmed).

Carry-forwards from the FA3 re-review, folded into this slice's
contract:
- UiTemplateListBox.FlushPreservingScroll -- preserves scroll offset
  across a rebuild instead of resetting to 0 (Flush's existing
  contract, unchanged, for Friends/Squelch).
- RowTemplateResolver -- the FA3 caching row-template resolver
  extracted from a MountSocialPanel local function into its own
  hermetically-testable class; now shared by Friends/Squelch/
  Fellowship's row families.
- Friends/Squelch scrollbars now resolve via the built
  UiTemplateListBox.ScrollbarElementId (DAT property 0x72) instead of
  a hardcoded literal, matching ConfigOptionsPageController's own OP6
  precedent.
- The Fellowship roster path never advances its revision latch on a
  partial resolver failure until the NEXT real membership change --
  never a per-frame retry loop.

Live-DAT verified (ACDREAM_PROBE_LIVE_MOUNT=1, extended
SocialPanelLiveMountProbeTests): the name field builds as UiField, all
11 buttons/checkboxes resolve, the row template's 5 checked fields
resolve to the right widget types, every checkbox label/tooltip and the
Open/Close captions resolve to real retail strings ("Open"/"Close"),
and a full production-path Bind() against live DATs produces zero
"not found" warnings.

App tests: +30 (7 UiTemplateListBox/RowTemplateResolver unit tests, 23
SocialFellowshipPageControllerTests covering roster diff/rebuild,
button enable rules, checkbox wiring, create-flow gating, D4
idempotency, and D5 formatting) plus 2 CharacterOptionsPageController
counts updated for the D7 un-dim (35->31 dimmed, 15->19 live).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-12 04:40:53 +02:00
parent 357d203202
commit 5bdd0528f1
16 changed files with 1941 additions and 114 deletions

View file

@ -111,7 +111,7 @@ public static class CharacterOptionsPageController
/// steps 13-18, written after the code landed):
///
/// <list type="bullet">
/// <item><description><b>NOT dimmed (15 rows, real acdream-side
/// <item><description><b>NOT dimmed (19 rows, real acdream-side
/// consumer):</b> Group B rows actually bound —
/// <c>ViewCombatTarget</c>/<c>AutoTarget</c>/<c>AutoRepeatAttack</c>
/// (combat: <c>LiveCombatAttackOperations.cs</c>,
@ -122,10 +122,17 @@ public static class CharacterOptionsPageController
/// <c>DisableDistanceFog</c>/<c>DisplayTimeStamps</c>/<c>ToggleRun</c>
/// (Group B, polled at <c>GameWindow.cs:655-680</c>),
/// <c>DragItemOnPlayerOpensSecureTrade</c> (TS-48,
/// <c>InteractionRetainedUiComposition.cs:326</c>), and all six
/// <c>InteractionRetainedUiComposition.cs:326</c>), all six
/// <c>ListenTo*Chat</c> ids (<c>TurbineChatMembershipGate.cs:105-136</c>
/// gates every Turbine room join on the matching bit).</description></item>
/// <item><description><b>Dimmed (35 rows, store-only):</b> every
/// gates every Turbine room join on the matching bit), and — landed at
/// Campaign FA slice FA4, D7 — <c>IgnoreFellowshipRequests</c>/
/// <c>FellowshipAutoAcceptRequests</c> (consumed by
/// <c>RetailUiRuntime.TryAutoRespondToFellowshipInvite</c>'s auto-
/// decline/auto-accept, D6) and <c>FellowshipShareXP</c>/
/// <c>FellowshipShareLoot</c> (consumed by the fellowship page's own
/// Create flow / D5 display / second checkbox surface,
/// <c>SocialFellowshipPageController</c>).</description></item>
/// <item><description><b>Dimmed (31 rows, store-only):</b> every
/// remaining Group A row (wire+store only — ACE, not acdream, is the
/// consumer) and every remaining Group B row the OP4 gate script's own
/// step 16 lists as "no consumer surface" (<c>ShowTooltips</c>,
@ -134,8 +141,10 @@ public static class CharacterOptionsPageController
/// <c>DisableMostWeatherEffects</c>, <c>PersistentAtDay</c>,
/// <c>FilterLanguage</c>, <c>MainPackPreferred</c>), plus every Group D
/// deferral (<c>SalvageMultiple</c>,
/// <c>DisableHouseRestrictionEffects</c>, <c>FellowshipShareXP</c>,
/// <c>HearPkDeathMessages</c>).</description></item>
/// <c>DisableHouseRestrictionEffects</c>,
/// <c>HearPkDeathMessages</c>) — <c>FellowshipShareXP</c> left Group D
/// at FA4 (above), it stays a bullet-list resident here only for the
/// historical count.</description></item>
/// <item><description><b>Flagged ambiguity, resolved by code evidence
/// (see final report, not re-litigated here):</b> the research doc's
/// §7.1 lists <c>AcceptLootPermits</c> in BOTH Group A's bullet list AND
@ -145,13 +154,14 @@ public static class CharacterOptionsPageController
/// not a behavioral reader — ACE alone decides whether to honor corpse
/// looting (<c>Player_Death.cs:755</c>). Classified Group A / dimmed.
/// <c>IgnoreFellowshipRequests</c>/<c>FellowshipAutoAcceptRequests</c>
/// have a genuine coded mutual-exclusion side effect
/// had a genuine coded mutual-exclusion side effect
/// (<c>RuntimeCharacterState.TrySetOption</c>, MF-2) that the research
/// doc's Group A classification does not capture (the doc predates
/// OP1's review-fix round) — kept dimmed per the doc's explicit Group A
/// listing and this task's instruction to treat the doc as the sole
/// authority rather than infer from code comments; a future un-dim is a
/// one-line judgment call, not a re-investigation.</description></item>
/// doc's Group A classification did not capture, kept dimmed at OP1
/// pending "a future un-dim is a one-line judgment call, not a
/// re-investigation" — that future consumer landed at Campaign FA slice
/// FA4 (see D7's addendum to register row AD-78), so both rows moved to
/// the NOT-dimmed bullet above, together with the two
/// <c>FellowshipShare*</c> rows FA4 also gave real consumers.</description></item>
/// </list>
/// </summary>
private const bool Live = false;
@ -192,12 +202,12 @@ public static class CharacterOptionsPageController
}),
new("ID_CharacterOption_Grouping_Section", new RowSpec[]
{
new(CharacterOptionId.IgnoreAllegianceRequests, "IgnoreAllegianceRequests", StoreOnly), // Group A
new(CharacterOptionId.IgnoreFellowshipRequests, "IgnoreFellowshipRequests", StoreOnly), // Group A (see ambiguity note)
new(CharacterOptionId.DisplayAllegianceLogonNotifications, "DisplayAllegianceLogonNotifications", StoreOnly), // Group A
new(CharacterOptionId.FellowshipShareXP, "FellowshipShareXP", StoreOnly), // Group D
new(CharacterOptionId.FellowshipShareLoot, "FellowshipShareLoot", StoreOnly), // Group A
new(CharacterOptionId.FellowshipAutoAcceptRequests, "FellowshipAutoAcceptRequests", StoreOnly), // Group A (see ambiguity note)
new(CharacterOptionId.IgnoreAllegianceRequests, "IgnoreAllegianceRequests", StoreOnly), // Group A — stays dimmed, D7/AD-78 addendum
new(CharacterOptionId.IgnoreFellowshipRequests, "IgnoreFellowshipRequests", Live), // D7/D6 (Campaign FA slice FA4): consumed by the fellowship-invite auto-decline (RetailUiRuntime.TryAutoRespondToFellowshipInvite)
new(CharacterOptionId.DisplayAllegianceLogonNotifications, "DisplayAllegianceLogonNotifications", StoreOnly), // Group A — stays dimmed, D7/AD-78 addendum
new(CharacterOptionId.FellowshipShareXP, "FellowshipShareXP", Live), // D7 (Campaign FA slice FA4): consumed by the fellowship Create flow (its value IS the sent shareXP bit) and the panel's D5 XP-share display
new(CharacterOptionId.FellowshipShareLoot, "FellowshipShareLoot", Live), // D7 (Campaign FA slice FA4): now also a SECOND live checkbox surface on the fellowship page itself (SocialFellowshipPageController)
new(CharacterOptionId.FellowshipAutoAcceptRequests, "FellowshipAutoAcceptRequests", Live), // D7/D6 (Campaign FA slice FA4): consumed by the fellowship-invite auto-accept (suppresses the confirmation dialog)
}),
new("ID_CharacterOption_OtherPlayers_Section", new RowSpec[]
{