From 38f08314c7ac17c1c92803a2890b68d8080d3b8e Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 12 Aug 2026 04:41:53 +0200 Subject: [PATCH] docs(fa4): register rows AD-80/AD-81, AD-78 addendum, gate script section, ledger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Register: AD-80 files the D5 XP-share display divergence between retail's byte-decoded table (acdream renders it verbatim) and the currently-targeted ACE server's slightly different actual grant (.3 vs .3111111 at 9 fellows, no 10-fellow row, wrong out-of-range default) -- an ACE-vs-retail gap, not an acdream-vs-retail one, filed because it is directly user-visible through this panel. AD-81 files the two unported retail text-composition primitives the fellowship page's mechanism needs (StringInfo variable substitution, ACCharGenData::FormatName) and what acdream renders instead (plain numeric composites, the raw typed name). AD-78's derivation table gains its D7 addendum: 4 of the 35 store-only rows (IgnoreFellowshipRequests/FellowshipAutoAcceptRequests/ FellowshipShareXP/FellowshipShareLoot) moved to the Live bullet with their new consumers named. Gate script: new §FA4 section covering create (name + shareXP), the open/close caption swap, button-enable rules, and the D5 display -- all solo-testable -- plus roster/recruit/dismiss/leader-handoff/invite- dialog steps marked [TWO-CLIENT] with an honest note that they defer to FA6's bot-vs-ACE gate if a second account isn't available for this connected gate. Corrects FA3's now-stale "these six buttons/four checkboxes are INERT" claims in steps 11-12 to point at the new section instead of leaving a wrong claim in place. Ledger: FA4 row CODE-COMPLETE with both commit SHAs, the reconciled 13,238->13,272 (+34) test-count arithmetic, the live-DAT verification summary (ACDREAM_PROBE_LIVE_MOUNT=1 against real installed DATs, including the structural finding that retail's own frame-visibility swap already gates the Create-flow controls away from the roster view with no extra code needed), and the four scoped deferrals/simplifications this slice made (the StringInfo/FormatName gap, the proportional-share omission, the Recruit button's superset-of-retail enable rule). Co-Authored-By: Claude Fable 5 --- .../retail-divergence-register.md | 4 +- ...26-08-11-fellowship-allegiance-campaign.md | 9 +- .../2026-08-12-campaign-fa-test-script.md | 227 ++++++++++++++++-- 3 files changed, 221 insertions(+), 19 deletions(-) diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index ea583539..0ee66d32 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -62,7 +62,7 @@ accepted-divergence entries (#96, #49, #50). --- -## 2. Adaptation (AD) — 59 active rows (AD-79 filed 2026-08-12 at Campaign FA slice FA3, D1 — the social panel's Friends/Squelch page action buttons (add/remove friend, appear offline, squelch add/remove/clear) are honest INERT, no wire implemented this campaign; AD-78 filed 2026-08-11 at Campaign OP's gate-2 follow-up (user-directed, verbatim "mark all options that are not implemented now, so I can clearly see what is not implemented") — the shared store-only-caption-dimming convention across the Character/Config option tabs and Configure Keyboard; AD-77 filed 2026-08-11 at the Campaign OP OP3 review-fix round — the client-wide floating-only `gmPanelUI` host divergence (retail also exposes a docked `0x21000017` host) the plan's §5 delegated to the OP3 dual review, scoped to every main panel not just Options; AD-76/AD-75/AD-74 filed 2026-08-11 at Campaign OP slice OP3 — the Options panel's Exit to Character Selection "behaves as Exit Game" adaptation (D6), the Urgent Assistance/Report Abuse dead-URL interface-text short-circuit (D5), and In-Game Help Files' asset-missing inert button (D5); AD-73 filed 2026-08-11 at the Campaign OP OP2 rework — `UiTabPanel`'s dormant-until-`ActivateTabBehavior()` activation model, replacing retail's unconditional per-instance tab-table wiring, so the four already-shipped Type-8 hosts keep their existing controller-owned switching without a double-driver race; AD-72 filed 2026-08-08 at the Slice 5.3 review corrections — `VendorPricing`'s double-precision narrowing versus retail's x87 extended precision, same class as AD-33; AD-65 RETIRED and AD-69 FILED 2026-08-07 at Campaign S S4 — the away-arm now snaps per retail @0x00509c50, while AD-66's byte-confirmed sibling landing is WITHHELD pending #341's measurement-anomaly apparatus, and AD-69 records the seam-frame dist gap the same pass discovered; AD-56 RESTORED 2026-08-07 — the a8a7d64b revert had collaterally DELETED it, the inverse of the AD-55 zombie it also created; its plumb-fall-freeze condition is live again since TS-4’s real retirement at Slice 2B; AD-55 RE-RETIRED 2026-08-07 — its 2026-07-30 retirement at 252e8068 was collaterally resurrected by the a8a7d64b revert of the unrelated TS-4 commit; the code kept the cos(10°) fix throughout; AD-68 filed 2026-08-07 at the #338 closure — the async-residency placeholder mover shape (0.4/0.4 steps + capsule) has no retail counterpart because retail loads synchronously; AD-67 filed 2026-08-07 at the #32 closeout — the narrowed `SetContactPlane` keeps its per-write `ContactPlaneCellId`, which retail writes only at `init_contact_plane`; AD-49 filed 2026-08-06 at the #334 fix — the BSP part-array flood runs its outdoor cell rectangle at seed time rather than only from retail’s residency-gated walk, keeping both registration floods on one residency rule; AD-64 filed 2026-08-05 at the C5b architecture review's D1 fix — AD-60's W2 wire-cell REACHABILITY decision is expressed once per host because the two hosts run parallel non-shared inbound routes; the committed VALUE is single-sourced at `RuntimeEntityObjectLifetime.CommitWireCellRebucket`, and unification is filed as #324; AD-60 CORRECTED the same day — its surviving-channel enumeration presented "the local force path, the missile arm" as exhaustive when the entire no-window host belonged in it; AD-1 RETIRED 2026-08-05, C5a deletion sweep — the legacy outdoor demote/restore lift this row described was `PhysicsEngine.Resolve`'s own body, deleted with zero production callers; AD-42 DELETED 2026-08-04, C4 route 3 — its last surviving citation, the headless portal-arrival resync's two-call Resolve/ResolvePlacement split, was retired by the canonical `RuntimeAcceptedPositionDriveController` portal arm; AD-2 amended same route with the deferred-place timing adaptation, the T8 tolerated-overwrite note, and the leash-anchor nuance; AD-63 filed 2026-08-04, cancelled-park presentation rollback — the rollback restores every presentation registration the park's Withdraw removed EXCEPT the player's selection, which is user intent rather than a projection; AD-62 filed 2026-08-03, C4 route 2 round 2 — a deferred ForcePosition retired without committing is not re-applied and its ack is not sent; AD-61 filed 2026-08-02, C3c review round 1 — the #270 settle compression now covers the local player; AD-59/AD-60 filed 2026-08-02, continuation-executor slice) +## 2. Adaptation (AD) — 61 active rows (AD-81 filed 2026-08-12 at Campaign FA slice FA4 — the fellowship roster/create-flow text-composition gap (unported `StringInfo` variable substitution + `ACCharGenData::FormatName`); AD-80 filed 2026-08-12 at Campaign FA slice FA4, D5 — the panel's retail-exact XP-share percentage display versus the currently-targeted ACE server's slightly different actual grant; AD-79 filed 2026-08-12 at Campaign FA slice FA3, D1 — the social panel's Friends/Squelch page action buttons (add/remove friend, appear offline, squelch add/remove/clear) are honest INERT, no wire implemented this campaign; AD-78 filed 2026-08-11 at Campaign OP's gate-2 follow-up (user-directed, verbatim "mark all options that are not implemented now, so I can clearly see what is not implemented") — the shared store-only-caption-dimming convention across the Character/Config option tabs and Configure Keyboard; AD-77 filed 2026-08-11 at the Campaign OP OP3 review-fix round — the client-wide floating-only `gmPanelUI` host divergence (retail also exposes a docked `0x21000017` host) the plan's §5 delegated to the OP3 dual review, scoped to every main panel not just Options; AD-76/AD-75/AD-74 filed 2026-08-11 at Campaign OP slice OP3 — the Options panel's Exit to Character Selection "behaves as Exit Game" adaptation (D6), the Urgent Assistance/Report Abuse dead-URL interface-text short-circuit (D5), and In-Game Help Files' asset-missing inert button (D5); AD-73 filed 2026-08-11 at the Campaign OP OP2 rework — `UiTabPanel`'s dormant-until-`ActivateTabBehavior()` activation model, replacing retail's unconditional per-instance tab-table wiring, so the four already-shipped Type-8 hosts keep their existing controller-owned switching without a double-driver race; AD-72 filed 2026-08-08 at the Slice 5.3 review corrections — `VendorPricing`'s double-precision narrowing versus retail's x87 extended precision, same class as AD-33; AD-65 RETIRED and AD-69 FILED 2026-08-07 at Campaign S S4 — the away-arm now snaps per retail @0x00509c50, while AD-66's byte-confirmed sibling landing is WITHHELD pending #341's measurement-anomaly apparatus, and AD-69 records the seam-frame dist gap the same pass discovered; AD-56 RESTORED 2026-08-07 — the a8a7d64b revert had collaterally DELETED it, the inverse of the AD-55 zombie it also created; its plumb-fall-freeze condition is live again since TS-4’s real retirement at Slice 2B; AD-55 RE-RETIRED 2026-08-07 — its 2026-07-30 retirement at 252e8068 was collaterally resurrected by the a8a7d64b revert of the unrelated TS-4 commit; the code kept the cos(10°) fix throughout; AD-68 filed 2026-08-07 at the #338 closure — the async-residency placeholder mover shape (0.4/0.4 steps + capsule) has no retail counterpart because retail loads synchronously; AD-67 filed 2026-08-07 at the #32 closeout — the narrowed `SetContactPlane` keeps its per-write `ContactPlaneCellId`, which retail writes only at `init_contact_plane`; AD-49 filed 2026-08-06 at the #334 fix — the BSP part-array flood runs its outdoor cell rectangle at seed time rather than only from retail’s residency-gated walk, keeping both registration floods on one residency rule; AD-64 filed 2026-08-05 at the C5b architecture review's D1 fix — AD-60's W2 wire-cell REACHABILITY decision is expressed once per host because the two hosts run parallel non-shared inbound routes; the committed VALUE is single-sourced at `RuntimeEntityObjectLifetime.CommitWireCellRebucket`, and unification is filed as #324; AD-60 CORRECTED the same day — its surviving-channel enumeration presented "the local force path, the missile arm" as exhaustive when the entire no-window host belonged in it; AD-1 RETIRED 2026-08-05, C5a deletion sweep — the legacy outdoor demote/restore lift this row described was `PhysicsEngine.Resolve`'s own body, deleted with zero production callers; AD-42 DELETED 2026-08-04, C4 route 3 — its last surviving citation, the headless portal-arrival resync's two-call Resolve/ResolvePlacement split, was retired by the canonical `RuntimeAcceptedPositionDriveController` portal arm; AD-2 amended same route with the deferred-place timing adaptation, the T8 tolerated-overwrite note, and the leash-anchor nuance; AD-63 filed 2026-08-04, cancelled-park presentation rollback — the rollback restores every presentation registration the park's Withdraw removed EXCEPT the player's selection, which is user intent rather than a projection; AD-62 filed 2026-08-03, C4 route 2 round 2 — a deferred ForcePosition retired without committing is not re-applied and its ack is not sent; AD-61 filed 2026-08-02, C3c review round 1 — the #270 settle compression now covers the local player; AD-59/AD-60 filed 2026-08-02, continuation-executor slice) Recent retirements: AD-3/AD-4 retired 2026-07-31 by exact active/per-candidate visible-cell availability, full-catalog containment-root validation, and the @@ -174,6 +174,8 @@ readiness/requeue adaptation. See | AD-77 | **Filed 2026-08-11 at the Campaign OP OP3 review-fix round (dual-review S4/MUST-FIX 2 — the plan's §5 "out of scope" list explicitly delegated this ruling to the OP3 review).** Retail exposes TWO `gmPanelUI` host variants for the same panel stack — a floating host (`0x2100006E`, `gmFloatyPanelUI`) and a docked host (`0x21000017`) — so a retail user can dock the Options panel (and every other `gmPanelUI` sibling) into a fixed screen position instead of leaving it freely floating. acdream mounts every main panel through `RetailWindowFrame.Mount` + `RetailPanelUiController.RegisterMainPanel` against the floating host ONLY; no code path resolves or mounts `0x21000017` at all. | `src/AcDream.App/UI/RetailUiRuntime.cs` (every `Mount*`/`RegisterMainPanel` call site for a `gmPanelUI` sibling — Character/Inventory/Spellbook/Effects/the four indicator-detail panels/Options); `src/AcDream.App/UI/Layout/RetailWindowFrame.cs` | This predates OP3 — every `gmPanelUI` sibling has shipped floating-only since its own slice landed; OP3 did not introduce the gap, it just added a tenth panel to an already-floating-only cohort. The plan explicitly scoped filing the row to "whichever slice's review deems it a divergence" rather than blocking any one panel's slice on building a docked-host variant no prior panel has either. | A user who expects to dock the Options panel (or any other main panel) the way retail allows cannot — every `gmPanelUI` sibling is floating-only in acdream, client-wide, not an Options-specific gap. | research doc `2026-08-10-options-panel-structure.md` §10.1 (docked/floating host pair); `docs/plans/2026-08-10-options-panel-campaign.md` §5 | | AD-78 | **Filed 2026-08-11, user-directed (verbatim: "mark all options that are not implemented now, so I can clearly see what is not implemented"), gate 2 of Campaign OP's follow-up.** Retail dims nothing on any Options-panel row or Configure-Keyboard action row — every retail row drives its own real consumer by construction, so retail has no "does this actually do anything" ambiguity to signal. acdream, by contrast, ships a large honest store-only set (AP-198/AP-199/AP-200/AP-203, TS-73/TS-74/TS-75/TS-76/TS-77/TS-78/TS-79/TS-80, and the Character-tab Group A/D rows) that persist and, where auto-save, send the wire bit, but drive nothing observable client-side. Per explicit user direction, every such row's CAPTION now renders in a shared neutral grey (`UiRenderContext.StoreOnlyCaptionColor`, `(0.5,0.5,0.5,1)` — the SAME value the existing disabled/ghosted convention already used, `UiMenu.TextColorGhosted`) instead of its normal white/DAT-authored color, while the row itself stays fully interactive (click/drag/persist exactly as before — only the caption's paint color changes). No invented marker text is added anywhere (the project's "no user-visible strings outside the DAT" rule stands); the dim IS the marker. | `src/AcDream.App/UI/UiRenderContext.cs` (`StoreOnlyCaptionColor`, the one shared constant); `src/AcDream.App/UI/Layout/ConfigOptionsPageController.cs` (21 of 27 rows dimmed — `ApplyLabelAndTooltip`/`SetLabelText`'s `storeOnly` parameter, threaded from each `BindXxxSection` call site); `src/AcDream.App/UI/Layout/CharacterOptionsPageController.cs` (35 of 50 rows dimmed — `RowSpec.StoreOnly`, derived per-row in the class doc's table, cross-checked against actual shipped consumers rather than the research doc alone); `src/AcDream.App/UI/Layout/KeyboardConfigController.cs` (`BuildActionRow` dims a row when `RetailActionIdentityTable.TryResolve` fails, i.e. `MappedAction` is null — AP-203's set); `src/AcDream.App/UI/Layout/ChatOptionsPageController.cs` (audited, zero dimmed rows — every row already has a live consumer). | Explicit, unambiguous user direction (this session, gate 2) overriding the earlier per-slice register rows' silence on presentation; the four controllers' own conformance tests (`ConfigOptionsPageControllerTests.CaptionDimming_MatchesTheStoreOnlySetExactly`, `CharacterOptionsPageControllerTests.StoreOnlyRows_MatchTheDerivationTableExactly` + `Bind_AppliesDimmedCaptionColor_ForStoreOnlyRows_AndWhiteForLiveRows`, `KeyboardConfigControllerTests.UnmappedRows_DimTheirCaption_MappedRowsStayWhite`) pin the exact dimmed set so a future consumer landing without also flipping its row's literal fails the build, not just the eye. | A reviewer comparing a byte-exact retail screenshot to acdream will see caption colors retail never has — this row exists precisely so that divergence is understood as intentional, not a bug. If a row's dim/live classification in the four cited tables ever drifts from its ACTUAL consumer state (a landed consumer whose row was never un-dimmed, or a regressed consumer whose row was never re-dimmed), the caption becomes misleading in the OPPOSITE direction it was built to prevent — treat any report of "this dimmed row visibly does something" or "this live-looking row does nothing" as a real defect, not a rendering nit (see the gate script's own note). This row retires only when acdream reaches full retail parity (zero store-only rows remaining), at which point the convention itself — not just its content — should be deleted. | None (acdream-only divergence; retail has no store-only rows to compare against) — `docs/research/2026-08-10-character-options-map.md` §7.1 (Group A/B/C/D split); `docs/research/2026-08-11-campaign-op-test-script.md` (per-tab store-only enumerations this row's dimmed set matches) | | AD-79 | **Filed 2026-08-12 at Campaign FA slice FA3, D1 (the plan's "Friends + Squelch pages bind READ-ONLY... their mutation actions are wired only if their wire is already served by ACE and trivially pinnable in-slice — otherwise the action buttons are honest INERT" decision).** The social panel's Friends page authors three buttons (Add/Remove Friend-shaped, `0x10000514`/`0x10000515`/`0x10000516`) plus an "Appear Offline"-shaped checkbox (`0x1000052C`); the Squelch page authors three buttons (`0x10000547`/`0x1000054B`/`0x1000054C`). All seven are built, laid out, and clickable exactly as authored, but carry no click handler — no Friends add/remove/appear-offline wire and no Squelch add/remove/clear wire is implemented this campaign. `gmFriendsUI`/`gmSquelchUI` were also outside lane A/B/C/D's own decompiled scope (only Fellowship/Allegiance were researched), so their real button semantics and wire opcodes are not yet established either — this row covers BOTH "not wired" and "not yet researched." | `src/AcDream.App/UI/Layout/SocialFriendsPageController.cs`; `src/AcDream.App/UI/Layout/SocialSquelchPageController.cs` (both classes' own doc comments cite this row) | FA3 is the panel SHELL slice; D1 sets the bar for which Friends/Squelch actions get wired in-slice at "trivially pinnable," which none of these seven meet without their own wire research. `SocialPanelControllerTests.FriendsAndSquelchActionButtons_AreClickable_ButHaveNoHandler` pins the INERT contract so a future consumer landing without also removing this row's citation fails nothing silently — the row is the only signal until a follow-up slice wires real handlers. | A user clicking Add/Remove Friend, Appear Offline, or any Squelch button in acdream sees no effect and no feedback — indistinguishable from a dead control unless they already expect the gap. The Friends/Squelch LISTS themselves are live (bound read-only to `RuntimeCommunicationState.Friends`/`.Squelch`) — only the mutation controls are inert. | None (no retail decomp anchor — `gmFriendsUI`/`gmSquelchUI` are outside this campaign's researched scope); `docs/research/2026-08-11-fa-panel-structure.md` §10 (coordinator addendum, the panel discovery that first surfaced these two pages); `docs/plans/2026-08-11-fellowship-allegiance-campaign.md` D1 | +| AD-80 | **Filed 2026-08-12 at Campaign FA slice FA4, D5.** The fellowship page's per-fellow percentage text renders retail's own byte-decoded XP-share table verbatim (1.0/.75/.6/.55/.5/.45/.4/.35/.3111111/.28, default 0.0 — `docs/research/2026-08-11-fa-fellowship-wire.md` §7.2, byte-decoded from the PDB-paired binary because both available decompilers folded the function to a constant). The currently-targeted ACE server computes the ACTUAL distributed XP from a DIFFERENT table (`.3` at 9 fellows instead of `.3111111`, no explicit 10-fellow row, and a wrong out-of-range default of `1.0` instead of `0.0` — `Fellowship.cs:604-632`, lane B §4.3). So a full (9-member) or over-full-in-retail's-table (10-member) fellowship's displayed percentage will not exactly match the XP ACE actually grants. This is a divergence between ACE and RETAIL, not between acdream and retail — acdream's client-side display is retail-faithful — but it is filed here because it is directly user-visible through this panel and a tester comparing "panel says 31.1%" against "server granted 30%" is measuring ACE's bug, not acdream's port. | `src/AcDream.App/UI/Layout/SocialFellowshipPageController.cs` (`EvenSplitPercentTable`, `FormatStatsText`) | The client-side table is byte-verified against the retail binary; re-deriving it to match ACE's (wrong) numbers would make acdream disagree with a REAL retail client observing the same fellowship, which is the opposite of this project's goal. | A tester with a 9- or 10-member fellowship on ACE sees a panel percentage that does not exactly match the XP bonus they actually receive; below 9 members the two agree exactly. The proportional (non-even-split) branch has a SEPARATE, narrower gap: acdream has not ported an `ExperienceToRaiseLevel`-equivalent table, so that branch omits the percentage entirely (level only) rather than computing a wrong number — see AD-81's citation of the same method. | `FellowshipSystem::GetEvenSplitXPPctg @0x005B9BA0` (lane B §7.2); ACE `Fellowship.cs:604-632`; `docs/research/2026-08-11-fa-fellowship-wire.md` §4.3 | +| AD-81 | **Filed 2026-08-12 at Campaign FA slice FA4.** Two retail text-composition primitives the fellowship page's mechanism needs are not ported, so this controller renders their CONTENT as plain numeric composites instead of retail's exact resolved sentence, never invented English: (1) **`StringInfo` variable substitution** — every row field beyond the bare name is a retail `StringInfo` template with embedded variables (`ID_Fellowship_FellowStats` + `ID_Level`/`ID_Experience`; the three `…Status` fields + `ID_Cur`/`ID_Max` — `docs/research/2026-08-11-fa-panel-structure.md` §3.1/§4.1), resolved at runtime through `StringInfo::InqString` → `StringTableMetaLanguage::UnescapeString`, a cross-cutting UI-string engine acdream has never ported (the SAME gap the pre-Campaign-OP Character window recorded, `docs/research/2026-06-25-character-window-faithful-spec.md`: "NOT yet ported — current controller uses canonical AC labels"); this controller instead renders `"{level} {pct}%"` and `"{cur}/{max}"` — the retail-authored NUMBERS, without retail's surrounding words. (2) **`ACCharGenData::FormatName`** — retail's Create flow canonicalizes the typed fellowship name and writes the formatted text back into the entry box before sending (lane B §2.2/§6.2); acdream sends the raw typed text verbatim. Neither gap affects the WIRE — the `0x00A2` builder's `str16L` field is unaffected either way; only the client-side PRESENTATION differs. | `src/AcDream.App/UI/Layout/SocialFellowshipPageController.cs` (`UpdateRow`, `FormatStatsText`, `SetVitals`, the create-button `OnClick`) | Porting `StringTableMetaLanguage` is a cross-cutting UI-string-engine prerequisite, not a fellowship-specific task, and guessing its token syntax without decoding `StringInfo::InqString` would risk silently-wrong substitution rather than an honestly-numeric fallback — exactly the guessing CLAUDE.md's workflow forbids. `FormatName`'s capitalization/character rules are a separate chargen algorithm with no fellowship-specific anchor read yet. | A user sees "12 31%" / "140/140" instead of retail's full sentence, and a typed fellowship name keeps whatever casing/spacing the player typed instead of retail's canonicalized form. The underlying DATA (level, percentage, cur/max, the name itself) is correct in every case — only the surrounding words/formatting are absent. | `StringInfo::InqString @0x0042e490` → `StringTableMetaLanguage::UnescapeString` (unresolved — not yet decoded); `gmFellowshipUI::CreateFellowship @0x0048F730` (the `ACCharGenData::FormatName` call, lane B §2.2); `docs/research/2026-06-25-character-window-faithful-spec.md` (the identical prior finding for the Character window) | --- diff --git a/docs/plans/2026-08-11-fellowship-allegiance-campaign.md b/docs/plans/2026-08-11-fellowship-allegiance-campaign.md index 6d7feef2..ab031f32 100644 --- a/docs/plans/2026-08-11-fellowship-allegiance-campaign.md +++ b/docs/plans/2026-08-11-fellowship-allegiance-campaign.md @@ -23,7 +23,12 @@ contract: the `Flush()` scroll-position reset under FA4's per-vitals-tick roster rebuilds; a production-resolver test for the new template cache; scrollbar-id literals promoted to the authored `ScrollbarElementId`; the bounded per-frame retry on a permanently -unresolvable template. FA4 in flight.** +unresolvable template. **FA4 CODE-COMPLETE 2026-08-12 (`357d2032` +Runtime, `5bdd0528` App+tests) — implementer pass done, all four FA3 +carry-forwards closed in this same slice; the dual-lens review (§7) and +the user's connected gate (`docs/research/2026-08-12-campaign-fa-test-script.md` +§FA4, several steps explicitly need a second account/character and are +marked `[TWO-CLIENT]`, deferrable to FA6's bot gate) are OWED.** **Goal:** retail's social panel — the four-tab `gmPanelUI` member at host slot `0x1000018F` (panel id **12**): **Friends / Allegiance / Fellowship / @@ -257,7 +262,7 @@ before anything builds on them. | FA1 | **CLOSED 2026-08-12** — narrow re-review verdict CLOSED, no reopen (`96df892d`, §7 of the mechanism findings doc: all 7 mechanism dispositions re-derived in the diffs, all 6 blast dispositions spot-verified, suite claim corroborated on post-fix binaries); re-review carry-forward CF-1 (two further stale `AllegianceTree` citations in the seam map's FA2 guidance, `:128`/`:214`) closed by coordinator addenda in the same commit as this ledger update — FA2 was contracted not to start before that | `7be86f47` (builders), `6bedbc47` (parsers), `5f9aa16f` (WeenieError), `4281750b` (delete AllegianceTree); fix-round: `ed308087` (mechanism+blast MUST/SHOULD-FIX code+tests), this commit (register/plan/seams doc corrections) | mechanism `docs/research/2026-08-12-fa1-review-mechanism.md` (2 MUST-FIX, 5 SHOULD-FIX, all applied); blast `docs/research/2026-08-12-fa1-review-blast.md` (4 MUST-FIX, 5 SHOULD-FIX, all applied). **Live-surface note (blast SF-1):** FA1 changed the observable output of the ALREADY-LIVE `@allegiance info` command in two retail-faithful ways — vassal print order reversed (now pinned by a 3-vassal test through `FormatAllegianceInfoLines`) and a malformed tree now prints nothing instead of a partial roster (now pinned at the `GameEventWiring` layer) — not a "purely unwired" slice. | automated: Release build + full suite green throughout. **Reconciled totals (blast MF-4):** the ledger's own prior figure (13,149/4/0) is CONFIRMED correct by direct measurement at the pre-fix-round tip `bc693728` (13,153 total = 4+916+1559+15+130+119+4856+877+4677 across all 9 test projects); the campaign-start baseline in §5 is 13,103/4/0, and the diff-verified FA1 delta is **+58 added / −9 deleted (deleted `AllegianceTreeTests.cs`) = net +49**, i.e. 13,103+49=13,152 — one test of drift against the directly-measured 13,153/13,149 baseline, attributed to the §5 baseline being captured at a different point in git history than the FA1 diff's actual parent, not a further miscount. This fix round adds a further **+9 tests** (2 golden vectors for the new `0x001F` builder, 1 D5 `<<1` pin at the `0x02C0` site, 4 MF-1/SF-1 boundary tests, 2 blast SF-1 live-surface pins) — **final: 13,158 passed / 4 skipped / 0 failed (13,162 total), directly measured.** | | FA2 | **FIX-ROUND CLOSED 2026-08-12** — both reviews' MUST-FIX/SHOULD-FIX findings applied; automated gate only, per contract | `RuntimeFellowshipState`/`RuntimeAllegianceState` (2 new sibling J-owners, `src/AcDream.Runtime/Gameplay/`), the full 8-edit template applied twice (`GameRuntime.cs`, `RuntimeGenerationReset.cs`, `RuntimeGameplayOwnership.cs`/`RuntimeSimulationOwnership.cs`, `GameRuntimeGameplayViews.cs`, `GameRuntimeCommands.cs`, `GameRuntimeEvents.cs`, `GameRuntimeViews.cs`), ~~12 new `WorldSession.Send*` wrappers, 15 new `GameEventWiring.WireAll` delegate holes~~ **[FA2 fix-round addendum, 2026-08-12 (docs/research/2026-08-12-fa2-review-blast.md SHOULD-FIX 3): both counts were wrong. `WorldSession.cs:2318-2404` adds **11** new `Send*` wrappers (7 fellowship + 4 allegiance) — `SendAllegianceInfoRequest` pre-dates FA2; the **12** figure belongs to a different count, `cced83b4`'s `*RuntimeCmd` records / `LiveSessionCommandBindings` send delegates. `GameEventWiring.cs:107-120` adds **10** delegate holes (9 new + the `0x027C` fold), not 15 — matching the "11 S→C events" the seam doc's §2.3 table names, of which 2 (`0x01C9`/`0x01CA`) are correctly left unregistered (dead COMDAT-fold no-ops) and 1 (`0x027C`) was already registered pre-FA2.]** registered at the single site (`LiveSessionEventRouter.cs`), both `LiveSocialSessionBindings` construction sites updated (`LiveSessionRuntimeFactory.cs`, `HeadlessSessionHost.cs`), `IRuntimeFellowshipCommands`/`IRuntimeAllegianceCommands` implemented on both host command adapters (`DirectGameRuntimeCommandAdapter`, `CurrentGameRuntimeCommandAdapter` + its `LiveSessionCommandRouter`/`LiveSessionCommandBindings` App-bus plumbing), divergence register rows TS-81 (filed) + TS-80 (narrowed); fix-round: `4272ad0e` (mechanism MUST-FIX 1/2 + blast MUST-FIX 1/2 + blast SF-1 + mechanism SF-2 — allegiance reset semantics, 0x027C stops seeding, teardown-ledger off-by-one, conditional delegate holes, disposed-checks-inside-lock), `ded23067` (mechanism SF-3/4/5/6 + blast SF-4/5/7 — RecalculateEvenXPSplitting, locked/departed admission gate, AllegianceProfileLookups reuse, non-null checkpoint defaults, router self/other-quit test, ResetSession disposed-guard parity, GetVassals allocation doc), this commit (register/plan/seams doc corrections) | mechanism `docs/research/2026-08-12-fa2-review-mechanism.md` (2 MUST-FIX, 6 SHOULD-FIX, all applied); blast `docs/research/2026-08-12-fa2-review-blast.md` (2 MUST-FIX, 7 SHOULD-FIX, all applied). **Allegiance register-row re-evaluation (blast SF-6):** the design blast SF-6 asked to be either dropped or given a register row is now fully retired by MF-1's fix — `RuntimeAllegianceState` clears at every generation reset exactly like `RuntimeFellowshipState` and exactly like retail's `OnEndCharacterSession` hook, so there is no remaining acdream-vs-retail deviation for allegiance persistence to name a row for. No register row added; conclusion recorded here per the fix-round contract. | automated: Release build + full suite green throughout. **Pre-fix-round baseline 13,201/4/0 (13,205 total, measured at `12053e61` by the blast review) → fix-round 13,215/4/0 (13,219 total), +14 tests, arithmetic exact:** `RuntimeAllegianceStateTests.cs` net +2 (−1 deleted `ApplyInfoResponseSelf_...`, +3 new `ResetSession_*`), `RuntimeFellowshipStateTests.cs` +10 (1 `ResetSession_AfterDispose_...` + 3 `[Theory]` cases for `RecalculateEvenXPSplitting` + 1 `ShareXp`-off case + 1 full-update-never-recomputes case + 4 locked/departed admission-gate cases), `GameRuntimeTests.cs` +1 (`CompletedTeardownStagesAccumulatesExactlyOneFlagPerStage`), `Session/LiveSessionEventRouterTests.cs` +1 (`FellowshipQuit_RoutesSelfGuidToClearAndOtherGuidToRemove`); `RuntimeGenerationResetTests.cs` and `GameEventWiringTests.cs` each renamed one test in place (net 0); `GameRuntimeContractTests.cs` gained two trailing constructor arguments at its sole positional `RuntimeStateCheckpoint` site (compile-fix only, no new test). | | FA3 | **FIX-ROUND CLOSED 2026-08-12** — both reviews' MUST-FIX/SHOULD-FIX/NIT findings applied; still owes the user's connected gate (script: `docs/research/2026-08-12-campaign-fa-test-script.md`, itself corrected by this fix round — see MF-1/MF-2 below). | `0a9ca2f1` (`SocialPanelController` + 4 per-page controllers + `SocialPanelRowText`, `UiTemplateListBox.Flush`, `RetailPanelCatalog.SocialPanel`/`WindowNames.SocialPanel`, `RetailUiRuntime` Mount/Tick/F3/F4 wiring, `InteractionRetainedUiComposition`'s `SocialRuntimeBindings`, register row AD-79); `74c3d85d` (fixture generator entry + committed `social_panel_2100006E_1000018F.json`, `SocialPanelLiveMountProbeTests`, `SocialPanelControllerTests`, `RetailPanelCatalogTests` additions, `FixtureLoader` additions); `b6a25110`/`d7e1cffd` (gate script + ledger + research addendum). Fix-round: `9afa05b5` (blast MF-1 scrollbar wiring + blast SF-2/SF-3 rebuild discipline/revision-latch ordering + mechanism SF-1 F3/F4 relabel + mechanism SF-8 disposed guard, code+tests), `35c40a9b` (mechanism SF-2/blast SF-4 allegiance per-frame allocation hoist + mechanism SF-7 coarser-gate FA5 acceptance line), `ae772709` (mechanism SF-4 fellowship checkbox count + mechanism SF-9 row-text doc + blast SF-5 Flush doc + mechanism SF-3 probe assertions), `a5553904` (mechanism MF-1/MF-2 gate-script corrections + mechanism SF-4 gate-script hedge fix + blast N-8 two new gate steps + blast SF-6 #383 timing correction + blast SF-7 bold-marker fix + mechanism SF-1 research-doc U11), this commit (ledger update). | mechanism `docs/research/2026-08-12-fa3-review-mechanism.md` (2 MUST-FIX, 9 SHOULD-FIX, all applied); blast `docs/research/2026-08-12-fa3-review-blast.md` (1 MUST-FIX, 6 SHOULD-FIX, 1 NIT, all applied). **Live-DAT finding (corrects the coordinator addendum, §10):** the real authored `0x2E` tab table pairs button `0x1000028C` ("Allegiance" caption) with page `0x10000291` as the DEFAULT entry — NOT Friends, which the addendum's x-order guess implied; each page's own `P0x57` independently corroborates (Allegiance page `P0x57=0x1000000E` == `ToggleAllegiancePanel`/F3, Fellowship page `P0x57=0x1000000F` == `ToggleFellowshipPanel`/F4). See `SocialPanelController`'s class doc for the full corrected table. **Unrelated fixture drift caught and reverted:** the `ACDREAM_REGENERATE_UI_FIXTURES=1` run used to produce the new fixture also silently regenerated `keyboard_config_21000009.json` and `options_2100002B.json` with large diffs against this machine's currently-installed DAT (pre-existing environment drift, not FA3-caused) — both were `git checkout`'d back to HEAD before committing; only the new fixture is included; the fix round's blast SF-6 correction narrows this drift to exactly those two OP-era fixtures (git timestamps put the drift window at ~18-21h, same day, not "days ago" as originally filed) and adds the mechanism reviewer's independent no-drift confirmation for the new social-panel fixture itself. Friends/Squelch action buttons are honest INERT per D1 (register row AD-79, one row covering both pages' seven controls, not one per button — now cited BY NAME in both page controllers' own doc comments, mechanism SF-6). The fix round's two connected-gate corrections (mechanism MF-1/MF-2) matter most for the still-owed user gate: the script previously carried a REFUTED tab x-order into step 1/9/10 (priming the user to report the correct Allegiance-left-most layout as wrong) and sent the user to a `@allegiance info`-reveals-the-panel trigger that cannot fire post-FA2 (told to report it as a bug when it correctly did nothing) — both are corrected to state the true FA3 expectation. | automated: Release build green throughout; full solution suite green at every commit. **Pre-fix-round baseline 13,233/4/0 (13,237 total) → fix-round 13,238/4/0 (13,242 total), +5 tests, arithmetic exact:** `SocialPanelControllerTests.cs` +5 (`Friends_ScrollbarModel_IsWiredToListBoxScroll`, `Squelch_ScrollbarModel_IsWiredToListBoxScroll`, `Friends_LongRoster_IsReachableViaScrollbar`, `Squelch_LongRoster_IsReachableViaScrollbar`, `Friends_RevisionBumpWhileHidden_DoesNotRebuild_ButRebuildsOnShow`); `SocialPanelLiveMountProbeTests.cs` gained two new assertions inside its existing single env-gated `[Fact]` (net 0 new test count — trivially passes without `ACDREAM_PROBE_LIVE_MOUNT=1`, same as before); two existing `SocialPanelControllerTests.cs` tests were extended in place (`FriendsAndSquelchActionButtons_AreClickable_ButHaveNoHandler` now also covers `0x1000052C`; `Friends_ReactsToRevisionChange_OnTick` now calls `OnShown()` to match the new visibility gate) — net 0 new tests from those two. Directly measured per-project: Cli 4, UI.Abstractions 916, Runtime 1607, Bake 15, Content 130, Headless 119, App 4876/3 skip, Core.Net 895, Core 4676/1 skip — sum 13,238 passed / 4 skipped / 0 failed. | -| FA4 | — | | | | +| FA4 | **CODE-COMPLETE 2026-08-12** — implementer pass done; dual-lens review and the user's connected gate (several steps `[TWO-CLIENT]`, deferrable to FA6) are owed | `357d2032` (Runtime: `IRuntimeFellowshipView.GetMembers`, `SelectionChangeSource.Social`, +4 `RuntimeFellowshipStateTests`); `5bdd0528` (App: `SocialFellowshipPageController` roster/D4/create/actions/checkboxes rewrite, `RowTemplateResolver` extraction, `UiTemplateListBox.FlushPreservingScroll`, `RetailUiRuntime` D6 intercept + `MountSocialPanel` rewiring, Friends/Squelch scrollbar carry-forward 3, `CharacterOptionsPageController` D7 un-dim, extended `SocialPanelLiveMountProbeTests`, +30 tests); this commit (register rows AD-80/AD-81 + AD-78 addendum, gate script §FA4, ledger) | owed | automated: Release build green throughout; full solution suite green at every commit. **Baseline 13,238/4/0 (13,242 total) → FA4 13,272/4/0 (13,276 total), +34 tests, arithmetic exact:** `RuntimeFellowshipStateTests.cs` +4 (`GetMembers_*`); `RowTemplateResolverTests.cs` +3; `UiTemplateListBoxFlushPreservingScrollTests.cs` +4; `SocialFellowshipPageControllerTests.cs` +23 (new file — roster build/diff/rebuild, D5 formatting, create-flow gating, member-action wiring, button-enable rules, checkbox wiring, D4 idempotency). `CharacterOptionsPageControllerTests.cs`/`SocialPanelControllerTests.cs`/`SocialPanelLiveMountProbeTests.cs` extended in place (net 0 new tests from those three — one existing assertion's expected counts changed, one probe test gained assertions inside its existing single env-gated `[Fact]`). **Live-DAT verification (`ACDREAM_PROBE_LIVE_MOUNT=1`, real installed DATs, not a fixture):** the fellowship name-entry field builds as `UiField` (Editable=1 confirmed authored), all 11 buttons/checkboxes resolve as `UiButton`, the ListBox's sole template pair (`0x21000030`/`0x10000281`) resolves through the production `RowTemplateResolver` with all 5 checked row fields at the right widget types, every checkbox label/tooltip resolves to real retail English (`ID_PlayerOption_*` in `0x23000003`), the Open/Close captions resolve to `"Open"`/`"Close"` (`ID_Fellowship_*` in `0x23000001`), and a full production-path `SocialFellowshipPageController.Bind()` against the live layout emits zero "not found" console warnings. **Structural finding (not a bug, a design confirmation):** the live dump shows the name field, Create button, and all four checkboxes are children of `0x1000026B` (the NOT-in-fellowship frame) — retail's Create-flow controls are visible ONLY while you have no fellowship, never simultaneously with the roster; the existing empty/full frame-visibility swap already produces this for free (child visibility cascades from an invisible ancestor — `UiElement.cs:486/540/573/586`), so no extra gating code was needed. **Contradictions/deferrals:** (1) the retail `StringInfo` variable-substitution engine (`StringInfo::InqString` → `StringTableMetaLanguage::UnescapeString`) is unresolved in this campaign's decomp scope — row/stats/vitals text renders as numeric composites, not retail's exact sentence (AD-81); (2) `ACCharGenData::FormatName` is unported — the Create flow sends the raw typed name, not retail's canonicalized form (also AD-81); (3) the D5 percentage table's proportional (non-even-split) branch needs a per-level XP-to-next-level table acdream does not have — that branch omits the percentage rather than computing one (documented in `FormatStatsText`'s own doc, not a separate register row); (4) the Recruit button's enable rule does not gate on "target is a player" (retail does) — acdream's UI layer has no cheap classification for this, and the server refuses a non-player target the same way retail's own click-handler silently no-ops, so this is a superset-of-retail enable rule, not a wire-behavior gap (inline comment, not a register row). | | FA5 | — | | | | | FA6 | — | | | | | FA7 | — | | | | diff --git a/docs/research/2026-08-12-campaign-fa-test-script.md b/docs/research/2026-08-12-campaign-fa-test-script.md index 94886e04..d14a5b29 100644 --- a/docs/research/2026-08-12-campaign-fa-test-script.md +++ b/docs/research/2026-08-12-campaign-fa-test-script.md @@ -1,9 +1,10 @@ # Campaign FA connected-gate test script -**Status: FA3 owes its connected gate.** Launch with `ACDREAM_LIVE=1` -against the local ACE server (`testaccount` / `+Acdream`). Anything marked -**INERT** is authored and clickable but deliberately does nothing yet — -that is the correct, contracted behavior for this slice (D1), not a bug. +**Status: FA3 owes its connected gate; FA4 (below) owes its own.** Launch +with `ACDREAM_LIVE=1` against the local ACE server (`testaccount` / +`+Acdream`). Anything marked **INERT** is authored and clickable but +deliberately does nothing yet — that is the correct, contracted behavior +for this slice (D1), not a bug. FA3 is the panel SHELL only: mount, F3/F4 open paths, tab switching, all four pages' empty states, and Friends/Squelch read-only lists. Fellowship @@ -11,6 +12,17 @@ roster rows, the create-fellowship dialog, live vitals, and every Allegiance swear/break/kick action are FA4/FA5 scope — do not report their absence here. +**FA4 makes the Fellowship page fully live** (§FA4 below): roster rows, +the `0x00A6` panel-open/vitals-stream gate, the create flow, member +actions + confirmations, the four option checkboxes, and the open/close +caption swap. **Honest limitation up front: several FA4 steps need a +SECOND character** (recruit, the inbound invite dialog, watching another +member's vitals update). If a second ACE account/character is not +available for this gate, run every step marked **[SOLO]** and defer the +steps marked **[TWO-CLIENT]** to Campaign FA slice FA6's bot-vs-ACE gate +(`docs/plans/2026-08-11-fellowship-allegiance-campaign.md`, D8) — do not +treat an unrun two-client step as a failure. + --- ## FA3 — the social panel shell @@ -84,17 +96,17 @@ absence here. as "three... a fourth may also be present"; the fixture and the decomp both settle it at four). No member list, no leader/quit/open/recruit/dismiss/disband buttons visible — those - belong to the OTHER (in-fellowship) frame, which is hidden. - **INERT:** the Create Fellowship button, the name field, and all - four checkboxes do nothing yet on click/edit (FA4 wires the create - flow and the checkboxes already have Options-tab live consumers — - this page's OWN copies are not yet cross-bound). + belong to the OTHER (in-fellowship) frame, which is hidden. **[FA4 + correction]** the Create Fellowship button, the name field, and the + four checkboxes are now LIVE, not inert — see §FA4 below for their + full behavior; this step's own scope is only the frame/control + PRESENCE, still correct as written. 12. **If the test character IS currently in a fellowship** (uncommon for `+Acdream`'s default state, but possible if a prior session left one - active), open the Fellowship tab instead expecting: a fellowship - name display, a member roster ListBox (empty rows — FA4 populates - them), and six buttons (Leader/Quit/Open/Recruit/Dismiss/Disband). - All six buttons are **INERT** for FA3. + active, or if you ran §FA4's Create step below), open the Fellowship + tab instead expecting: a fellowship name display, a member roster + ListBox, and six buttons (Leader/Quit/Open/Recruit/Dismiss/Disband). + **[FA4 correction]** all six buttons are now LIVE — see §FA4. ### Allegiance page — empty state @@ -237,11 +249,194 @@ Inventory, and Vitae all restore their own last open/closed state too. ### Explicitly NOT in scope for this gate -- Fellowship roster population, vitals, the create-fellowship dialog, - recruit/dismiss/leader/open/disband wire sends, and the option-row - un-dims (FA4). - Allegiance monarch/patron/vassal LIVE population, swear/break/kick wire sends, and their confirmation dialogs (FA5). - Friends/Squelch add/remove/appear-offline/clear wire sends (D1 — out of campaign scope entirely, register row AD-79). - The bot-vs-ACE two-session fellowship gate (FA6). + +--- + +## FA4 — the Fellowship page fully live + +**Prerequisite:** open the Fellowship tab (F4, or click it from the +already-open panel). All step numbers below restart at 1 for this +section's own numbering. + +### Create (name + shareXP) — [SOLO] + +1. **With no fellowship, type a name into the fellowship-name field and + watch the Create Fellowship button.** It is DISABLED while the field + is empty/whitespace-only and ENABLES the instant you type a + non-blank character — this IS retail's refusal mechanism (lane B + §2.2: the button itself is the guard; there is no separate error + message to expect). +2. **Before clicking Create, toggle the "Share Fellowship Experience and + Luminance" checkbox** (on or off — your choice) and note its state. +3. **Click Create Fellowship.** The panel switches from the empty-state + frame to the populated frame: your fellowship's name appears, you + appear as the sole roster row (with your own name, level, and + vitals bars), and the six member-action buttons appear. Only the + Quit button should be enabled (you're the leader, but with no other + member selected, Leader/Dismiss stay disabled — see "button states" + below) — Disband and Open should ALSO be enabled (you're the leader + of a real fellowship now). +4. **Check the ACE server confirms `ShareXP` matches what the checkbox + said** at the moment you clicked Create (a chat command like + `@fellow` status, or simply trusting the wire — this is a soft + check, not required to fail the gate over). + +### Roster with a second character — [TWO-CLIENT] (bot alternative: FA6) + +5. **Recruit a second character into the fellowship**: log a second + ACE account/character into a separate acdream (or retail) client, + select them in the world (click them), then click **Recruit** on the + first client. If accepted (see the invite steps below), the second + character appears as a NEW roster row on the first client — + confirm their name, level, and vitals bars render (not blank, not a + crash). +6. **With two members in the fellowship, scroll the roster if it + overflows the visible area** and confirm both rows remain reachable + (same scrollbar mechanism FA3 already gated, `UiTemplateListBox`). + +### Vitals updating while the panel is open AND the frozen-roster +behavior when closed — [TWO-CLIENT], the `0x00A6` gate made observable + +7. **With the Fellowship tab open and a second member in the + fellowship, have that second character take damage or use + stamina/mana** (attack a monster, cast a spell, etc.). Their + health/stamina/mana bars on the FIRST client's roster should update + within a second or two — this is the vitals stream D4 turns on + (`0x00A6` panel-open declaration gates ACE's `0x02C0` sends, lane B + §4.5). +8. **Close the social panel (or switch to a different tab), have the + second character take more damage, then reopen the Fellowship tab.** + EXPECTED: the roster shows the LAST vitals it had before the panel + closed (frozen), then updates live again within a second or two of + reopening — this is the direct, observable consequence of `0x00A6` + being sent `false` on hide and `true` on show. A roster that keeps + updating in real time WHILE THE PANEL IS CLOSED would mean D4 is not + actually gating anything — report that as a bug. +9. **Your OWN row's vitals should always update** (your own vitals are + driven by the existing player-vitals pipeline, not the fellowship + `0x02C0` stream) — this is expected and not a sign that `0x00A6` is + failing to gate the OTHER member's stream. + +### Recruit/dismiss/quit/disband/leader flows with their confirmations + +10. **[SOLO, self only]** With a solo (1-member) fellowship you lead, + click **Quit**. You leave the fellowship (no confirmation dialog for + Quit — retail has none, lane B §2.5) and the panel reverts to the + empty-state frame. +11. **[SOLO, self only]** Create another fellowship, then click + **Disband** instead. Same visible outcome (empty-state frame) via + the wire's `disband=true` flag — no confirmation dialog either. +12. **[TWO-CLIENT]** With two members, select the OTHER member's roster + row (click their name) and click **Dismiss**. They are removed from + the roster; their own client sees themselves leave the fellowship. +13. **[TWO-CLIENT]** With two members, select the other member's row + and click **Leader** (Assign Leadership). The `LeaderGuid` changes — + confirm via the leader's name tinting gold in the roster (this + controller's own visual cue — lane A's row template has no + dedicated leader marker, see `SocialFellowshipPageController`'s + class doc) — and confirm the enable/disable states flip: the OLD + leader's Disband/Open buttons should now be DISABLED, the NEW + leader's (on their own client) should now be ENABLED. +14. **[TWO-CLIENT, the leader-handoff rule]** As the CURRENT leader of a + 2+-member fellowship, click **Quit** (not Disband). EXPECTED: + leadership transfers to the other member FIRST (their client should + briefly show themselves as leader), THEN you leave — this is + retail's pre-quit leader hand-off (lane B §2.5/§3.6, already ported + in `RuntimeFellowshipState.RequiresLeaderHandoffBeforeQuit`). Report + if the fellowship is left leaderless or the WRONG member becomes + leader. + +### The open-toggle caption swap — [SOLO] + +15. **With a fellowship active, note the Open/Close button's caption.** + A CLOSED fellowship shows **"Open"**; click it and it should flip to + show **"Close"** (retail's button reads as the ACTION available, not + the current state — lane A §4.1). Click again to flip back. +16. **The Open/Close button is enabled only when you are the leader** — + confirm it is disabled (greyed / unclickable) if you are a member + but not the leader (needs a second client to observe from the + non-leader side). + +### The invite dialog + both option-bit behaviors — [TWO-CLIENT] + +17. **Baseline (neither option bit set):** on the SECOND client, ensure + both "Ignore Fellowship Requests" and "Automatically Accept + Fellowship Requests" are UNCHECKED (Character tab or this page's own + checkboxes — either surface, they're the same value). From the + FIRST client (in a fellowship, as leader or with Recruit rights), + select the second character in the world and click **Recruit**. The + SECOND client should show a confirmation dialog asking to join the + fellowship. **Accept it** — the second character joins, appears on + the first client's roster. +18. **Repeat, but with "Ignore Fellowship Requests" CHECKED on the + second client.** No dialog should appear at all — the invite is + auto-declined silently (D6). Confirm the second character does NOT + end up in the fellowship. +19. **Repeat, but with "Automatically Accept Fellowship Requests" + CHECKED on the second client** (and Ignore unchecked — the two are + mutually exclusive; checking one should auto-uncheck the other, + confirm that too). No dialog should appear — the second character + joins IMMEDIATELY without any click. +20. **Reject an invite** (baseline state, dialog showing): click Reject + instead of Accept. The second character does NOT join; no crash or + stuck dialog state on either client. + +### Share-column expectations vs ACE — [SOLO, low member counts] + +21. **With a solo (1-member) fellowship, Share XP checked, the stats + text should read a percentage of 100%** (the even-split table's + first entry, lane B §7.2). With 2+ members (if a second client is + available) sharing evenly, the percentage should drop per the + table (75% at 2, 60% at 3, …). **This is a SOFT / informational + check, not a fail-the-gate item:** if you happen to reach exactly 9 + members, the panel will show **31%** (retail's own 0.3111111) while + ACE's actual XP grant math currently uses .3 — a KNOWN, filed + divergence between ACE and retail, not an acdream bug (register row + AD-80). Do not report a 9-member mismatch between the panel's + percentage and the XP you actually received. +22. **With Share XP UNCHECKED, the stats text should read "0%"** for + every member (retail's own literal `pct = 0.0f` branch, not a gap). + +### What to report (FA4-specific — in addition to the FA3 list above) + +- The Create button staying enabled with an empty/whitespace-only name + field, or staying disabled once real text is typed. +- Any roster row showing a blank name, a frozen level, or a vitals bar + that never updates AT ALL while the panel is open and a second member + is taking damage (contrast with step 8's EXPECTED freeze while the + panel is CLOSED — that one is correct, not a bug). +- A membership change (join/leave) resetting your scroll position to the + top of a long roster — the whole point of the FA3 carry-forward this + slice closed (`UiTemplateListBox.FlushPreservingScroll`). +- Quit/Disband/Dismiss/AssignLeader/SetOpen not reaching the server (no + visible effect on either client), or reaching it with the WRONG guid + (e.g. Dismiss removing the wrong member). +- The leader hand-off (step 14) leaving the fellowship leaderless, or + transferring leadership to the wrong member. +- An invite dialog appearing when the ignore/auto-accept bit says it + should not (or the reverse — no dialog when both bits are off). +- The two option checkboxes NOT staying mutually exclusive (both ending + up checked at once). +- Any crash, hang, or exception in the log during create/recruit/ + dismiss/quit/disband/leader/open actions or while the invite dialog is + open. + +### Explicitly NOT in scope for this gate (FA4) + +- Allegiance monarch/patron/vassal LIVE population, swear/break/kick + wire sends, and their confirmation dialogs (FA5). +- Friends/Squelch add/remove/appear-offline/clear wire sends (D1 — out + of campaign scope entirely, register row AD-79). +- The bot-vs-ACE two-session fellowship gate (FA6) — every step marked + **[TWO-CLIENT]** above may be deferred there if a second account is + not available for this connected gate. +- Retail's exact `StringInfo`-templated row/stats sentence and + `ACCharGenData::FormatName` name canonicalization — acdream renders + plain numeric composites and the raw typed name instead (register row + AD-81); do not report "the text doesn't read like a full sentence" or + "my typed name wasn't auto-capitalized" as bugs.