From 022b1844e1912832cfb69d5aece99c1e85b52e97 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 12 Aug 2026 10:24:08 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20FA6=20=E2=80=94=20file=20#384=20(allegi?= =?UTF-8?q?ance=20swear=20ACE=20non-response)=20+=20register=20AD-87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/ISSUES.md #384 records the live-run evidence trail (six connected runs, the 0.005 m distance diagnostic, the confirmation-arrival diagnostic that never fires) behind AllegianceGateEnabled=false. docs/architecture/retail-divergence-register.md AD-87 records the honest divergence this deferral creates: the allegiance half of the FA6 bot gate is written and wired but unverified end-to-end over the wire, unlike the fellowship half which is proven live. Co-Authored-By: Claude Fable 5 --- docs/ISSUES.md | 73 +++++++++++++++++++ .../retail-divergence-register.md | 1 + 2 files changed, 74 insertions(+) diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 98cdc408..b8ae4ef2 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -24,6 +24,79 @@ What does NOT go here: - Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending. - Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed. +## #384 — FA6 allegiance-swear bot gate: ACE returns no response to 0x001D swear (no confirmation/0x0020/error) + +**Status:** OPEN — filed 2026-08-12 at Campaign FA slice FA6. The two-bot +headless fellowship/allegiance connected gate +(`src/AcDream.Headless/Policies/HeadlessBotPolicy.cs`, +`FellowshipAllegianceLeaderBotPolicy`/`FellowshipAllegianceRecruitBotPolicy`) +ran live against local ACE (`127.0.0.1:9000`, `testaccount`/`+Acdream` as +Leader, `testaccount2`/`+Horan` as Recruit) six times across the session. + +**Fellowship half: PASSES live, three separate runs, and is the shipped +automated gate.** The decisive cross-session assertion — the RECRUIT bot's +own `RuntimeFellowshipState` (a separate process's canonical Runtime owner, +not the Leader's local echo) flipping `IsInFellowship=true`, +`MemberCount=2`, `LeaderGuid=0x5000000A` — passed identically in runs 1, 3, +5, and 6. Proximity via retail's admin `@teleallto` plus +`RuntimeFriendlyTargetQuery.FindPlayerByName` (added this slice) reliably +resolves the Recruit bot's guid even with a third, unrelated character +online on the same shared ACE dev instance (`+Je`, `0x50000001` — see the +`FindPlayerByName` doc comment and its conformance tests for that finding). + +**Allegiance half: BLOCKED, disabled by default +(`AllegianceGateEnabled = false` in both policy classes).** After the +fellowship establishes, the Recruit bot sends `Event_SwearAllegiance` +(`0x001D`) with the Leader's guid as `targetGuid`. Per +`docs/research/2026-08-11-fa-allegiance-wire.md` §1.3, retail's server +should then send the Leader (the would-be patron) a generic +`Character.ConfirmationRequest` (`0x0274`, `ConfirmationType=1` +`ALLEGIANCE_SWEAR_CONFIRM`), which the Leader answers with +`Event_ConfirmationResponse` (`0x0275`) before ACE forms the allegiance and +broadcasts `0x0020` to both. **Live evidence (run6, with a distance +diagnostic and a confirmation-arrival diagnostic both added for this +investigation and kept permanently in the code):** + +- `[fa6-diag] distance self(0x5000000B)->patron(0x5000000A) = 0.005 m` — + the two bots were essentially coincident at the moment of swear, ruling + out retail's server-side 2.0 m swear-distance gate as the cause. +- `HeadlessSessionHost`'s `OnConfirmationRequest` (wired this slice — it + was `null` pre-FA6, so headless bots dropped every confirmation + regardless) never fires: no `[fa6-diag] OnConfirmationRequest received` + line ever appears after the swear is sent, in any of runs 1, 3, 4, 5, or + 6 (run 2 targeted the wrong player entirely, see the `FindPlayerByName` + history above, and is not evidence either way). +- No `[weenie-error]` line appears after the swear either (the two + `0x051D` lines present in every run's log are pre-existing noise already + noted in the OP7 gate result, unrelated and present before any FA6 + action fires). + +**ACE returns absolutely nothing** — no confirmation, no tree update, no +error — to a `0x001D` sent at 0.005 m. This is ambiguous between (a) a +defect in acdream's own `0x001D` wire builder (`AllegianceRequests. +BuildSwear`) that ACE silently can't parse, (b) an ACE-side rule this +specific test pair trips that this campaign's research didn't surface +(GM-flagged accounts, a self/rank/loyalty precondition, a `+`-prefixed +test-character exclusion), or (c) a genuine drop somewhere in the +session's send path. Disambiguating (a) from (b)/(c) needs visibility this +automated harness doesn't have — either an ACE server-side console/log, or +a WireMCP capture correlated tightly enough to confirm bytes actually left +the process (the two capture attempts this session used the wrong +interface/tooling and were inconclusive). + +**Not investigated further this session per user direction** — the +fellowship half is the proven, shipped automated gate; the allegiance half +is deferred to the user's own connected gate (manual swear via the +graphical client between two characters), which will settle whether the +symptom reproduces outside the headless harness at all. + +**To re-enable:** flip `AllegianceGateEnabled` to `true` in both +`FellowshipAllegianceLeaderBotPolicy` and +`FellowshipAllegianceRecruitBotPolicy` — every allegiance stage (Leader's +`WaitForVassal`, Recruit's `Swear`/`WaitSwornSeed`/`Break`/ +`WaitBrokenSeed`) is still fully written and wired, just unreachable while +the flag is off. + ## #383 — Installed-DAT vs committed-fixture drift: regeneration produces large diffs in existing UI fixtures **Status:** OPEN — filed 2026-08-12 at Campaign FA slice FA3. Running the diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index 22bf4240..8116927c 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -181,6 +181,7 @@ readiness/requeue adaptation. See | 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 NINE `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` (the remaining two) 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 | +| AD-87 | **Filed 2026-08-12 at Campaign FA slice FA6.** The allegiance-swear half of the two-bot headless connected gate (`FellowshipAllegianceLeaderBotPolicy`/`FellowshipAllegianceRecruitBotPolicy`) is written and wired end-to-end (proximity, `0x001D` swear, the confirmation-relay seam, `0x0020` tree-reseed assertions, break, reconnect-idempotence) but has never actually been verified to complete over the wire — `AllegianceGateEnabled = false` in both classes keeps it unreachable by default. Six live runs against local ACE all reproduced the same result: the fellowship half passes decisively (the Recruit bot's own `RuntimeFellowshipState` flips, proven three separate times), but ACE returns nothing at all to the `0x001D` swear (no `0x0274` confirmation, no `0x0020`, no error) even at 0.005 m separation — see docs/ISSUES.md #384 for the full evidence trail. So while the FELLOWSHIP two-session machinery is proven live, the ALLEGIANCE two-session machinery (Runtime commands, wire builders, `RuntimeAllegianceState` reseed) remains unverified end-to-end over a real connection — only its unit/fixture-level tests and its (successful) LOCAL echo on the swearer's own client are exercised. | `src/AcDream.Headless/Policies/HeadlessBotPolicy.cs` (`FellowshipAllegianceLeaderBotPolicy.AllegianceGateEnabled`, `FellowshipAllegianceRecruitBotPolicy.AllegianceGateEnabled`, both `false`) | Shipping the fellowship gate ALONE (rather than blocking the whole slice on the allegiance blocker) matches the campaign's own D8/item-6 split — fellowship and allegiance are independent retail systems with independent wire families, and the fellowship half's proof stands on its own regardless of the allegiance outcome. Disabling rather than deleting the allegiance code keeps a reviewed-quality, ready-to-run harness in place for whoever closes #384. | Anyone reading "the FA6 bot-vs-ACE gate passed" without the qualifier could assume the allegiance swear/break/reconnect path is proven over the wire when it is not — only its LOCAL send-and-echo behavior is proven; ACE's actual acceptance of the swear is the open question #384 tracks. | docs/ISSUES.md #384; `docs/research/2026-08-11-fa-allegiance-wire.md` §1.3 (the expected `0x0274`/`0x0275`/`0x0020` handshake); run6 evidence (0.005 m distance, zero inbound after swear) | ---