diff --git a/src/AcDream.App/UI/Layout/CharacterOptionsPageController.cs b/src/AcDream.App/UI/Layout/CharacterOptionsPageController.cs index 7cfe75c5..6e7158f0 100644 --- a/src/AcDream.App/UI/Layout/CharacterOptionsPageController.cs +++ b/src/AcDream.App/UI/Layout/CharacterOptionsPageController.cs @@ -111,7 +111,7 @@ public static class CharacterOptionsPageController /// steps 13-18, written after the code landed): /// /// - /// NOT dimmed (19 rows, real acdream-side + /// NOT dimmed (16 rows, real acdream-side /// consumer): Group B rows actually bound — /// ViewCombatTarget/AutoTarget/AutoRepeatAttack /// (combat: LiveCombatAttackOperations.cs, @@ -125,14 +125,10 @@ public static class CharacterOptionsPageController /// InteractionRetainedUiComposition.cs:326), all six /// ListenTo*Chat ids (TurbineChatMembershipGate.cs:105-136 /// gates every Turbine room join on the matching bit), and — landed at - /// Campaign FA slice FA4, D7 — IgnoreFellowshipRequests/ - /// FellowshipAutoAcceptRequests (consumed by - /// RetailUiRuntime.TryAutoRespondToFellowshipInvite's auto- - /// decline/auto-accept, D6) and FellowshipShareXP/ - /// FellowshipShareLoot (consumed by the fellowship page's own - /// Create flow / D5 display / second checkbox surface, - /// SocialFellowshipPageController). - /// Dimmed (31 rows, store-only): every + /// Campaign FA slice FA4, D7, SURVIVING the fix-round correction below + /// — FellowshipShareXP (the Create-flow click genuinely reads + /// it as the sent shareXP bit, SocialFellowshipPageController.WireButtons). + /// Dimmed (34 rows, store-only): 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" (ShowTooltips, @@ -142,9 +138,53 @@ public static class CharacterOptionsPageController /// FilterLanguage, MainPackPreferred), plus every Group D /// deferral (SalvageMultiple, /// DisableHouseRestrictionEffects, - /// HearPkDeathMessages) — FellowshipShareXP left Group D - /// at FA4 (above), it stays a bullet-list resident here only for the - /// historical count. + /// HearPkDeathMessages). + /// + /// + /// FA4 fix-round correction, 2026-08-12 (D6/D7 plan correction + + /// mechanism SF-8). FA4 originally un-dimmed FOUR rows here: + /// IgnoreFellowshipRequests/FellowshipAutoAcceptRequests + /// (claiming a consumer in a client-side invite auto-respond + /// interceptor) and FellowshipShareXP/FellowshipShareLoot + /// (claiming the fellowship page's own checkbox surface as a + /// consumer). Both claims were wrong, for two SEPARATE reasons: + /// + /// + /// D6's correction (the plan doc, 2026-08-12): + /// retail's client reads NEITHER option bit on the invite/confirmation + /// path — Handle_Character__ConfirmationRequest @0x005640A0, + /// RecvNotice_FellowshipRequest @0x00490880, and + /// MakeFellowRequestDialog @0x00490620 (whose ONLY guard is + /// m_fellowRequestContext) were read in full; a whole-file sweep + /// of both accessors finds zero reads on any confirmation path. ACE + /// filters both bits SERVER-SIDE. The client-side interceptor + /// (RetailUiRuntime.TryAutoRespondToFellowshipInvite) was + /// therefore deleted outright — the fellow-invite dialog always shows, + /// exactly like retail. IgnoreFellowshipRequests/ + /// FellowshipAutoAcceptRequests go back to store-only, exactly + /// like the two allegiance bits (IgnoreAllegianceRequests/ + /// DisplayAllegianceLogonNotifications) they were always meant + /// to parallel — both pairs are pure server-side filters with NO client + /// consumer. + /// Mechanism SF-8 (the review, same date): + /// FellowshipShareLoot's claimed consumer — "a second live + /// checkbox surface on the fellowship page" — is not a consumer at + /// all: nothing in acdream ever READS the stored value back + /// (FormatStatsText uses snapshot.ShareXp only; the + /// 0x00A2 Create builder carries shareXP alone), and the + /// live-DAT dump confirms its checkbox (0x10000273) is a child + /// of the NOT-in-fellowship frame — invisible whenever you actually + /// have a fellowship to loot-share within. A second EDITOR of a stored + /// value is not a consumer by AD-78's own definition ("drives nothing + /// OBSERVABLE client-side"). Only FellowshipShareXP survives — + /// its value is genuinely READ by the Create click. + /// + /// + /// Net: 35 (pre-FA4) → FA4 shipped 31 → fix round reverts three of the + /// four un-dims (Ignore, AutoAccept, ShareLoot) → 34 of 50 dimmed / + /// 16 live, ONE net un-dim from pre-FA4 baseline + /// (FellowshipShareXP only). + /// /// Flagged ambiguity, resolved by code evidence /// (see final report, not re-litigated here): the research doc's /// §7.1 lists AcceptLootPermits in BOTH Group A's bullet list AND @@ -156,12 +196,11 @@ public static class CharacterOptionsPageController /// IgnoreFellowshipRequests/FellowshipAutoAcceptRequests /// had a genuine coded mutual-exclusion side effect /// (RuntimeCharacterState.TrySetOption, MF-2) that the research - /// 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 - /// FellowshipShare* rows FA4 also gave real consumers. + /// doc's Group A classification did not capture — this remains true and + /// harmless (the mutual exclusion is enforced regardless of which + /// surface's caption is dimmed), but it is NOT itself a client-visible + /// CONSUMER of the option's VALUE, so both rows stay dimmed per the + /// fix-round correction above. /// /// private const bool Live = false; @@ -203,11 +242,11 @@ public static class CharacterOptionsPageController new("ID_CharacterOption_Grouping_Section", new RowSpec[] { 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.IgnoreFellowshipRequests, "IgnoreFellowshipRequests", StoreOnly), // fix-round D6 correction: pure server-side filter, NO client consumer — reverted from FA4's Live (see class doc) 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(CharacterOptionId.FellowshipShareXP, "FellowshipShareXP", Live), // D7 (Campaign FA slice FA4): consumed by the fellowship Create flow (its value IS the sent shareXP bit) + new(CharacterOptionId.FellowshipShareLoot, "FellowshipShareLoot", StoreOnly), // fix-round mechanism SF-8: no real acdream-side READER of the stored value — reverted from FA4's Live (see class doc) + new(CharacterOptionId.FellowshipAutoAcceptRequests, "FellowshipAutoAcceptRequests", StoreOnly), // fix-round D6 correction: pure server-side filter, NO client consumer — reverted from FA4's Live (see class doc) }), new("ID_CharacterOption_OtherPlayers_Section", new RowSpec[] { diff --git a/tests/AcDream.App.Tests/UI/Layout/CharacterOptionsPageControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/CharacterOptionsPageControllerTests.cs index aa9bd75a..10645d1f 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CharacterOptionsPageControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CharacterOptionsPageControllerTests.cs @@ -698,19 +698,28 @@ public sealed class CharacterOptionsPageControllerTests // ── AD-78 caption dimming (user-directed, 2026-08-11, gate 2) ─────────── /// - /// The exact 31 values this slice dims + /// The exact 34 values this slice dims /// ( == true), transcribed independently /// of CharacterOptionsPageController.Groups from the derivation /// table in that class's own doc comment. Wiring a future consumer /// for any of these means removing it from this literal set AND /// flipping its Groups table entry from StoreOnly to /// Live consciously — leaving either one stale fails this - /// test. Campaign FA slice FA4, D7 removed four ids from this set — + /// test. + /// + /// + /// Campaign FA slice FA4, D7 originally removed four ids from this set. + /// The FA4 FIX ROUND (2026-08-12) put THREE of them back: /// IgnoreFellowshipRequests/FellowshipAutoAcceptRequests - /// (the fellowship-invite auto-decline/auto-accept, D6) and - /// FellowshipShareXP/FellowshipShareLoot (the fellowship - /// page's Create flow / D5 display / second checkbox surface) all - /// gained real acdream-side consumers. + /// (D6's correction — retail's client reads neither bit on the invite + /// path; the client-side auto-respond interceptor that was their + /// claimed consumer is deleted) and FellowshipShareLoot + /// (mechanism SF-8 — its claimed "second checkbox surface" consumer + /// never actually READS the stored value back). Only + /// FellowshipShareXP survives as a genuine live row (the + /// fellowship Create flow reads it as the sent shareXP bit). + /// Net: 35 (pre-FA4) → 34 (post-fix-round), one net un-dim. + /// /// private static readonly HashSet ExpectedStoreOnlyIds = [ @@ -729,9 +738,12 @@ public sealed class CharacterOptionsPageControllerTests CharacterOptionId.FilterLanguage, CharacterOptionId.ShowHelm, CharacterOptionId.ShowCloak, - // Group 3 (Grouping) — 2 of 6 (FA4, D7: the other four — see class doc) + // Group 3 (Grouping) — 5 of 6 (fix round: only FellowshipShareXP stays live — see class doc) CharacterOptionId.IgnoreAllegianceRequests, + CharacterOptionId.IgnoreFellowshipRequests, CharacterOptionId.DisplayAllegianceLogonNotifications, + CharacterOptionId.FellowshipShareLoot, + CharacterOptionId.FellowshipAutoAcceptRequests, // Group 4 (Other Players) — 10 of 11 CharacterOptionId.AcceptLootPermits, CharacterOptionId.UseDeception, @@ -762,8 +774,8 @@ public sealed class CharacterOptionsPageControllerTests .ToHashSet(); Assert.Equal(ExpectedStoreOnlyIds, actualStoreOnly); - Assert.Equal(31, actualStoreOnly.Count); - Assert.Equal(19, 50 - actualStoreOnly.Count); // the 19 live rows (FA4, D7: +4) + Assert.Equal(34, actualStoreOnly.Count); + Assert.Equal(16, 50 - actualStoreOnly.Count); // the 16 live rows (fix round: net +1 from pre-FA4 baseline) } [Fact]