# Campaign FA slice FA1 — MECHANISM-FAITHFULNESS review **Date:** 2026-08-12. **Lens:** mechanism faithfulness (is every byte and every rule right?). **Mode:** read-only; this doc is the only file written. **Target:** `7be86f47` (builders), `6bedbc47` (parsers), `5f9aa16f` (WeenieError), `4281750b` (delete `AllegianceTree`), ledger `ee1124ca`. **Contract:** `docs/plans/2026-08-11-fellowship-allegiance-campaign.md` slice FA1 + D3/D5/D6/D9, with `docs/research/2026-08-11-fa-fellowship-wire.md` (lane B) and `docs/research/2026-08-11-fa-allegiance-wire.md` (lane C) as ground truth. **Primary sources re-read independently for this review** (not taken on the lane docs' word): `docs/research/named-retail/acclient_2013_pseudo_c.txt` (`Fellow::UnPack @0x005B9AD0`, `Fellowship::UnPack @0x005B94F0`, `PackableHashTable::UnPack @0x00569A90`, `CM_Fellowship::DispatchUI_UpdateFellow @0x006A6700`, `CM_Fellowship::Event_Create @0x006A67A0`, `AllegianceHierarchy::UnPack @0x005B7520`, `AllegianceHierarchy::Add @0x005B6E90`, `AllegianceProfile::UnPack @0x005B6B90`, the `0x20` dispatch case at `0055b5dd`, `ClientCommunicationSystem::HandleFailureEvent @0x00571990`), `docs/research/named-retail/acclient.h:2979-2994` (`AllegianceVersion`), `docs/research/named-retail/symbols.json`, and the ACE writers `references/ACE/Source/ACE.Server/Network/Structure/AllegianceHierarchy.cs`, `.../AllegianceData.cs`, `.../AllegianceProfile.cs`, `.../GameEvent/Events/GameEventAllegianceUpdate.cs`. --- ## VERDICT: **APPROVE-WITH-FIXES** Every wire layout in this slice is byte-correct against the retail decompilation, and both premise-contradiction calls are **independently verified correct from primary source** (§2). The fixes below are two rule/coverage gaps and a set of smaller fidelity items; none of them invalidates a landed byte vector, and all of them are unreachable against ACE today — which is exactly why they need pinning now rather than discovering them against a non-ACE server or a hand-built FA2 fixture. **MUST-FIX: 2. SHOULD-FIX: 5. Nits (non-blocking): 5.** --- ## 1. MUST-FIX ### MF-1 — `AllegianceHierarchy::Add`'s FOURTH rejection rule (`_id == 0`) is not modeled `src/AcDream.Core.Net/Messages/ClientCommandResponses.cs:501-524` models three of the four rejection rules in `AllegianceHierarchy::Add @0x005B6E90`. The whole function body in retail is wrapped in `if (_id != 0)`: ``` 005b6e96 uint32_t _id = arg3->_id; 005b6e9e if (_id != 0) <-- every success path is inside this ... 005b6ecd return 0; <-- _id == 0 falls straight out ``` and the caller (`005b779d`) turns any `Add` failure into `return 0` from `UnPack`, i.e. the whole message is discarded. Lane C §4.4's own pseudo-code lists it first (`if (data._id == 0) return 0; // reject`). acdream reads the monarch at `:505` and each record at `:512` with no `CharacterId != 0` check, so: * a profile whose **monarch** record has id 0 is accepted where retail discards it, and * every subsequent record naming `treeParent = 0` is then admitted by `knownIds.Contains(0)` — where retail rejects `treeParent == 0` unconditionally (`005b6ee5`: `esi = (arg2 != 0) ? Search(...) : nullptr`, then `esi != 0` is required). One guard fixes both: reject the whole message when any record's `CharacterId == 0`. Add the boundary test beside the existing three at `tests/AcDream.Core.Net.Tests/Messages/AllegianceProfileVersionGateTests.cs:312-353`. Unreachable against ACE (`AllegianceData.cs:69` always writes a real guid) — the same "correct only because ACE is generous" class lane C §0 warns about for the version gates, which this slice otherwise closed. ### MF-2 — `0x001F AllegianceUpdateRequest` builder is missing `src/AcDream.Core.Net/Messages/AllegianceRequests.cs:34-83` ships Swear (`0x001D`), Break (`0x001E`) and the new Kick alias — and nothing else. There is no `0x001F` builder anywhere in the tree (repo-wide grep for `0x001F` / `AllegianceUpdateRequest` over `src/**/*.cs` returns only `PropertyBool.AllegianceUpdateRequest`, an unrelated property id). `0x001F` is the allegiance panel's subscribe/unsubscribe toggle — the exact structural twin of the fellowship `0x00A6` this slice *did* repair and rename. Lane C §1.2 pins all four retail send sites (`gmAllegianceUI::PostInit` tail `@0x004911C6` → 1, `RecvNotice_PlayerDescReceived @0x00490D59` → 1, `OnVisibilityChanged` visible `@0x004912DD` → 1, hidden `@0x00491311` → 0) and lane C §7.1 numbers it #3 of the six messages the minimum-viable panel needs, with the explicit instruction "ACE ignores the value but retail servers do not; send it correctly." FA1 is the Core.Net truth slice and FA2 (Runtime owners + typed commands) lands before FA5; leaving this out pushes a `AcDream.Core.Net` builder addition into a Runtime or panel slice. Six lines plus a golden vector (`[u32 0xF7B1][u32 seq][u32 0x1F][u32 on]`, total `0x10` per lane C §3.2). --- ## 2. The two premise-contradiction calls — BOTH VERIFIED CORRECT I re-derived both from primary source rather than accepting the commit messages. Both stand. ### (a) The 8 fellowship `WeenieError` ids genuinely have no retail text — `5f9aa16f` is right, D9's premise was wrong D9 asserted "the 8 missing fellowship WeenieError strings are added in FA1 … all resolve from the DAT string tables, never invented." The implementer refused and documented absence instead. Verified: 1. **Case-label walk.** In `HandleFailureEvent @0x00571990` (`acclient_2013_pseudo_c.txt:382616`+) the label sequence is `case 0x413: → case 0x414: → case 0x416: → case 0x41d: → case 0x41e: → case 0x41f:` — **no label for `0x417`–`0x41C`** — and `case 0x4d9: → case 0x4da: → case 0x4dd:` — **no label for `0x4DB`/ `0x4DC`**. 2. **The `else if` chain too.** `WeenieErrorMessages`' own class doc warns that five ids dispatch via `else if (arg2 == N)` above the switch, so a case-label-only sweep would be unsound. The function contains exactly seven such comparisons — `0x43`, `0x3f7`, `0x3e`, `0x23`, `0x36`, `0x4e8`, `0x48a`, `0x408`, `0x3ee`, `0x4f` — none of them ours. 3. **Whole-file sweep.** `\b0x417\b` … `\b0x41c\b`, `\b0x4db\b`, `\b0x4dc\b` across all 1.4 M lines return only unrelated hits: struct field offsets in three `*(uint16_t*)(ecx+0x418)` copy loops, and `[0x417]`/`[0x418]`/… array-index headers inside a `MeshBuffer` render-batch data dump at `.data 0x008872xx` / `0x008c4axx`. Nothing textual, nothing comparative. 4. **Decimal forms.** No `1047`–`1052`/`1243`/`1244` anywhere in the function's address range. 5. **No `default:`** in the switch, so an unmatched id produces no text at all — which is precisely what `Resolve()` already returns. Conclusion: acdream's existing silence for `0x0417 FellowshipIgnoringRequests` and `0x04DB FellowshipDeclined` (the two ACE actually sends) is retail-faithful. Adding invented English would have been the exact failure mode the table's no-default-case rule exists to prevent. The conformance `[Theory]` at `tests/AcDream.Core.Tests/Chat/WeenieErrorMessagesTests.cs:390-393` is the right artifact. **No fix owed; no register row owed** (silence *is* the retail behavior, so there is no deviation to record). ### (b) `0x0275` is client-authored — a receive parser was never the ask, and D6's need is satisfied Verified: `0x0275` is a C→S GameAction sub-opcode (`CM_Character::Event_ConfirmationResponse @0x006A1210`, buffer `0x18`; consumed server-side by ACE's `GameActionConfirmationResponse`). Retail never receives it. So "the missing third message of the triple" (D6) could not have meant a parser. `ClientCommandRequests.BuildConfirmationResponse` already existed pre-FA1 byte-for-byte (`git show 6bedbc47~1` — 24-byte body, `[env][seq][0x0275][type][context][accepted]`, matching retail's `0x18` allocation exactly). What was genuinely absent was the **typed discriminator**, and `GameEvents.ConfirmationType` (`src/AcDream.Core.Net/Messages/GameEvents.cs:552-561`) supplies it with all seven values matching retail's `ClientUISystem::Handle_Character__ConfirmationRequest @0x005640A0` switch (symbol verified at that exact address) and ACE's enum. **D6's FA4/FA5 flows are buildable on this today**: receive `0x0274` via `ParseCharacterConfirmationRequest` (`:512`, `Type` as `uint` — cast to the new enum), send via `BuildConfirmationResponse((uint)ConfirmationType.Fellowship, …)`, receive `0x0276` via `ParseCharacterConfirmationDone` (`:533`). The reinterpretation is sound. --- ## 3. SHOULD-FIX ### SF-1 — the monarch's `MayPassupExperience` must be forced false `AllegianceHierarchy::UnPack`'s last act before returning success is `AllegianceData::SetMayPassupExperience(&m_pMonarch->_data, 0)` (`acclient_2013_pseudo_c.txt:454670`, `005b77b3`) — the monarch can never pass up, whatever the wire bit says. Lane C §4.1 names this in the same paragraph as the `HasPackedLevel`-absent rule that `ClientCommandResponses.cs:564-565` *did* port; only one of the pair landed. The field is public (`AllegianceMemberRecord.MayPassupExperience`, `:201`), so FA5 could render it wrong. Clear it on the monarch record at `:505-506`. Inert against ACE (`AllegianceData.cs:76` never sets the bit for a monarch), but it becomes live the moment the record is hand-built or a non-ACE server is used. ### SF-2 — `ParseFellowshipDisband` rejects a non-empty body; retail never does `src/AcDream.Core.Net/Messages/GameEvents.cs:749`: `ParseFellowshipDisband(payload) => payload.Length == 0`. Retail's `CM_Fellowship::DispatchUI_Disband @0x006A5E80` reads **only** the opcode and then calls the handler — it never inspects or validates a trailing byte. Encoding a length check retail does not perform means any future non-empty body (an ACE change, a trailing pad) silently swallows the disband and leaves FA2's roster stuck in a fellowship the server already destroyed. The faithful contract is unconditional acceptance. The matching test at `tests/AcDream.Core.Net.Tests/Messages/FellowshipEventsTests.cs:197-201` pins the unfaithful half and should flip with it. ### SF-3 — D5's `<<1` shape is not pinned at the `0x02C0` site D5 requires the raw-uint `shareLoot` read at **both** message sites. The mechanism is right — `ReadFellow` (`GameEvents.cs:777`, `shareLoot` at `:786`) is shared by `ParseFellowshipFullUpdate` (`:661`) and `ParseFellowshipUpdateFellow` (`:706`), and `FellowMember.ShareLoot` (`:622`) is a `uint` with no `== 1` comparison anywhere in the tree. But the tests pin `0x10` and `2` only at `0x02BE` (`FellowshipEventsTests.cs:70,95`); the `0x02C0` fixture writes `shareLoot = 0` (`:154`). Add the `<<1`-shape (`2`) assertion at the `0x02C0` site so a future refactor that splits `ReadFellow` cannot silently reintroduce a bool there. Carry-forward for the FA2 reviewer: `!= 0` semantics are currently only *documented* (`:606-609`) — nothing consumes `ShareLoot` yet. The consuming comparison lands in FA2 and must be checked there. ### SF-4 — AP-90's register row now cites a deleted class `docs/architecture/retail-divergence-register.md:298` (AP-90) reads "… and its `AllegianceTree` is not wired into GameWindow". `4281750b` deleted that class. The row itself is still valid (radar relationship state is still not delivered), but the prose must be re-pointed at `ClientCommandResponses.AllegianceProfileLookups` / the FA2 owner. The register rule says the bookkeeping rides the same commit; this one slipped. ### SF-5 — the "eleven gates" numbering in the code comments tops out at ten, and one lane citation does not exist Two documentation-accuracy items that will cost a future auditor time: * `ClientCommandResponses.cs:411-491` labels the gates "1/2, 3, 4, 5, 6, 7, 8, 9, 10". The contract, the commit message and lane C all say **eleven**. All eleven *fields* are gated correctly (verified §4 below) — only the comment numbering is off, and it is the exact artifact someone would use to answer "are all eleven present?". * `SocialActions.cs:184` cites "lane B §4.5, §12". Lane B has no §12 — the intended reference is master-table **row 12** (`Panel visibility → update request`). --- ## 4. Verified clean (re-derived, not taken on trust) ### 4.1 Golden byte vectors — all six independently re-derived, all correct | Builder | Expected bytes (hand-derived from the cited retail sender) | Test | Verdict | |---|---|---|---| | `BuildFellowshipCreate(seq 7,"Team",true)` | `B1F70000 07000000 A2000000` + str16L`0400 54 65 61 6D 0000` + `01000000` = 24 B | `SocialActionsTests.cs:65-83` | ✔ | | `BuildFellowshipCreate(seq 1,"X",false)` | 12 + 4 (str16L `0100 58 00`) + 4 = 20 B, tail `00000000` | `:85-95` | ✔ | | `BuildFellowshipUpdateRequest(9,true)` | `…A6000000 01000000` | `:130-146` | ✔ | | `BuildFellowshipAssignNewLeader(2,0x50001234)` | `…90020000 34120050` | `:151-168` | ✔ | | `BuildFellowshipChangeOpenness(4,false)` | `…91020000 00000000` | `:173-190` | ✔ | | `BuildSwear/BuildBreak/BuildKick` | `…1D000000 AAAA0000` / `…1E000000 BBBB0000` / `…1E000000 42000050` | `AllegianceRequestsTests.cs:39-90` | ✔ | The `0x00A2` repair is confirmed at the strongest possible provenance: `CM_Fellowship::Event_Create @0x006A67A0` allocates `strPackSize + 0x10`, stores `0xa2` at `006a6809`, packs the string at `006a6828`, then `*(uint32_t*)var_c = arg2` at `006a6835` — **one trailing u32 shareXP and no openness field**. The prior two-byte `openness`/`shareXp` shape (and the test that pinned it) was exactly the inversion lane B §5.2(a) described. `PackString16L` (`SocialActions.cs:389-403`) is Windows-1252 with the pad computed over `2 + length` — matching retail's `PStringBase::Pack`, ACE's `WriteString16L`, and the reader `ReadString16L` (`GameEvents.cs:913-926`). No endianness, padding, or encoding slip anywhere in the set. Byte-write-vs-u32-write equivalence (`body[12] = 0|1` into a zeroed 16-byte buffer) is byte-identical to retail's full-word store for these 0/1 payloads — see nit N-2. ### 4.2 `Fellow` record layout (the load-bearing one) `Fellow::UnPack @0x005B9AD0` reads, in order: `_cp_cache`, `_lum_cache`, `_level`, `_max_health`, `_max_stamina`, `_max_mana`, `_current_health`, `_current_stamina`, `_current_mana`, `_share_loot`, then `str16L _name` (min size `0x28`). `GameEvents.ReadFellow` (`:777-793`) is field-for-field identical. ✔ ### 4.3 `0x02BE` field order and the hash-table header `Fellowship::UnPack @0x005B94F0`: `_fellowship_table` → `_name` → `_leader` → `_share_xp` → `_even_xp_split` → `_open_fellow` → `_locked` → `_fellows_departed`, then returns — it never reads field 9 (locks). `ParseFellowshipFullUpdate` (`GameEvents.cs:637-681`) matches exactly, **including** the deliberate non-parse of field 9, which is the retail-faithful choice (lane B §3.9 suggested reading it; retail does not, and lane B's own U3 flags the struct width as unverified — the implementer's stop-and-document call is correct and needs no register row). Header: `PackableHashTable::UnPack @0x00569A90` reads one u32 and splits `_table_size = v >> 16`, `count = v & 0xFFFF` — so the wire is `[u16 count][u16 numBuckets]` LE, which is what the parser reads for both the member table (`:653-654`) and the departed table (`:670-671`). ✔ ### 4.4 `0x02C0` is guid-first — the Chorizite disagreement independently re-resolved `CM_Fellowship::DispatchUI_UpdateFellow @0x006A6700`: ``` 006a6726 cond = *(uint32_t*)eax_3 == 0x2c0; // opcode at +0 006a673b uint32_t edi = *(uint32_t*)(eax_3 + 4); // MEMBER GUID at +4 006a6763 Fellow::UnPack(&var_30, &arg2, …); // Fellow from +8 006a676c uint32_t ecx_4 = *(uint32_t*)eax_7; // updateType last ``` Retail puts the guid first. `ParseFellowshipUpdateFellow` (`GameEvents.cs:700-712`) reads guid → `Fellow` → `updateType`. ✔ The lane-B three-source resolution (retail + ACE + holtburger over Chorizite's generated shape) is confirmed at its retail end. ### 4.5 The eleven version gates — every threshold and the ORDER Re-derived from `AllegianceHierarchy::UnPack @0x005B7520` and the `AllegianceVersion` enum (`acclient.h:2979-2994`). Order on the wire is **not** monotonic in version — officer titles (v9) precede the pools block (v2) — and the implementation follows the wire, not the numbering: | # | Field | Retail predicate (address) | `ClientCommandResponses.cs` | ✔ | |---|---|---|---|---| | 1 | officers `PHashTable` | `m_oldVersion >= 6` (`005b7570`/`005b7585`) | `:416-425` | ✔ | | 2 | legacy spokesperson 4-byte skip | `1 <= v < 6` (`005b759b`) | `:426-429` | ✔ | | 3 | officer titles `PSmartArray` (bare i32 count) | `>= OfficersTitlesAdded(9)` (`005b75b4`) | `:434-439` | ✔ | | 4–7 | monarch/spokes broadcast time+today ×4 | `>= PoolsAdded(2)` (`005b75e0`) | `:442-448` | ✔ | | 8 | `m_motd`, `m_motdSetBy` | `>= MotdAdded(3)` (`005b7645`) | `:453-457` | ✔ | | 9 | `m_chatRoomID` | `>= ChatRoomIDAdded(4)` (`005b7696`) | `:461-462` | ✔ | | 10 | `m_BindPoint` (32-byte Position) | `>= Bindstones(7)` (`005b76b2`) | `:468-472` | ✔ | | 11 | `m_AllegianceName` + `m_NameLastSetTime` | `>= AllegianceName(8)` (`005b76d1`) | `:477-481` | ✔ | | 12 | `m_isLocked` | `>= LockedState(10)` (`005b770d`) | `:485-486` | ✔ | | 13 | `m_ApprovedVassal` | `>= ApprovedVassal(11)` (`005b7728`) | `:490-491` | ✔ | `BannedCharactersAdded(5)` gates nothing — confirmed, and pinned by the negative test `VersionGate_4to5_BannedCharactersAddedGatesNothing`. **Boundary tests sit on the boundaries.** Every gate has an `N-1`/`N` fixture pair: 0→1, 1→2, 2→3, 3→4, 4→5 (negative), 5→6, 6→7, 7→8, 8→9, 9→10, 10→11 (`AllegianceProfileVersionGateTests.cs:152-308`). For the gates whose fields are not surfaced (pools, bind point, officers, titles) the assertion is the monarch record surviving intact — which IS a real structural proof here, because the fixture writer (`BuildProfileWire`, `:40-134`) applies the gate conditions independently of the parser: a wrong threshold would consume or skip bytes the writer did not emit and garble every field after it. Cross-checked against ACE's writer (`AllegianceHierarchy.cs:140-155`): identical field order, and `oldVersion = 0x000B` always (`:50`) — so these gates are inert against ACE and the live `@allegiance info` path is unchanged, exactly as intended. ### 4.6 `AllegianceData` field order and `AllegianceProfile`/`0x0020` framing `AllegianceData` per lane C §4.1, cross-checked against ACE `AllegianceData.cs:95-117`: id, cpCached, cpTithed, bitfield, u8 gender, u8 heritage, u16 rank, [u32 level if `HasPackedLevel`], u16 loyalty, u16 leadership, [8 bytes either branch], str16L name. `ReadAllegianceData` (`:532-571`) matches. ✔ `AllegianceProfile::UnPack @0x005B6B90` = `totalMembers`, `totalVassals`, hierarchy — matches `:406-409`. ✔ The `0x20` dispatch case (`0055b5dd`-`0055b607`) reads one u32 and then `AllegianceProfile::UnPack`, and ACE's `GameEventAllegianceUpdate.cs:22-25` writes `rank` then the profile — so `ParseAllegianceUpdate`'s rank-leading + shared-body split (`:375-391`) is right, as is the guid-leading `0x027C` sibling. Sharing one `ReadAllegianceProfileBody` is exactly lane C §7.2's reuse verdict honored. ✔ ### 4.7 Tree rules — three of four correct, and the reversal test pins ORDER Orphan / self-parent / duplicate all match `Add`'s semantics: * orphan — retail `esi = (arg2 != 0) ? Search(arg2, monarch) : nullptr; if (esi == 0) return 0` ≡ `!knownIds.Contains(parentGuid)` (`:514`); * self-parent — retail `esi->_data._id != _id` required, and `esi` was found *by* `treeParentId`, so it is `treeParentId == _id` ≡ `parentGuid == record.CharacterId` (`:515`); * duplicate — retail `Search(_id, monarch) == 0` required ≡ `knownIds.Contains(record.CharacterId)` (`:516`); * all three `return null`, i.e. whole-message discard, matching `UnPack`'s `return 0` at `005b779d`. ✔ The record loop's first-record-has-no-`treeParent` shape is confirmed at `005b776e` (`if (ebp_1 != 0) { read treeParent }`) and matches `:505-512`. ✔ (The fourth rule is MF-1.) **Sibling reversal — the test pins the ORDER, not just membership.** `FindVassals` (`:288-294`) walks the record list backwards, which is the correct model of retail's push-front insert (`node->_peer = parent->_vassal; parent->_vassal = node;` at `005b6f15`-`005b6f38`) followed by a `GetFirstVassal`/`GetNextVassal` walk. `TreeAssembly_SiblingOrder_ReversesOnAssembly` (`AllegianceProfileVersionGateTests.cs:371-396`) asserts the exact sequence `{"VassalC","VassalB","VassalA"}` against wire order A,B,C. ✔ Note this changes the live `@allegiance info` vassal render order (it was forward before FA1) — the new order is the retail one; lane C §9 U3 flags a side-by-side eyeball as the closing check. ### 4.8 Dead opcodes `0x01C9`/`0x01CA` `ParseFellowshipFellowUpdateDone` / `…StatsDone` (`GameEvents.cs:759-775`) return a non-nullable record and cannot fail: empty payload → `RawValue = null`, trailing u32 → captured. Both cases are tested (`FellowshipEventsTests.cs:205-233`). Nothing user-visible is possible — neither is registered in `GameEventWiring.WireAll` (grep for `Fellowship` there returns nothing), and `RawValue` has no consumer. Matches lane B §2.7's parse-and-ignore instruction and its BN-zero-fold caution (payload tolerated, not assumed empty). ✔ ### 4.9 Retail anchors in code comments — spot-check (10, not 5) Resolved against `symbols.json` (exact-address match unless noted): | Anchor cited in code | Result | |---|---| | `gmFellowshipUI::CreateFellowship @0x0048F730` | exact ✔ | | `gmFellowshipUI::OnVisibilityChanged @0x0048E460` | exact ✔ | | `Event_AssignNewLeader @0x006A5F70` | exact (`CM_Fellowship::`) ✔ | | `Event_ChangeFellowOpeness @0x006A6040` | exact (`CM_Fellowship::`) ✔ | | `AssignLeadershipToFellow @0x0048F1E0` | exact ✔ | | `Fellow::Pack @0x005B9A10` / `UnPack @0x005B9AD0` | exact ✔ | | `AllegianceHierarchy::Add @0x005B6E90` | exact ✔ | | `Handle_Character__ConfirmationRequest @0x005640A0` | exact (`ClientUISystem::`) ✔ | | `gmAllegianceUI::CloseKickConfirmationDialog @0x00490B00` | exact ✔ | | `gmFellowshipUI` Quit button case 8 `@0x0049034B`; `Event_BreakAllegiance @0x006A69DA` | instruction-level, correctly inside `gmFellowshipUI::ListenToElementMessage @0x004901C0` and `CM_Allegiance::Event_BreakAllegiance @0x006A6980` ✔ | No misattributed anchor found. ### 4.10 Other contract items * All **11 unwired S→C parsers** are present (fellowship `0x00A3`, `0x00A4`, `0x01C9`, `0x01CA`, `0x02BE`, `0x02BF`, `0x02C0`; allegiance `0x0003`, `0x0020`, `0x01C8`, `0x027A`), and all 11 ids exist in `GameEventType`. ✔ * `AllegianceTree` deletion is clean — no production or test reference survives (the one `AllegianceTree` hit in `ChatChannelInfoTests.cs:28` is an unrelated test-method name). Lane C §7.3's DELETE verdict honored. ✔ (See SF-4 for the register prose.) * Test fixtures are generated by `AceWireWriter`, a line-for-line mirror of ACE's `Extensions.cs` writer — provenance is the server's own algorithm, not hand-typed hex. ✔ * No stale callers of the renamed/re-signatured builders remain. ✔ --- ## 5. Nits (non-blocking; listed for completeness, no action required) * **N-1** — Retail degrades *gracefully* on a short legacy blob (each gated read is additionally guarded by "≥4 bytes remain", e.g. `005b759b`, `005b75fc`); acdream throws `FormatException` and discards the message instead. Only reachable on a truncated pre-v11 blob, which ACE cannot produce. * **N-2** — `BuildFellowshipQuit`/`BuildFellowshipUpdateRequest`/ `BuildFellowshipChangeOpenness` write a single byte where retail writes a full u32. Byte-identical for the 0/1 payloads these carry (zeroed buffer), and blessed by lane B §5.2 — noted only so nobody "fixes" it into a behavior change. * **N-3** — Retail's record count is a **signed** `int16_t` (`005b7546`: `int16_t i = ecx;` … `if (i > 0)`), so a header with the high bit set reads as zero records in retail and as up to 32 767 records in acdream. Bounded and unreachable against ACE. * **N-4** — `GameEvents.ParseConfirmationResponse` (`:581`) is by construction unreachable in production (nothing receives `0x0275`); its only consumer is `ConfirmationTripleTests`. Justified in its own doc comment as a round-trip conformance mechanism, and it is the cheapest way to prove the builder against a second implementation — but it is new never-called code in a slice whose D3 rule is "connected, repaired, or deleted — never trusted", so FA2 should either route it or leave the doc comment intact as the standing justification. * **N-5** — `4281750b`'s commit message repeats lane C §0's "~1000×" figure for the deleted `AllegianceMath.ComputePassup`. The actual error is roughly 40× at maxed loyalty and *under*-states at low loyalty (`(50 + 22.5·291)/291 = 22.67` vs a true passup factor of ≈`0.53`). The DELETE verdict is unaffected and correct; the magnitude claim originates in the lane doc, not the implementer. --- ## 6. Note on evidence I could not produce The slice's automated gate (Release build + full suite 13,149/4/0) is the regression reviewer's lane. I did not build. The Debug test binaries in this worktree predate FA1 (a `--no-build --filter` run over the six FA1 test classes matched only the 16 pre-existing `SocialActionsTests`/ `AllegianceRequestsTests` cases), so per the stale-build-artifact rule I treat that run as evidence of nothing and reviewed every new test by reading it instead. All test *logic* above is verified by inspection against the primary sources cited. --- # 7. Re-review of the fix round — 2026-08-12 **Fix commits:** `ed308087` (code + tests), `511ba6e5` (docs). ## VERDICT: **CLOSED** — all 7 findings (2 MUST-FIX, 5 SHOULD-FIX) verified fixed. No REOPEN. One **carry-forward** for FA2's start (CF-1, §7.3) — not a reopen of any finding, and not a defect in the fix round: it is residual staleness the blast doc's own MF-2 enumeration did not reach. ## 7.1 Disposition of my findings | # | Finding | Disposition | Evidence | |---|---|---|---| | MF-1 | `Add`'s `_id == 0` rule unmodeled | **CLOSED** | `ClientCommandResponses.cs:509-510` rejects a zero-id monarch before it is assigned; `:533` adds `record.CharacterId == 0u` to the child-record guard. Both `return null` = whole-message discard, matching `005b6e9e`→`005b6ecd`. `treeParent == 0` is now provably fatal for every non-monarch record: `knownIds` seeds from a monarch id that cannot be 0, so `Contains(0)` is always false. Three boundary tests added (`AllegianceProfileVersionGateTests.cs:355-405`: zero-id monarch, zero-id child, zero treeParent), each asserting `null`. | | MF-2 | `0x001F AllegianceUpdateRequest` builder missing | **CLOSED** | `AllegianceRequests.cs:40-53` (`AllegianceUpdateRequestOpcode = 0x001Fu`) + `:93-102` (`BuildAllegianceUpdateRequest`). Re-derived from primary source: `CM_Allegiance::Event_UpdateRequest @0x006A7260` allocates `0x10`, stores `0x1f` at `006a72ba` (the cited anchor is the opcode store itself), and writes `arg1` as a full u32 at `006a72cb` — the builder's `[0xF7B1][seq][0x1F][on]` is byte-exact. Two golden vectors (on/off) at `AllegianceRequestsTests.cs:92-127`, both hand-derived and correct. All five anchors in the new doc comment resolve: `0x006A7260` exact; `0x004911C6` / `0x00490D59` / `0x004912DD` / `0x00491311` inside `gmAllegianceUI::PostInit`, `::RecvNotice_PlayerDescReceived`, and `::OnVisibilityChanged` respectively. The ACE claim is accurate — `GameActionAllegianceUpdateRequest.cs:12` reads the u32 and replies unconditionally. | | SF-1 | Monarch `MayPassupExperience` not force-cleared | **CLOSED** | `ClientCommandResponses.cs:544-553`: `monarch = monarch.Value with { MayPassupExperience = false }`, placed after the record loop and inside `if (recordCount > 0)` — the same position and guard as retail's `005b77a7`/`005b77b3` (`if (m_pMonarch != 0) SetMayPassupExperience(…, 0)`), and unreachable from the `return null` paths exactly as in retail. The fixture relocation is correct **and necessary**: the old legacy-compat test sat on the monarch, where the new clear would make "fallback fired" indistinguishable from "clear fired". It now carries a two-record wire (monarch bitfield `0xC`, vassal bitfield `0x4`) and asserts both halves separately; the decisive new test feeds the monarch bitfield `0x1C` (the wire bit explicitly SET) and still asserts `false`. | | SF-2 | `ParseFellowshipDisband` validated a body length retail never inspects | **CLOSED** | `GameEvents.cs:777` is now `=> true`, with the rationale in its doc comment. Test flipped: `ParseFellowshipDisband_NonEmptyBody_StillReturnsTrue` (`FellowshipEventsTests.cs:224-232`). | | SF-3 | D5 `<<1` shape unpinned at `0x02C0` | **CLOSED** | `ParseFellowshipUpdateFellow_ShareLootIsRawNotBool_D5` (`FellowshipEventsTests.cs:169-195`) writes `shareLoot = 2` on the `0x02C0` leg and asserts `== 2u` / `!= 1u`, mirroring the `0x02BE` pin. | | SF-4 | AP-90 cites the deleted `AllegianceTree` | **CLOSED** (shared with blast MF-1 — see §7.2) | | | SF-5 | Gate comment numbering stopped at ten; bogus "lane B §12" citation | **CLOSED, and better than asked** | `ClientCommandResponses.cs:411-424` renumbers to the **actual `AllegianceVersion` enum values** rather than wire-appearance order, and states explicitly that the wire order is a different sequence. I re-checked all ten labels against `acclient.h:2979-2994`: SpokespersonAdded 1, PoolsAdded 2, MotdAdded 3, ChatRoomIDAdded 4, MultipleAllegianceOfficersAdded 6, Bindstones 7, AllegianceName 8, OfficersTitlesAdded 9, LockedState 10, ApprovedVassal 11 — every label now matches its enum member. Gate 5 (BannedCharactersAdded) is called out by name as real-but-gating-nothing, which is what made the old numbering appear to lose a gate. `SocialActions.cs:184-185` now cites "master-table row 12". | No path I flagged was fixed by suppression, early return, or a narrowing guard; each is the retail mechanism ported at the site retail performs it. ## 7.2 Spot-verification of the blast doc's six dispositions | Item | Verdict | |---|---| | **MF-1 / AP-90 re-point** | ✔ `retail-divergence-register.md:298` re-points the evidence column at `ClientCommandResponses.cs` (`AllegianceProfileLookups`) and `GameEvents.cs` (fellowship parsers), keeps the deviation **active** rather than retiring it (correct — no live owner exists until FA2), and dates the edit. `ClientCommandResponses.cs:250-295` is a valid range: the class opens at `:250` and `FindVassals` is at `:288`. | | **MF-2 / four seam-map corrections** | ✔ all four applied as dated strike-through + addendum rather than silent rewrite (the right call for a committed research record): `:791` allegiance-owner "wrapping `AllegianceTree`" and its `AllegianceTree.cs:56-162` precedent both struck and re-pointed; `:666` `SetOpen → BuildFellowshipUpdate` struck and re-pointed at `BuildFellowshipChangeOpenness` (`0x0291`) with the semantic-bug warning; the 4-arg `BuildFellowshipCreate` corrected in **both** places (`:429` dimming discussion and `:668` bot-command table); open question 8 struck and answered. I verified question 8's answer independently — `PackString16L` pads the record to a 4-byte multiple *including* its own u16 length, so `12 + name.Length` is already aligned before the trailing u32, and ACE's `BinaryReaderExtensions.ReadString16L:47` skips the identical `CalculatePadMultiple(2 + length, 4)`. The answer is correct and the question is properly closed. Cited line numbers `SocialActions.cs:144-155`, `:222-230`, `:389-403` all resolve. | | **MF-3 / D9 + slice-row annotation** | ✔ D9 (`:103`) and the FA1 slice-map row (`:130`) both struck and annotated with the shipped finding, each pointing at §2(a) of this doc and at the conformance `[Theory]`. The annotation correctly states that no register row is owed (acdream matches retail, so there is no deviation to record). The slice row also picks up `0x001F` in its opcode list. | | **MF-4 / ledger arithmetic** | ✔ reconciled and now internally closed: 13,153 total = 4+916+1559+15+130+119+4856+877+4677 (I summed it: 13,153), minus 4 skips = **13,149 passed**, matching the prior figure; the "+50" framing is replaced with the diff-verified **+58 / −9 = +49**, and the one-test residual is attributed to the §5 baseline being captured at a different point in history rather than papered over. That is an honest reconciliation, not a re-transcription. | | **SF-1 / two live-command pins** | ✔ `ClientCommandResponsesTests.cs:317-390`: a three-vassal test through `FormatAllegianceInfoLines` pinning the exact reversed print order (`VassalC / VassalB / VassalA`) — the pre-existing full-tree test had one vassal and could not distinguish direction — and a `GameEventWiring`-layer test proving a malformed tree now prints nothing (`Assert.Empty(chat.Snapshot())`). Both pin the *observable* change at the surface a user sees. | | **SF-4 / doc-comment fix + SF-5 discriminator note** | ✔ `GameEvents.cs:596` now cites the real `ConfirmationTripleTests`. The SF-5 split is cross-referenced at all three sites (`:506-512` request, `:536-539` done, `:561-573` the enum) and states the decision FA4 inherits rather than leaving an unexplained inconsistency. | ## 7.3 Carry-forward for FA2 (not a reopen) **CF-1 — lane D still names the deleted `AllegianceTree` in four more places, two of them forward-looking.** The fix round applied blast MF-2's enumeration exactly; the enumeration itself stopped at four rows. A repo-wide sweep finds four further hits in `docs/research/2026-08-11-fa-acdream-seams.md`: * `:128` — the FA2 owner-graph diagram: "`AllegianceOwner : RuntimeAllegianceState (new) └── owns the ported AcDream.Core.Allegiance.AllegianceTree`". * `:214` — "`AllegianceTree` already lives in `AcDream.Core.Allegiance` — **so the allegiance tree can be passed directly as a state parameter, exactly like `friends`**". Both are **design guidance FA2 will follow**, i.e. the same danger class as the `:791` row that *was* corrected — an FA2 implementer reading either is told to own or pass a class that no longer exists. (`:23` and `:890` are the other two hits; both are historical "what exists today" inventory prose and are fine left alone, as is every surviving `BuildFellowshipUpdate` mention in lane B and in the plan's D3 — those correctly describe the pre-FA1 defect and must NOT be rewritten.) This is the single-lens-enumeration failure mode the project's own `feedback_blast_radius_single_lens` note describes: the sweep leaked along the axis it did not traverse. One more dated addendum at each of `:128` and `:214` closes it; FA2 should not start before that. ## 7.4 Suite claim — consistent, and corroborated by direct measurement The `13,158 passed / 4 skipped / 0 failed` claim is **arithmetically consistent and independently corroborated**, though I did not run the full solution suite myself. * **Mechanical test-count delta.** `ed308087`'s test diff adds exactly **9** `[Fact]`/`[Theory]` attributes, removes **0**, and adds **0** new `InlineData` rows. 13,149 + 9 = **13,158**; 13,153 + 9 = **13,162** total. Both stated figures close. * **The ledger's own +9 attribution is correct**: 2 (`0x001F` golden vectors) + 1 (D5 `<<1` at `0x02C0`) + 4 (MF-1 ×3 and SF-1's monarch test) + 2 (blast SF-1 live-surface pins) = 9, and that matches the files line by line. * **Direct measurement.** The Release binaries in this worktree are stamped 00:47:19–00:47:20, **after** `ed308087` (00:46:42), so `--no-build` is legitimate evidence here (unlike the stale Debug binaries noted in §6). `AcDream.Core.Net.Tests` now runs **886 passed / 0 failed / 0 skipped** — exactly the blast doc's measured pre-fix anchor of 877 plus 9, and all 9 new tests live in that project. A narrower FA1-family filter (the six FA1 test classes plus `ClientCommandResponsesTests`) runs **92 passed / 0 failed**. That is as far as this lens can take it: every number in the ledger row now reconciles with every other, and the one project that changed measures exactly the claimed delta. Whether the *other eight* projects still total what the row says is the regression lens's call.