Narrow re-review of the fix round (ed308087code+tests,511ba6e5docs) against the mechanism findings doc. Verdict CLOSED, no REOPEN. All 2 MUST-FIX and 5 SHOULD-FIX verified in the actual diffs, each re-derived rather than taken on the commit message's word: - MF-1: zero-id rejection on BOTH the monarch and child paths, which is also what makes treeParent == 0 provably fatal (knownIds can never contain 0); three boundary tests. - MF-2: the 0x001F builder re-verified against primary source -- CM_Allegiance::Event_UpdateRequest @0x006A7260 allocates 0x10, stores 0x1f at 006a72ba, writes the arg as a full u32 at 006a72cb. Both golden vectors correct; all five new anchors resolve; the ACE claim (GameActionAllegianceUpdateRequest.cs:12 reads and ignores the value) is accurate. - SF-1: monarch clear placed at retail's own position/guard; the fixture relocation onto a vassal is not just correct but necessary, since the clear would otherwise mask the legacy-compat fallback. - SF-2/SF-3/SF-5 all closed; SF-5 resolved better than asked, renumbering to the real AllegianceVersion enum values (verified against acclient.h:2979-2994) and naming gate 5 as real-but-gating-nothing. Spot-verified all six blast dispositions: AP-90 re-pointed without being wrongly retired; four seam-map corrections applied as dated strikes (its open-question-8 answer independently re-verified against PackString16L and ACE's ReadString16L pad skip); D9 + slice row struck and annotated; ledger arithmetic now closes (13,153 total sums correctly, -4 skips = 13,149). Suite claim corroborated: the fix diff adds exactly +9 [Fact]/[Theory] and removes 0, and the post-fix Release binaries (stamped aftered308087, so --no-build is legitimate here) measure AcDream.Core.Net.Tests at 886/0/0 -- exactly the blast doc's 877 pre-fix anchor plus 9, with all 9 new tests in that project. One carry-forward, NOT a reopen: blast MF-2's enumeration stopped at four rows; lane D still names the deleted AllegianceTree at :128 and :214, both forward-looking FA2 design guidance of the same danger class as the :791 row that was corrected. Two more dated addenda close it; FA2 should not start before that. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
594 lines
36 KiB
Markdown
594 lines
36 KiB
Markdown
# 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<unsigned long,Fellow>::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<unsigned long,Fellow>::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<u32,u32>` | `m_oldVersion >= 6` (`005b7570`/`005b7585`) | `:416-425` | ✔ |
|
||
| 2 | legacy spokesperson 4-byte skip | `1 <= v < 6` (`005b759b`) | `:426-429` | ✔ |
|
||
| 3 | officer titles `PSmartArray<PString>` (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.
|