From bc29a1dbdbde7b0e560376877560dd2272f932f1 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 12 Aug 2026 08:41:10 +0200 Subject: [PATCH] docs(fa5): register rows AD-84/AD-85/AD-86 + AD-82 addendum, gate-script SFA5, ledger Register: - AD-84 -- Swear button's missing "target is a player" enable-rule gate, same class as AD-83's Recruit-button gap. - AD-85 -- the unported StringInfo variable-substitution engine (AD-81's same root cause) extended to the Allegiance page's numeric-only followers/rank/experience-passed-up fields and its three local confirmation dialogs (verbatim-or-bare-name, never invented). - AD-86 -- ACE's deliberate zeroing of seven AllegianceProfile/ AllegianceData fields (officers, officer titles, MOTD, MOTD-set-by, name-last-set-time, lock, approved vassal, timeOnline, allegianceAge), dropped past acdream's own parse layer to match retail's own gmAllegianceUI, which has no widget for any of them either. - AD-82 addendum: the vassal-row click-target-only selection shares point (3)'s limitation, but NOT the invented leader/selection tints (point 1/2) or the Fellowship-only world-selection sync (point 4) -- Allegiance's list-selection message has no SetSelectedObject call. Gate script: new docs/research/2026-08-12-campaign-fa-test-script.md SFA5 section, mirroring SFA4's structure -- the CF-1 subscription steps (including the reconnect-while-closed MF-3-REOPEN analogue), the SF-7 per-relationship monarch/patron steps, vassal-list steps, swear/break/ kick with their confirmations, the ACE-zeroed-field honesty note, and full "what to report"/"explicitly not in scope" lists. Plan ledger: FA5 row filled in against 7ed79eaf with per-item summary, directly-measured totals (13,296/4/0, +11 net from FA4's 13,285/4/0), and the two primary-source resolutions this slice needed beyond the research docs (the self-rank field's live buffed-quality source, and "your follower count" == _total_vassals, confirmed by a fresh targeted decompile of UpdatePlayerData rather than inferred). Co-Authored-By: Claude Fable 5 --- .../retail-divergence-register.md | 7 +- ...26-08-11-fellowship-allegiance-campaign.md | 11 +- .../2026-08-12-campaign-fa-test-script.md | 239 +++++++++++++++++- 3 files changed, 253 insertions(+), 4 deletions(-) diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index a41c36e0..519d5308 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) — 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) +## 2. Adaptation (AD) — 66 active rows (AD-86 filed 2026-08-12 at Campaign FA slice FA5, item 4 — ACE's deliberate zeroing of officers/officer titles/MOTD/MOTD-set-by/name-last-set-time/lock/approved-vassal/timeOnline/allegianceAge, dropped past acdream's own parse layer to match retail's own no-widget presentation; AD-85 filed 2026-08-12 at Campaign FA slice FA5 — the Allegiance page's numeric-only fields and its three local confirmation dialogs' unsubstituted-verbatim-or-bare-name text, the same unported `StringInfo` gap AD-81 filed for Fellowship; AD-84 filed 2026-08-12 at Campaign FA slice FA5 — the Swear button's missing "target is a player" gate, the same class as AD-83's Recruit-button gap; 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 @@ -176,8 +176,11 @@ readiness/requeue adaptation. See | 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-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 | **[FA5 addendum, 2026-08-12:** `SocialAllegiancePageController`'s vassal-row click target shares point (3)'s IDENTICAL limitation — only the row's name text (`0x10000268`) is clickable, for the same "no generic per-row click primitive" reason. UNLIKE Fellowship's row click, Allegiance's does NOT sync to the world selection (lane A §6.2: `gmAllegianceUI::ListenToElementMessage`'s list-selection arm reads the row's `0x10000001` into `m_iidSelectedVassal` only — no `ACCWeenieObject::SetSelectedObject` call), so point (4)'s world→panel sync does not apply to Allegiance at all; only points (1)-(3)'s class of limitation recurs, and point (1)/(2)'s invented tint colors are NOT reused — `SocialAllegiancePageController` uses only a plain offline-grey (`OfflineNameColor`) already covered by the retail-authored `0x100004AA` marker, not an invented selection/leader tint.]** | | 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) | +| AD-84 | **Filed 2026-08-12 at Campaign FA slice FA5.** The Allegiance page's Swear button enable rule does not gate on "target is a player" — retail's `gmAllegianceUI::UpdateSwearButton @0x004908E0` enables Swear only when the current world selection `ACCWeenieObject::IsPlayer()` (lane C §1.3 step 1); acdream's UI layer has the same missing player-vs-non-player classification AD-83 already named for the Fellowship page's Recruit button, so `RefreshButtonStates` enables Swear for any selected, not-already-a-member, not-self target regardless of type. Same root cause and same disposition as AD-83, filed separately because it lives in a different controller/page. | `src/AcDream.App/UI/Layout/SocialAllegiancePageController.cs` (`RefreshButtonStates`) | Identical to AD-83's argument: acdream's `SelectionState`/world-object model has no cheap player classification at this UI seam; building one for two single enable-rules (Recruit, Swear) is a disproportionate addition, and the server still refuses a non-player Swear target the same way retail's own disabled button would have silently no-op'd. | A lit, clickable Swear button when a non-player object is selected — clicking it sends a Swear request the SERVER refuses. Not a wire-behavior gap (the swear/target end state is identical to retail's disabled-button no-op) — a UI-affordance divergence a screenshot comparison would catch. | `gmAllegianceUI::UpdateSwearButton @0x004908E0` (lane C §1.3 step 1); `ACCWeenieObject::IsPlayer` (unlocated exact VA, same as AD-83) | +| AD-85 | **Filed 2026-08-12 at Campaign FA slice FA5.** The SAME unported `StringInfo` variable-substitution engine AD-81 filed for the Fellowship page's row/stats text (`StringInfo::InqString` → `StringTableMetaLanguage::UnescapeString`) also affects the Allegiance page's numeric fields and its three LOCAL confirmation dialogs: (1) **Numeric fields** — self/monarch followers (`0x10000252`/`0x10000258`), self rank (`0x10000253`), and the "experience passed up" text (`0x10000492` ×2, the vassal row's `0x10000269`) render as bare numbers with no surrounding retail words, same disposition as AD-81's `"{level} {pct}%"`. (2) **Swear/Break/Kick confirmation dialogs** — `MakeSwearConfirmationDialog`/`MakeBreakConfirmationDialog`/`MakeKickConfirmationDialog` bind the target's name into a `StringInfo` template (`ID_Allegiance_SwearConfirmation`/`BreakConfirmation`/`KickConfirmation`, lane C §5.1); this controller shows the RESOLVED retail template text VERBATIM (unsubstituted — the raw DAT string, whatever it says) when resolution succeeds, and falls back to the bare target name (also non-invented) when it does not. Neither path ever composes new English. (3) **The server-driven "accept incoming swear" dialog** (`ConfirmationType` 1, handled generically by `GameplayConfirmationController` — no allegiance-specific code) inherits the SAME gap from the wire side: ACE sends the target's bare `Name` as the ENTIRE confirmation message (lane C §6.4: `Player_Allegiance.cs:91`/`ConfirmationManager.cs:38`), not a formatted sentence — retail's own client wraps it via the identical `StringInfo` mechanism this row already covers. | `src/AcDream.App/UI/Layout/SocialAllegiancePageController.cs` (`RefreshSelfBlock`, `RefreshMonarchBlock`, `RefreshPatronBlock`, `UpdateRow`, `OnSwearClick`, `OnBreakClick`, `OnKickClick`) | Same argument as AD-81: porting `StringTableMetaLanguage` is a cross-cutting prerequisite, not allegiance-specific, and guessing its token/placeholder syntax to blend a name into an unverified sentence shape would risk silently-wrong substitution — exactly the guessing CLAUDE.md's workflow forbids. Using retail's own unsubstituted text verbatim (or the bare name) is the non-inventing alternative. | A user sees bare numbers instead of retail's full sentences for followers/rank/XP-passed-up, and the three local confirmation dialogs (plus the server-driven accept-swear dialog) show either an unsubstituted retail template or a bare name instead of "Swear allegiance to PlayerName?"-style prose. The underlying DATA (the numbers, the target's real name) is correct in every case — only the surrounding words are absent. | `gmAllegianceUI::UpdatePlayerData @0x00491330`, `UpdateMonarchData @0x00491B40`, `UpdatePatronData @0x004917C0`, `UpdateVassalsData @0x00492340` (lane C/A field sources); `MakeSwearConfirmationDialog @0x004927B0` family (lane A §5.1); `StringInfo::InqString @0x0042e490` (unresolved, same citation as AD-81) | +| AD-86 | **Filed 2026-08-12 at Campaign FA slice FA5, item 4.** ACE deliberately zeroes or empties seven `AllegianceProfile`/`AllegianceData` fields on the wire — officers, officer titles, MOTD, MOTD-set-by, name-last-set-time, lock state, and approved vassal are always empty/false/zero regardless of the allegiance's real state; `timeOnline`/`allegianceAge` are hard-coded 0 forever (lane C §5.1). acdream's FA1 parser reads all of these (to keep the byte cursor aligned for the fields after them) but drops most at increasing layers: `AllegianceMemberRecord` never surfaces `timeOnline`/`allegianceAge` as fields at all; `RuntimeAllegianceState.ApplyUpdate` (FA2) does not forward `Motd`/`MotdSetBy`/`ChatRoomId`/`NameLastSetTime`/`IsLocked`/`ApprovedVassal` from the parsed `AllegianceUpdate` record to `RuntimeAllegianceSnapshot` even though the C# record itself carries them; retail's own `gmAllegianceUI` (FA5) has no widget for any of the seven either (lane A §3.3: "No allegiance MOTD / officer / ban / hometown UI" — they are chat-verb-only in the 2013 client, out of this campaign's scope per the plan's §4). | `src/AcDream.Core.Net/Messages/ClientCommandResponses.cs` (`ReadAllegianceProfileBody`, `AllegianceMemberRecord`, `AllegianceUpdate`); `src/AcDream.Runtime/Gameplay/RuntimeAllegianceState.cs` (`ApplyUpdate`) | Retail's own client renders nothing for these seven fields either (no panel widget consumes them) — dropping them past the parse layer matches retail's OWN presentation exactly, and is strictly safer than surfacing values that are always wrong/empty against ACE. | Any FUTURE consumer (the chat-verb-only officer/MOTD/lock/ban management features, §2 master table features #11-31 of the allegiance wire research, explicitly out of Campaign FA's scope) that reads these fields off the Runtime layer will find them permanently zero/empty against ACE regardless of the allegiance's real server-side state — do not chase this as a parser bug; it is ACE's own zeroing. | ACE `Network/Structure/AllegianceHierarchy.cs:53-56,62-64,74-75,78-83,86-89,153-155` (broadcast counters/isLocked/officers/officerTitles/motd/approvedVassal); ACE `Network/Structure/AllegianceData.cs:59-60,86-89,111-112` (timeOnline/allegianceAge); `docs/research/2026-08-11-fa-allegiance-wire.md` §5.1 | --- diff --git a/docs/plans/2026-08-11-fellowship-allegiance-campaign.md b/docs/plans/2026-08-11-fellowship-allegiance-campaign.md index 7984d1fa..1a74c854 100644 --- a/docs/plans/2026-08-11-fellowship-allegiance-campaign.md +++ b/docs/plans/2026-08-11-fellowship-allegiance-campaign.md @@ -29,6 +29,15 @@ 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.** +**FA5 CODE-COMPLETE 2026-08-12 (`7ed79eaf` code, this commit register/ +gate-script/ledger) — implementer pass done: CF-1's corrected `0x001F` +subscription at retail's three arming points, the SF-7 per-relationship +monarch/patron gate, the vassal roster, and swear/break/kick with their +confirmations. The dual-lens review (§7) and the user's connected gate +(`docs/research/2026-08-12-campaign-fa-test-script.md` §FA5, several +steps `[TWO-CLIENT]`, deferrable to FA6) are OWED; the live-mount probe +extension has not been run against real DATs in this worktree (none +installed here).** **Goal:** retail's social panel — the four-tab `gmPanelUI` member at host slot `0x1000018F` (panel id **12**): **Friends / Allegiance / Fellowship / @@ -302,6 +311,6 @@ before anything builds on them. | 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 | **CODE-CLOSED 2026-08-12, connected gate OWED** — both dual-lens reviews' items applied (5 MUST-FIX + 9 SHOULD-FIX + 4 NIT mechanism; 1 SHOULD-FIX blast), narrow re-review CLOSED with ONE REOPEN (MF-3 0x00A6 placed pre-world), coordinator re-fix `04161def` (latch-advances-only-on-Accepted + RedeclareAfterWorldEntry wired to the post-world EnteredWorld seam; RED-verified regression pins), re-review of the re-fix CLOSED `06dbf1cf` (seam ordering traced: RestoreLayout fires after _inWorld=true + command activation). Final full suite 13,286/4/0. The user's connected gate (several steps `[TWO-CLIENT]`, deferrable to FA6) remains owed. | Original: `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); `38f08314` (register rows AD-80/AD-81 + AD-78 addendum, gate script §FA4, ledger). **Fix round** (`docs/research/2026-08-12-fa4-review-mechanism.md` + `-blast.md`): `290f9b58` (MUST-FIX 2 — delete `TryAutoRespondToFellowshipInvite`; type-4 dialog test); `5499f058` (MUST-FIX 1 D5 truncation + MUST-FIX 4 world→panel selection sync + SF-1/SF-2/SF-3/N-0/N-1/N-2/N-3, all in `SocialFellowshipPageController.cs`); `df000306` (MUST-FIX 3 panel-level 0x00A6 reconnect re-arm + SF-4 `Dispose` unsubscribe, `SocialPanelController.cs`); `300d8189` (D6/D7/SF-8 dimming reversal — `IgnoreFellowshipRequests`/`FellowshipAutoAcceptRequests`/`FellowshipShareLoot` revert to `StoreOnly`, only `FellowshipShareXP` stays `Live`); `55b17e15` (SF-6 live-mount-probe assertions); `1d743277` (register AD-82/AD-83 + AD-78 count correction, gate-script SF-7/MUST-FIX-1/3/4/2 corrections, plan D7 SF-8 addendum); this commit (ledger). | owed | automated: Release build green throughout; full solution suite green at every commit, both original and fix round. **Original baseline 13,238/4/0 (13,242 total) → FA4-original 13,272/4/0 (13,276 total), +34 tests.** **Fix-round delta: +13 tests, 0 deletions** (the D6 intercept had no dedicated tests to remove) — `SocialFellowshipPageControllerTests.cs` +8 (2 new `[InlineData]` cases on the existing D5 theory for 6/8-fellow truncation, +6 new `[Fact]`: member-leaves/selection-clear, row-build-failure-doesn't-retry, Recruit-reads-membership, world-selects-fellow, world-selects-non-fellow-keeps-selection, optimistic-caption); `SocialPanelControllerTests.cs` +4 (`[Fact]`: D4 panel-level conjunction, reconnect re-arms, reconnect stays silent when not open, Dispose unsubscribes); `GameplayConfirmationControllerTests.cs` +1 (`[Fact]`: type-4 dialog verbatim + accept). `CharacterOptionsPageControllerTests.cs` (dimming set content changed, count 31/19 → 34/16, net 0 new tests) and `SocialPanelLiveMountProbeTests.cs` (assertions added inside its existing env-gated `[Fact]`, net 0 new tests) extended in place. **Final: 13,285 passed / 4 skipped / 0 failed (13,289 total), directly measured** (13,272 + 13 = 13,285; 4 skips unchanged; arithmetic exact). Per-finding disposition: MUST-FIX 1/2/3/4/5 fixed; mechanism SHOULD-FIX 1-9 and NIT N-0/N-1/N-2/N-3 all applied; blast SHOULD-FIX 1 (AD-78 stale count) fixed. Dimmed-row count: 35 (pre-FA4) → 31 (FA4-original, incorrect) → **34 of 50 (fix-round final, correct)** / 16 live — net ONE row (`FellowshipShareXP`) un-dimmed from the pre-FA4 baseline, not four.** `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. **[FIX-ROUND CORRECTION: item (4)'s "inline comment, not a register row" disposition was itself the wrong call under the register rule (MUST-FIX 5) — filed as register row AD-83 in the fix round.]** | -| FA5 | — | | | | +| FA5 | **CODE-COMPLETE 2026-08-12 — implementer pass done, dual-lens review (§7) and the user's connected gate OWED.** The allegiance page is fully live against FA1's parser and FA2's already-shipped `RuntimeAllegianceState`/`IRuntimeAllegianceCommands` (no Runtime-layer changes needed this slice — FA2 had already built `SetUpdateSubscription`, `Swear`, `Break`, `Kick`, and the full `IRuntimeAllegianceView` accessor set). CF-1's corrected `0x001F` subscription is wired at all three retail arming points (`Bind`'s PostInit attempt, the post-world `EnteredWorld` seam via `RedeclareAfterWorldEntry` — UNCONDITIONAL, explicitly NOT edge-triggered, to avoid repeating FA4's MF-3-REOPEN bug class — and the visible-branch toggle via `SetPageVisible`, folded into `SocialPanelController`'s existing window-shown+active-tab conjunction). Monarch/patron/self blocks implement fix-round SF-7's per-relationship gate (fresh decompile of `UpdatePlayerData`/`UpdateMonarchData`/`UpdatePatronData` pinned every field source: `0x10000251` is the ALLEGIANCE's own name not the viewer's, followers are `TotalVassals`/`TotalMembers-1` straight off the wire, `0x10000492`'s doubled instances are the viewer's own `CpTithed`). The vassal roster reuses FA4's `FlushPreservingScroll` diff pattern in the wire's already-reversed order. Swear/Break/Kick each open a local confirmation dialog before sending (Swear targets the WORLD selection via the same `ClientObjectTable` name resolver `ToolbarRuntimeBindings.ResolveName` uses; Break targets the current patron; Kick targets the panel-local selected vassal row, no world-selection sync, lane A §6.2); the server-driven "accept incoming swear" (`ConfirmationType` 1) needed no new code since `GameplayConfirmationController` already handles every type generically — a new test verifies this explicitly rather than assuming FA4's blast review's claim. Four new register rows: AD-84 (Swear's missing IsPlayer gate, mirrors AD-83), AD-85 (the unported `StringInfo` gap extended to Allegiance's numeric fields + three confirmation dialogs, mirrors AD-81), AD-86 (ACE's seven zeroed profile fields, dropped past acdream's own parse layer to match retail's own no-widget presentation), and an addendum bracket on AD-82 (the vassal-row click-target limitation recurs, but NOT its invented tint colors or its Fellowship-only world-sync). | `7ed79eaf` (code: Runtime command wrapper (`InteractionUiRuntimeSources.cs`), `SocialRuntimeBindings`/`MountSocialPanel` widening (`RetailUiRuntime.cs`), the Allegiance projection delegates (`InteractionRetainedUiComposition.cs`), `SocialPanelController`'s CF-1 wiring, the full `SocialAllegiancePageController.cs` rewrite, plus 10 new `SocialPanelControllerTests.cs` tests, 1 new `GameplayConfirmationControllerTests.cs` type-1 test, and the `SocialPanelLiveMountProbeTests.cs` production-mount extension); this commit (register rows AD-84/AD-85/AD-86 + AD-82 addendum, gate-script §FA5, ledger) | owed | automated: Release build green throughout; full solution suite green. **Baseline 13,285/4/0 (13,289 total, FA4's own ledger figure) → FA5 13,296/4/0 (13,300 total), directly measured, +11 tests net** (`SocialPanelControllerTests.cs` nets +9: the old coarse-gate `Allegiance_HasProfile_ShowsBothBlocks` is REMOVED and replaced by 4 SF-7 per-relationship tests, 1 roster-population test, 3 swear/break/kick wiring tests, and 2 CF-1 subscription-arming tests — 10 added, 1 removed; `GameplayConfirmationControllerTests.cs` +1 the type-1 verification test; `SocialPanelLiveMountProbeTests.cs` extended in place inside its existing single env-gated `[Fact]`, net 0 new tests). **Live-DAT verification NOT run this slice** — this worktree has no `Documents\Asheron's Call` installed, so the extended `SocialPanelLiveMountProbeTests.ProbeLiveMountShapes` (scoped `0x10000492` dual-resolution, the vassal row template, the checkbox, the five confirmation/label strings, and a full production `SocialAllegiancePageController.Bind()` pass) compiles and is logically sound against the already-verified FA3 fixture element ids, but has not been exercised against the live installed DATs; owed alongside the user's connected gate. **Contradictions/deferrals:** (1) the self-rank field (`0x10000253`) retail-sources from a LIVE buffed-quality query (`CBaseQualities::InqInt(qualities, 0x1e)`, i.e. `PropertyInt.AllegianceRank`) plus a 20-table title lookup (`AllegianceData::GetTitle`) neither of which this controller has a seam for — substitutes the numerically-equivalent `RuntimeAllegianceSnapshot.Rank` from the same `0x0020` message, rendered bare (documented in the class doc, not a separate register row since it is a data-source substitution rather than a presentation gap); (2) "your follower count" (`0x10000252`) was NOT explicitly formula-cited in the panel-structure research doc — a targeted fresh decompile of `UpdatePlayerData` (`pseudo_c:157629`) confirmed `_total_vassals` directly, resolving the ambiguity from primary source rather than inferring it. | `docs/research/2026-08-12-campaign-fa-test-script.md` §FA5 (new, mirrors §FA4's structure: CF-1 subscription steps, the SF-7 per-relationship steps, vassal-list steps, swear/break/kick with their confirmations, `[TWO-CLIENT]` tags deferrable to FA6, ACE-zeroed-field honesty, full "what to report"/"explicitly not in scope" lists) | | 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 8ce7662b..6fd4724d 100644 --- a/docs/research/2026-08-12-campaign-fa-test-script.md +++ b/docs/research/2026-08-12-campaign-fa-test-script.md @@ -1,6 +1,6 @@ # Campaign FA connected-gate test script -**Status: FA3 owes its connected gate; FA4 (below) owes its own.** Launch +**Status: FA3, FA4, and FA5 (below) all owe their connected gates.** 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 @@ -513,3 +513,240 @@ not a client bug, and is out of scope for this gate either way.]** 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. + +--- + +## FA5 — the Allegiance page fully live + +**Prerequisite:** open the Allegiance tab (F3, or click it — it is the +authored DEFAULT tab, so simply pressing F3 from a cold start lands here). +All step numbers below restart at 1 for this section's own numbering. + +**Honest limitation up front, same shape as FA4's:** swearing allegiance +genuinely needs a SECOND character (you cannot swear to yourself, and +retail requires the target to already be online and selected in the +world). If a second ACE account/character is not available, 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. + +### The data subscription (CF-1) — profile populates on SHOW, not on +`@allegiance info` — [SOLO] + +**[FA2 re-review CF-1 correction, carried into this slice's own contract: +the panel's data comes from `0x001F AllegianceUpdateRequest` → `0x0020 +AllegianceUpdate`, NOT from the `0x027B`/`0x027C` `@allegiance info` +chat-command pair, which is text-only and does not feed the panel (FA2 +MF-2's fix). Do not use `@allegiance info` as a trigger for anything in +this section.]** + +1. **With NO allegiance (a fresh character), open the Allegiance tab.** + The self line (your name/rank/follower count) renders at the top; + both the monarch and patron blocks are HIDDEN (no allegiance means + neither relationship exists) — the empty state FA3 already gated, now + reachable live. +2. **If you already have an allegiance (monarch and/or patron), the + blocks should populate WITHIN A SECOND OR TWO of opening the tab** — + this is the `0x001F(1)` → `0x0020` round trip. If the blocks stay + empty indefinitely after opening the tab, that is the CF-1 subscribe + failing to reach the server — a real bug, not a display gap. +3. **Close the panel (or switch to Fellowship), then reopen the + Allegiance tab.** The data should still be there (or repopulate + quickly) — closing/reopening exercises the visible-branch toggle + (`0x001F(0)` on hide, `0x001F(1)` on show) without losing the profile. +4. **[reconnect, the FA4 MF-3-REOPEN analogue] Force a disconnect/ + reconnect while ANY allegiance data was previously visible.** Once + the client re-enters the world on the new session, open (or confirm + already-open) the Allegiance tab. EXPECTED: the profile repopulates + on the new session within a second or two — this is + `RedeclareAfterWorldEntry`'s UNCONDITIONAL post-world `0x001F(1)` + resend, which fires regardless of whether the panel happens to be + open at that exact moment (retail's own `RecvNotice_PlayerDescReceived` + arm does not check panel visibility either — see the class doc's CF-1 + section). BUG if the profile stays empty/stale for the rest of the new + session — that would mean the exact bug class MF-3-REOPEN caught for + Fellowship's `0x00A6` recurred here for `0x001F`. + +### Monarch / patron / self blocks — [SOLO if you already have an +allegiance, otherwise needs a TWO-CLIENT swear first] + +5. **Your own line (top of the page) always shows your allegiance's + name, your follower count, and your rank as plain numbers** — retail's + exact sentence wording is NOT reproduced (the unported `StringInfo` + substitution engine, register row AD-85, same class as FA4's AD-81); + do not report "it just shows a number, not a sentence" as a bug. +6. **If you are a VASSAL (you have a patron who is not the monarch)**, + the patron block shows their name and a follower-adjacent + "experience passed up" number. If your patron IS the monarch + (single-tier allegiance), the patron block should be HIDDEN and the + monarch block should show an ADDITIONAL sub-line (the + "experience passed up" number) that does not appear for a + multi-tier allegiance — this is retail's `PatronSlashMonarchLabel` + swap (fix-round SF-7, the coarser FA3 gate's own owed correction). +7. **If you ARE the monarch of your allegiance**, the monarch block + should be HIDDEN (retail hides it when the monarch IS the viewer, + not just when there is no monarch at all) — confirm this specifically + if you can arrange to be a monarch; it is easy to miss since "no + monarch block" looks identical whether you have no allegiance at all + or you're sitting at the top of one. +8. **A logged-out monarch or patron's block should render visually + dimmed** (their name still shows, but the block itself looks + "greyed"/disabled) — retail's `SetState(1)`/`SetState(0xD)` per-block + online/offline toggle. This needs a second client to log out while + you're watching, so mark it **[TWO-CLIENT]** if you can't arrange it + solo. + +### Vassal list — [TWO-CLIENT for a populated list; SOLO to confirm the +empty case] + +9. **With no vassals, the vassal list box is simply empty** (no + placeholder text, matching retail — there is no "you have no vassals" + frame the way Fellowship has an empty-state frame swap). +10. **With one or more vassals (needs a second character sworn to you — + see the swear steps below), each row shows the vassal's name and an + "experience passed up" number** (their OWN tithed amount, not + yours — distinct from the monarch/patron block's number, which is + YOUR OWN tithed amount). +11. **An OFFLINE vassal's row should visually distinguish itself** + (acdream tints the name grey rather than porting retail's dedicated + offline-marker element visually — confirm SOME visible difference + between an online and offline vassal row). +12. **With several vassals, scroll the list if it overflows the visible + area** and confirm every row remains reachable via the scrollbar + (same `UiTemplateListBox` mechanism FA3/FA4 already gated). +13. **Sibling order — a SOFT/informational check, not a fail-the-gate + item.** If you can arrange three or more vassals sworn to the same + patron in a KNOWN order, the list should render them in REVERSE + swear order (the last one to swear appears FIRST) — this is retail's + own tree-assembly rule (lane C §4.4 point 3), not a display bug. Hard + to verify without careful setup; report what you observe but do not + treat a mismatch as an automatic fail without double-checking the + actual swear order first. + +### Swear — [TWO-CLIENT] + +14. **On the SECOND client (the would-be vassal, with NO current + patron), select the FIRST client's character in the world** (click + their 3D model) and click **Swear** on the Allegiance page. + EXPECTED: a confirmation dialog appears on the SECOND client asking + to confirm swearing allegiance — the message is whatever retail's + template resolves to VERBATIM, or the target's bare name if the + template did not resolve (register row AD-85 — do not report "the + dialog doesn't read like a full sentence" as a bug). +15. **Accept the dialog on the second client.** EXPECTED: the FIRST + client (the new patron) receives ITS OWN confirmation dialog asking + to accept the new vassal — this is the SERVER-DRIVEN + `ConfirmationType` 1 round trip (`GameplayConfirmationController`, + generic, no allegiance-specific code), showing ACE's own message + (typically just the vassal's bare name, lane C §6.4 — again, not a + full sentence; not a bug). +16. **Accept the dialog on the first client too.** Both clients' Allegiance + pages should update: the second character now shows a patron block + naming the first character; the first character now shows the second + character as a new row in their vassal list. +17. **Swear button enable rule** — with NO current patron and a player + selected in the world who is not already in your allegiance, Swear + should be ENABLED; with a patron already sworn, or nothing selected, + or yourself selected, it should be DISABLED. **[register row AD-84]** + acdream does not check "is the selection a player" the way retail + does — selecting a non-player object (a monster, an item) may leave + Swear lit when retail would grey it out; the server refuses the send + either way, so this is a cosmetic gap, not a wire-behavior bug — do + not fail the gate over it, but do note it if you observe it. +18. **Reject the swear-target dialog (step 14) instead of accepting.** No + dialog reaches the first client; nothing changes on either side. +19. **Reject the accept-swear dialog (step 15) instead of accepting.** The + second character does NOT become a vassal; both clients' allegiance + state stays as it was before step 14. + +### Break and Kick — [TWO-CLIENT] (needs an existing swear relationship +from the steps above, or a pre-existing allegiance) + +20. **As the VASSAL, click Break.** A confirmation dialog appears asking + to break allegiance FROM your CURRENT patron (target latched at + click time, matching retail — changing your world selection after + clicking Break should NOT change who you break from, since Break's + target is your patron, not a world selection at all). Accept it: + your patron block clears/hides; the patron's vassal list loses your + row. +21. **As the PATRON, select a vassal row (click their name in the vassal + list — NOT the world) and click Kick.** A confirmation dialog + appears asking to kick that SPECIFIC vassal. Accept it: they are + removed from your vassal list; their own client's patron block + clears/hides. +22. **Kick button enable rule** — with no vassal row selected, Kick + should be DISABLED; select any vassal row and it should ENABLE. +23. **Break button enable rule** — with no patron, Break should be + DISABLED; with any patron, it should be ENABLED. +24. **Reject either dialog** (Break or Kick) instead of accepting — no + change on either client, no crash or stuck dialog state. + +### The `IgnoreAllegianceRequests` checkbox — [SOLO] + +25. **The checkbox on this page and the SAME-named row on the Options → + Character tab are the SAME value** — toggling one should reflect on + the other (open both surfaces side by side, or toggle-then-reopen to + confirm). This bit is a pure server-side filter with no client-side + consumer in retail either (lane C §1.6) — checking it does not change + any OTHER client-visible behavior; do not report "checking it doesn't + seem to do anything" as a bug, that is correct. + +### ACE-zeroed fields — honest, not a bug + +26. **You will never see officer names, a message-of-the-day, a lock + indicator, or an "allegiance age" anywhere on this page.** Retail's + OWN `gmAllegianceUI` has no widgets for any of these either (they are + chat-verb-only in the 2013 client — `@allegiance officer`, `@allegiance + motd`, etc., all out of this campaign's scope) — do not report their + absence as a bug. Separately, even the CHAT-VERB versions of these + features (if you try them) will show blank/default values against + the currently-targeted ACE build, because ACE deliberately zeroes + these fields on the wire regardless of the allegiance's real state + (register row AD-86) — that is an ACE-vs-retail divergence, not an + acdream bug either way. + +### What to report (FA5-specific — in addition to the FA3/FA4 lists above) + +- The Allegiance tab NOT being the panel's default (step 1 of §FA3 already + covers the tab itself; this section is about its DATA) — the profile + never populating after opening the tab (step 2), or staying stale + after a reconnect (step 4). +- The monarch block staying VISIBLE while you are the monarch yourself + (step 7), or the patron block staying visible while your patron is the + monarch (step 6) — both are the exact SF-7 per-relationship gate this + slice fixed; a coarse "both blocks show whenever I have ANY profile" + regression would be the FA3 bug resurfacing. +- The monarch/patron "experience passed up" sub-line failing to appear + under the correct block per step 6's single-tier-vs-multi-tier + distinction. +- A vassal row not appearing at all after a successful swear (step 16), + or an online/offline vassal being visually indistinguishable (step 11). +- Swear/Break/Kick sending the WRONG target guid — e.g. Break targeting + something other than your actual current patron, or Kick targeting a + vassal other than the one you clicked. +- Either confirmation-dialog direction of Swear (steps 14/15) not + appearing, or accepting one but not sending the matching wire command. +- The `IgnoreAllegianceRequests` checkbox NOT staying in sync between + this page and the Options → Character tab (step 25). +- Any crash, hang, or exception in the log during swear/break/kick, + during a reconnect with the Allegiance tab open, or while a + confirmation dialog is open. + +### Explicitly NOT in scope for this gate (FA5) + +- The bot-vs-ACE two-session swear 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 sentences for followers/rank/ + experience-passed-up and the three confirmation dialogs — acdream + renders plain numbers and, for dialogs, either retail's unsubstituted + template text or the bare target name (register row AD-85); do not + report "it just shows a number" or "the dialog doesn't read like a + full sentence" as bugs. +- Officer/MOTD/lock/ban/hometown management — chat-verb-only in retail, + out of this campaign's scope entirely (§4 of the plan), and unreliable + against the currently-targeted ACE build regardless (register row + AD-86). +- Swear's missing "target is a player" enable-rule check (register row + AD-84) — a cosmetic superset-of-retail gap, not a wire-behavior bug.