From 1d743277711cc0f867a00fe74be5d948b8bbd351 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 12 Aug 2026 07:34:09 +0200 Subject: [PATCH] docs(fa4): fix-round register rows AD-82/AD-83, AD-78 count correction, gate-script SF-7/MUST-FIX-1/3 corrections Register (docs/architecture/retail-divergence-register.md): - AD-78: the Character-tab dimmed count had drifted stale through two campaigns (still read "35" after FA4 shipped 31; now 34 after the fix round's three reversions). Addendum explains the full D6/SF-8 chain. Blast review's own SHOULD-FIX 1. - AD-82 (new): the invented leader-tint/selection-tint colors, the name-text-only row click target, and the page-local (not generic UiTemplateListBox) world->panel selection sync -- MUST-FIX 4's disposition plus two items MUST-FIX 5 named as owed rows. - AD-83 (new): the Recruit button's missing "target is a player" gate, previously an inline comment, not a register row -- MUST-FIX 5's third item. Section header bumped 61 -> 63 active rows. Gate script (docs/research/2026-08-12-campaign-fa-test-script.md): - SF-7: fixed step 3's self-contradiction ("only Quit" then "Disband and Open should ALSO be enabled"). - MUST-FIX 3: new reconnect step after the existing close/reopen step. - MUST-FIX 4: new world-selection step under the recruit/dismiss/quit section. - MUST-FIX 1: new HARD-check step for the 6/8-fellow 44%/34% truncation (distinct from the existing SOFT 9-member ACE-divergence note). - MUST-FIX 2 correction: the old invite steps tested whether acdream's CLIENT gates the dialog on the option bits -- a mechanism that never existed in retail and no longer exists in acdream. Rewritten to test the corrected behavior (the dialog always shows regardless of the target's own checkbox state) and to explain what ACE-side filtering would look like if the local server implements it, so a tester doesn't misattribute ACE's behavior to a client bug. - Renumbered steps 9-22 to 9-25 to fit the two new steps; updated the "what to report" section's step cross-references and rewrote its invite/dimming bullets to match the corrected mechanism. Plan (docs/plans/2026-08-11-fellowship-allegiance-campaign.md): - D7 addendum: SF-8's further correction (FellowshipShareLoot reverts too; only FellowshipShareXP survives as genuinely live). Co-Authored-By: Claude Fable 5 --- .../retail-divergence-register.md | 6 +- ...26-08-11-fellowship-allegiance-campaign.md | 15 +- .../2026-08-12-campaign-fa-test-script.md | 165 +++++++++++++----- 3 files changed, 135 insertions(+), 51 deletions(-) diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index 0ee66d32..a41c36e0 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) — 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) +## 2. Adaptation (AD) — 63 active rows (AD-83 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 5) — the Recruit button's missing "target is a player" gate, previously an inline comment not a row; AD-82 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 4/5) — the invented leader-tint/selection-tint colors, the name-text-only row click target, and the page-local (not generic-`UiTemplateListBox`) world→panel selection sync; 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 @@ -172,10 +172,12 @@ readiness/requeue adaptation. See | AD-75 | **Filed 2026-08-11 at Campaign OP slice OP3 (D5).** Urgent Assistance (`0x10000206`) and Report Abuse (`0x10000207`) never call `ShellExecuteA` against `http://support.turbine.com/ics/support/ticketnewwizard.asp?style=classic` — the endpoint is dead in 2026. Each button instead ALWAYS emits its own byte-verified retail failure body (the `ShellExecuteA`-failure `MessageBoxA` text, `(Error code %d)` dropped since no real Win32 error ever occurs, the URL kept verbatim) through the interface-text seam (`RetailLogTextType.ClientLocal`) instead of a native `MessageBoxA` popup. | `src/AcDream.Core/Chat/OptionsPanelText.cs` (`UrgentAssistanceUnavailable`/`ReportAbuseUnavailable`); `src/AcDream.App/UI/Layout/OptionsPanelController.cs` (button wiring) | The URL genuinely does not resolve to a live Turbine support endpoint; attempting `ShellExecuteA` would open a browser to a dead page rather than usefully fail. The retained failure TEXT is retail's own (byte-verified), just always shown instead of conditionally on a real launch failure, and routed to acdream's existing interface-text channel rather than a modal OS dialog (retail's own EoR-era mechanism has no acdream analogue for a one-off native `MessageBoxA`). | If Turbine ever revives the endpoint, both buttons would still short-circuit instead of opening it — a silent staleness, not a crash. | `gmGameplayOptionsUI::ListenToElementMessage @0x0049E110`; `ShellExecuteA` call sites `0x0049E154`/`0x0049E1F0`; research doc `2026-08-10-keyboard-config-and-gameplay-tab.md` §4.1/§4.2 | | AD-76 | **Filed 2026-08-11 at Campaign OP slice OP3 (D5).** In-Game Help Files (`0x10000205`) is authored and clickable but has no handler — clicking it does nothing visible. | `src/AcDream.App/UI/Layout/OptionsPanelController.cs` (button wiring — no callback bound) | Retail's own `KeyStone::OpenHelp` loads a third-party embedded help viewer (`plugins\ACHelpPlugin.dll` via `keystone.dll`) that acdream does not have and cannot port (no DAT-resident help content, no source). Retail ITSELF fails silently with the plugin absent (`KeyStone::m_fnAC2HelpPluginExecute` unresolved) — mirroring that as an inert button is the faithful behavior for "the asset is missing", not an invented stub screen. | A user clicking In-Game Help Files gets no feedback at all, same as retail with the plugin missing — indistinguishable from a dead button unless they already expect the asset-missing case. | `KeyStone::OpenHelp @0x00557010`; `KeyStone::Init @0x00556CF0` (the unresolved plugin function pointer); research doc `2026-08-10-keyboard-config-and-gameplay-tab.md` §4.5 | | 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-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. **[FA4 fix-round addendum, 2026-08-12 — blast SHOULD-FIX 1 + mechanism SF-8/SF-9: this row's own count had drifted stale THROUGH two campaigns (FA4's D7 un-dim landed 31, but this row still read the pre-FA4 "35"; the fix round then reverted three of FA4's four un-dims — see below — landing at 34). The Character-tab count is now 34 of 50 dimmed / 16 live.]** | `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` (**34 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. FA4 D7 originally un-dimmed 4 rows — `IgnoreFellowshipRequests`/`FellowshipAutoAcceptRequests`/`FellowshipShareXP`/`FellowshipShareLoot` — landing at 31. The FA4 FIX ROUND, 2026-08-12, reverted THREE of those four back to dimmed: `IgnoreFellowshipRequests`/`FellowshipAutoAcceptRequests` per the corrected plan D6 (retail's client reads neither option bit on the fellowship-invite path — both are pure server-side filters with no client consumer, exactly like the two allegiance bits that were always meant to parallel them; the client-side auto-respond interceptor that was their claimed consumer, `RetailUiRuntime.TryAutoRespondToFellowshipInvite`, is deleted outright), and `FellowshipShareLoot` per mechanism review SF-8 (its claimed "second checkbox surface" consumer never actually reads the stored value back — a second EDITOR of a value is not a CONSUMER of it). Only `FellowshipShareXP` survives as genuinely live (the fellowship Create flow reads it as the sent `shareXP` bit) — net ONE row un-dimmed from the pre-FA4 baseline, not four.); `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). **The FA4 fix round is itself an instance of this exact risk materializing** — the register row lagged two code-side count changes across one campaign before this addendum caught up. 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) | +| AD-82 | **Filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 4/5).** Three fellowship-panel selection/presentation primitives with no decompiled anchor for the SPECIFIC mechanism, plus a deliberately page-local reimplementation of a retail generic: (1) **the leader-name gold tint** (`SocialFellowshipPageController.LeaderNameColor`, `(1, 0.84, 0, 1)`) — lane A's row-template inventory names no dedicated "this fellow is the leader" element, so this is an invented, clearly-adaptive visual cue, not a ported DAT mechanism. (2) **The panel-local "selected row" tint** (`SelectedNameColor`, `(0.45, 0.85, 1, 1)`) — same disposition, invented for the SAME reason: no decompiled per-row selection marker exists. (3) **Row selection is restricted to the row's name-text click target** — retail's list selection message (`3`/`0x42`, `ListenToElementMessage @0x004901C0`) fires on the WHOLE row; acdream has no generic per-row-element click primitive on an imported template subtree, so only the name text (always present) is clickable — clicking the stats text, a meter, or row whitespace does nothing. (4) **The world→panel selection sync is page-local, not a generic `UiTemplateListBox` primitive** — retail's `gmFellowshipUI::UpdateFellowSelection @0x0048F0F0` keys row identity via `SetAttribute_InstanceID(row, 0x1000000D, fellowIid)` + `UIElement_ListBox::SetSelectedItem`, a mechanism `UiTemplateListBox` does not port (`docs/research/2026-08-11-fa-panel-structure.md` §6.6: "no Flush, no selection model, no per-row instance-id" — `Flush`/`FlushPreservingScroll` shipped at FA3/FA4; the selection half did not). `SocialFellowshipPageController.SyncSelectionFromWorld`/`SetSelectedFellow` reproduce the OBSERVABLE behavior (Dismiss/Leader enable + a row highlight) against this controller's own guid-keyed row dictionary instead. | `src/AcDream.App/UI/Layout/SocialFellowshipPageController.cs` (`LeaderNameColor`, `SelectedNameColor`, `SelectFellow`, `SyncSelectionFromWorld`, `SetSelectedFellow`, `_rows`) | (1)/(2): a minimal, clearly-adaptive visual cue is preferable to inventing a DAT mechanism that was never found — same reasoning the class doc already applied to the leader tint before this row existed. (3): acdream's widget layer has no generic "whole imported subtree is one click target" primitive; the name text is retail's own always-present anchor. (4): the OBSERVABLE contract (button-enable + highlight on world selection) is met without porting the generic `UiTemplateListBox`/`SetAttribute_InstanceID` selection model, which would need a broader ListBox API change touching every ListBox consumer (Options/Config/Chat/Friends/Squelch), not just Fellowship — scoped here as a deliberate, page-local minimum rather than an unscoped widget-layer redesign. | A reviewer comparing a retail screenshot sees two colors retail never paints (gold leader tint, blue selection tint). A user clicking a row's stats text, a meter, or blank row space gets no selection feedback (must click the name specifically). If a future slice (Options/Config/Chat row selection) needs the SAME generic mechanism, this page-local implementation will not serve it — a real `UiTemplateListBox` selection-model port remains owed. | `gmFellowshipUI::UpdateFellowSelection @0x0048F0F0`; `RecvNotice_SelectionChanged @0x0048F1C0`; `ListenToElementMessage @0x004901C0` (message `3`/`0x42`); `docs/research/2026-08-11-fa-panel-structure.md` §6.2/§6.6/§7.3 | +| AD-83 | **Filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 5).** The Recruit button's enable rule does not gate on "target is a player" — retail disables Recruit unless the currently-selected world object IS a player (`ACCWeenieObject::IsPlayer`, `UpdateButtons`, lane B §2.8); acdream's UI layer has no cheap player-vs-non-player classification at this seam, so `RefreshButtonStates` enables Recruit for ANY selected, non-full-fellowship, not-already-a-member target regardless of type. This was previously an inline code comment, not a register row — the wrong call under the register rule (a divergence found without a row is a bug twice over), corrected here. | `src/AcDream.App/UI/Layout/SocialFellowshipPageController.cs` (`RefreshButtonStates`) | acdream's `SelectionState`/world-object model does not carry a player-vs-non-player classification cheaply reachable from the UI layer today; building one for this single enable-rule would be a disproportionate addition for a superset-of-retail rule whose actual SEND is still refused correctly. | A lit, clickable Recruit button when a chest, corpse, or monster is selected instead of a player — clicking it sends a Recruit request the SERVER refuses (the same silent no-op retail's own disabled button would have produced, but reachable in acdream where retail's click handler is unreachable because the button itself is disabled). Not a wire-behavior gap — the recruited/target end state is identical — but a UI-affordance divergence a screenshot comparison would catch. | `gmFellowshipUI::UpdateButtons` (lane B §2.8, the Recruit enable rule); `ACCWeenieObject::IsPlayer` (unlocated exact VA — cited via lane B's UpdateButtons trace) | --- diff --git a/docs/plans/2026-08-11-fellowship-allegiance-campaign.md b/docs/plans/2026-08-11-fellowship-allegiance-campaign.md index 6d94b56d..09a57609 100644 --- a/docs/plans/2026-08-11-fellowship-allegiance-campaign.md +++ b/docs/plans/2026-08-11-fellowship-allegiance-campaign.md @@ -180,7 +180,20 @@ served by the single inbound wiring site. allegiance bits, with no client consumer. FA4's un-dim of these two REVERTS (dimmed count 31 → 33 of 50); only the fellowship SHARE rows the create flow/panel genuinely reads stay un-dimmed. AD-78's addendum names - the corrected set.]** + the corrected set.]** **[FA4 FIX-ROUND further correction, 2026-08-12 + (mechanism review SF-8): "the fellowship SHARE rows... genuinely reads" + above overstated it — only `FellowshipShareXP` is actually READ back by + acdream (the Create-flow click sends it as the `shareXP` wire bit). + `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 is a child of the + NOT-in-fellowship frame — invisible whenever you actually have a + fellowship to loot-share within. `FellowshipShareLoot` REVERTS to + dimmed too (dimmed count 33 → 34 of 50; net ONE row un-dimmed from the + pre-FA4 baseline of 35, not four). AD-78's addendum names this final + corrected set.]** - **D8 — bot-vs-ACE gates are first-class.** A second ACE account/character + a role-discriminated headless policy let one bot recruit/swear at another, with the decisive assertion on the RECRUITED 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 d14a5b29..7b2e58ff 100644 --- a/docs/research/2026-08-12-campaign-fa-test-script.md +++ b/docs/research/2026-08-12-campaign-fa-test-script.md @@ -276,11 +276,13 @@ section's own numbering. 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). + vitals bars), and the six member-action buttons appear. **[fix-round + SF-7 correction: the previous wording here contradicted itself — + "only Quit" followed by "Disband and Open should ALSO be enabled" — + which would prime a tester to report correct behavior as a defect.]** + Quit, Disband, AND Open should ALL be enabled (you're the leader of a + real fellowship now); Leader and Dismiss stay DISABLED because no + other member is selected yet (see "button states" below). 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 @@ -317,32 +319,44 @@ behavior when closed — [TWO-CLIENT], the `0x00A6` gate made observable 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. +9. **[fix-round MUST-FIX 3, new step] Reconnect while the Fellowship page + is left open.** With the Fellowship tab open and a fellowship active, + force a disconnect/reconnect (kill the ACE connection, or use + whatever local reconnect trigger is available) WITHOUT closing the + social panel or switching tabs. Once the client re-enters the world + on the new session, confirm fellow vitals resume updating (repeat + step 7's damage-and-watch check). EXPECTED: vitals update normally on + the new session, exactly as before the reconnect. BUG if vitals stay + frozen for the rest of the new session — that means `0x00A6` was + never re-declared after the generation reset + (`SocialPanelController.ResetSessionDeclaration`). +10. **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, +11. **[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 +12. **[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 +13. **[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 +14. **[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 + class doc, register row AD-82) — 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. +15. **[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 @@ -350,45 +364,80 @@ behavior when closed — [TWO-CLIENT], the `0x00A6` gate made observable in `RuntimeFellowshipState.RequiresLeaderHandoffBeforeQuit`). Report if the fellowship is left leaderless or the WRONG member becomes leader. +16. **[fix-round MUST-FIX 4, new step] Select a fellow in the WORLD + (click their 3D model), not their panel row.** EXPECTED: Dismiss and + Assign-Leader enable exactly as if you had clicked their panel row, + and their row shows the panel's own selection highlight — retail's + `gmFellowshipUI::UpdateFellowSelection` reverse-selection arm (world + click selects the panel row too), reproduced against acdream's own + guid-keyed row tracking rather than a ported generic ListBox + selection model (register row AD-82). ### The open-toggle caption swap — [SOLO] -15. **With a fellowship active, note the Open/Close button's caption.** +17. **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** — + show **"Close"** IMMEDIATELY on click, not after a delay (retail's + button reads as the ACTION available, not the current state, and + pre-toggles its own state before the server echo — lane A §4.1, + fix-round NIT N-0). Click again to flip back. +18. **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] +### The invite dialog — [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 +**[fix-round MUST-FIX 2 correction, 2026-08-12: the OLD steps 18/19 here +tested whether acdream's CLIENT gates the invite dialog on the +Ignore/AutoAccept option bits. That mechanism never existed in retail and +has been deleted from acdream (D6's correction) — retail's client shows +the confirmation dialog UNCONDITIONALLY for every request it receives; +ACE alone decides whether to send one at all, by filtering +server-side. The steps below test the CORRECTED behavior: the dialog +always shows regardless of the SECOND client's own checkbox state. If +your local ACE build implements the server-side filter (lane B: refuses +the recruiter outright when the target has Ignore set; auto-joins +without a confirmation when the target has AutoAccept set), you will +observe the FIRST client's Recruit failing/auto-succeeding with NO +dialog ever reaching the second client at all — that is ACE's behavior, +not a client bug, and is out of scope for this gate either way.]** + +19. **Baseline: 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, + with the server's message text rendered VERBATIM (no added + "Continue?" suffix — type 4 is not in the 2/3/5/6 suffix set). + **Accept it** — the second character joins, appears on the first + client's roster. +20. **Repeat with "Ignore Fellowship Requests" CHECKED on the second + client (the FIX ROUND's regression check — this is the DEFAULT + state for a character who has never touched the option).** EXPECTED: + the dialog STILL shows on the second client exactly as in step 19 + (assuming your local ACE does not itself refuse the recruit + server-side first) — the second character's OWN checkbox state must + NOT silently swallow the invite with no dialog and no chat line. If + it does, that is the exact regression MUST-FIX 2 exists to prevent. +21. **Repeat 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). Same expectation as step 20: the dialog still shows on the + client (acdream no longer auto-responds on its behalf); if your + local ACE implements the auto-accept-without-confirmation + server-side behavior, you may instead see the character join + immediately with no dialog — that is ACE's mechanism, not this + client's. +22. **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 +23. **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 @@ -399,7 +448,17 @@ behavior when closed — [TWO-CLIENT], the `0x00A6` gate made observable 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 +24. **[fix-round MUST-FIX 1, new step — a HARD check, not soft like step + 23's ACE note] At exactly 6 and 8 fellows (even split, Share XP + checked), the panel must show 44% and 34% respectively — NOT 45% or + 35%.** Retail TRUNCATES the percentage (`_ftol2`), it does not + round; both stored float constants (`0.44999998807907104` at 6, + `0.3499999940395355` at 8) sit just BELOW the nice decimal, so + retail's own truncation lands one point lower than naive rounding + would produce. This is a genuine acdream-vs-retail bug if it shows + 45%/35% instead of 44%/34% — unlike step 23's 9-member note, this is + NOT an ACE-divergence exemption. +25. **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) @@ -410,21 +469,31 @@ behavior when closed — [TWO-CLIENT], the `0x00A6` gate made observable 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). +- Fellow vitals staying frozen for the REST OF A NEW SESSION after a + reconnect (step 9) — distinct from step 8's expected freeze, which + clears within a second or two of reopening the tab. - 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 +- The leader hand-off (step 15) 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). +- Selecting a fellow in the WORLD (step 16) NOT enabling Dismiss/Leader + or NOT highlighting their row. +- An invite dialog NOT appearing on the second client regardless of that + client's own Ignore/Auto-Accept checkbox state (steps 19-21) — unless + your local ACE build itself refuses/auto-joins the recruit + server-side first, in which case no request ever reaches the second + client and that is expected, not a bug. - The two option checkboxes NOT staying mutually exclusive (both ending up checked at once). +- A 6- or 8-fellow percentage reading 45%/35% instead of 44%/34% (step + 24 — a real acdream bug, not an ACE-divergence exemption). - Any crash, hang, or exception in the log during create/recruit/ - dismiss/quit/disband/leader/open actions or while the invite dialog is - open. + dismiss/quit/disband/leader/open actions, during a reconnect with the + panel left open, or while the invite dialog is open. ### Explicitly NOT in scope for this gate (FA4)