One user-ordered batch across the FA social panel + world selection.
Every root cause was probe-proven before the fix (new
ProbeSocialClickRouting in SocialPanelLiveMountProbeTests - production
window mount + real UiRoot hit-tests + a synthetic click):
1. STUCK CHECKBOXES (fellowship x4, allegiance x1, "always checked /
can't change any options"): the authored checkboxes carry DAT
ToggleBehavior, so UiButton SELF-FLIPS Selected at MouseUp - the old
handlers read the flipped value and wrote the ORIGINAL back, snapping
every click to where it started (the probe recorded (id, oldValue)).
Fix: SuppressSelfToggle (the CH6a/b mirror discipline) + derive the
next value from the STORE; the per-tick seeding mirrors it back.
2. UNCLICKABLE ROSTER ROWS ("only get the move window cursor"): the row
name text is display-text ClickThrough=true, which the hit-test walk
skips regardless of HandlesClick - the wired OnClick was unreachable.
Fix: UiText.OnClick assignment now clears ClickThrough (central,
documented); the stats text gains the same select handler so most of
the row's width selects the fellow.
3. TRUNCATED EMPTY-STATE ("You do not belong... To create MISSING"):
the authored string resolves COMPLETE (three sentences) but embedded
'\n's rendered as one clipped line. DatWidgetFactory now splits
authored strings into one Line per newline, with the provider still
re-reading DefaultColor live (the state-color contract - caught by
BuildText_AuthoredLineTracksStateFontColor).
4. FELLOW NAMES WHITE (user-directed): the AD-82 invented leader-gold +
selection-blue tints are deleted; names always white (register row
narrowed).
5. ALLEGIANCE HEADER LABELS: bare "0"/"0" -> "Followers: N" / "Rank: [N]"
(user-specified format; the full retail StringInfo composition stays
AD-85's gap), monarch block matching.
6. FRIENDS/SQUELCH LIVE (AD-79 mostly retired): Add friend (name box ->
0x0018, retail clears the box - Request_AddFriend @0x0048D240),
Remove (row-click selection -> 0x0017), Appear Offline (CharacterOption
0x27 via the immediate 0x0005 auto-save, ACE pushes FriendStatusChanged
to your friend-of list), Squelch Character/Account add-by-name
(0x0058 guid0/type AllChannels + 0x0059) and Remove for the selected
row. The wire beneath (builders, WorldSession sends, Runtime commands,
parsers) existed end-to-end since J4.1/FA1 - this is panel wiring only
(docs/research/2026-08-13-social-wire-completion.md, committed here).
Send Tell stays inert (not in the order; AD-79's remainder).
7. WORLD SELF-SELECTION ("clicking my own char should select myself"):
retail has NO self-exclusion (CPhysicsPart::Draw @0x0050D823 arms
every physobj; RecvNotice_SmartBoxObjectFound @0x004E5BAE selects
unconditionally) - the includeSelf gate was an unregistered
divergence, now removed on both the left-click and right-click paths.
Element roles were probe-measured, never guessed (Add 0x10000514 /
Remove 0x10000515 / Send Tell 0x10000516 / Appear Offline 0x1000052C /
name field 0x1000051B; Squelch: field 0x10000540, Remove 0x10000547,
Squelch Character 0x1000054B, Squelch Account 0x1000054C).
Register: AD-79 mostly retired, AD-82 narrowed. Known remainder, filed
not hidden: the fellowship page's authored 600px content vs the 362px
viewport leaves Dismiss/Assign-Leader below the fold until the window is
resized taller (probe-measured; candidate follow-up).
Tests: Checkbox_Click fact rewritten to the mirror contract (both
directions), monarch-followers label updated, includeSelf expectation
updated, probe extended (click routing, synthetic click, action-widget
role dump). App suite 4,976/3 skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The FA4 fix round's MUST-FIX 3 placed the 0x00A6 reconnect re-arm at the
wrong lifecycle point (re-review 8bbceff5): ResetSessionTransientUi runs
via the SessionDialogs reset stage BEFORE _inWorld=true, so SetPanelOpen
(world-gated, Validate requireWorld:true) returned Inactive and published
nothing — yet _pageVisible was latched true anyway, so no later hook
re-declared and fellow vitals stayed frozen for the whole new session.
The unit test passed only because the fake recorded unconditionally.
Two-part fix, both retail-faithful mechanisms not suppressions:
- SocialFellowshipPageController.SetPageVisible advances the edge-trigger
latch ONLY when the declaration is Accepted (published), so a dropped
pre-world send leaves the latch clear and a later attempt retries.
- ResetSessionDeclaration (pre-world) now ONLY clears the latch; the new
RedeclareAfterWorldEntry fires from the LiveSession EnteredWorld seam
(wired via RestoreLayout, idempotent if a persisted layout already
re-showed the page) so a still-open Fellowship page re-declares 0x00A6
in world and vitals resume.
Regression pins that actually catch it (the prior test could not):
- SetPageVisible_DoesNotLatch_WhenDeclarationDropped_SoItRetriesInWorld
(widget-level root, world-gated fake);
- Reconnect_ReDeclares0x00A6_AfterWorldEntry_NotDuringPreWorldReset +
Reconnect_StaysSilent_WhenFellowshipPageIsNotActuallyOpen (panel-level,
world-gated). RED-verified: reintroducing the pre-world declaration
fails the reconnect test.
Full Release suite: 13,286 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MUST-FIX 1 -- D5's percentage conversion rounds where retail truncates.
gmFellowshipUI::UpdateFellowStats @0x0048ECC9 forms pct*100.0f on the x87
stack then calls _ftol2 (MSVC's round-to-truncate helper), never
MathF.Round. The stored floats for 6 and 8 fellows are 0.44999998807907104
and 0.3499999940395355 (byte-read from the PDB-paired binary), so retail's
own products truncate to 44/34, not 45/35 -- and (int)(pct*100f) alone does
not fix it, since 0.45f*100f already rounds UP to exactly 45.0f in single
precision. Fixed as (int)((double)pct * 100.0), forming the product the
same wider-than-single-precision way retail's x87 does. Pinned with new
[InlineData] cases for both sizes.
MUST-FIX 4 -- gmFellowshipUI::UpdateFellowSelection @0x0048F0F0 (the
world->panel arm of retail's two-directional selection coupling) was never
ported; only the panel->world arm (SelectFellow) shipped. Selecting a
fellow in the WORLD left Dismiss/Assign-Leader disabled and showed no row
highlight. SyncSelectionFromWorld/SetSelectedFellow reproduce the
observable contract (button-enable + a row tint) against this
controller's own guid-keyed row dictionary instead of porting retail's
generic ListBox SetAttribute_InstanceID/SetSelectedItem primitive (scoped
disposition recorded at register row AD-82).
Also in this pass over the controller:
- SF-1: cache the fellowship-name LinesProvider; only reassign on an
actual name change (was allocating once per Tick, even while hidden).
- SF-2/SF-3: track true membership in _memberGuids, independent of which
rows finished building. Fixes an unbounded DAT-locked rebuild retry
when a row template permanently fails to build, and fixes Recruit's
"already a fellow" check reading render rows instead of membership.
- N-0: the Open/Close caption now flips optimistically on click, matching
retail's pre-toggle-before-server-echo (lane B feature 11).
- N-1/N-2/N-3: doc-only notes on the meter-child-text gap, the max>0
guard, and Tick's two-read non-atomicity.
- ResetPageVisibleLatch: the fellowship-controller half of MUST-FIX 3
(see the SocialPanelController commit for the panel-level half).
Per docs/research/2026-08-12-fa4-review-mechanism.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>