FA5 mechanism-faithfulness review of 7ed79eaf/bc29a1db/7e394cbf. Verdict APPROVE-WITH-FIXES. Every high-stakes claim re-derived from the PDB-paired 2013 decomp: CF-1's unconditional 0x001F post-world arm (00490d59 sits OUTSIDE the busy-count guard), the monarch/patron/self field sources (UpdatePlayerData/UpdateMonarchData/UpdatePatronData), the SF-7 per-relationship gate, swear=world-selection/no-SetSelectedObject, and the AD-86 ACE-zeroed-field citations all match retail. MANDATORY live-mount probe RAN and PASSED against the real installed DATs (1/1) — the scoped doubled-0x10000492 NotSame assertion and a full production Bind() with zero "not found" held. FA5 unit suite 36/36 green. One SHOULD-FIX (LOW): FA5 greys the offline vassal NAME (OfflineNameColor) — retail's UpdateVassalsData @004924c3 sets the name with no color; the offline cue is exclusively the authored 0x100004AA marker toggle. Either drop OfflineNameColor or honestly register it (the AD-82 addendum's "covered by the marker" framing understates it). Does not block the gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
18 KiB
Campaign FA slice FA5 — mechanism-faithfulness review
Reviewer lens: mechanism-faithfulness (retail-fidelity), read-only.
Date: 2026-08-12. Target commits: 7ed79eaf (code+tests),
bc29a1db (register/gate-script/ledger), 7e394cbf (NUL-byte repair).
Contract: docs/plans/2026-08-11-fellowship-allegiance-campaign.md
§FA5 (CORRECTED row — 0x001F subscription, NOT 0x027B) + lane-C
allegiance-wire doc + fa-panel-structure §3.2/§10.
Worktree HEAD at review: b6c4a4fa (clean).
VERDICT: APPROVE-WITH-FIXES (one SHOULD-FIX, LOW severity)
FA5 is mechanism-faithful. Every high-stakes claim was re-derived from
the PDB-paired 2013 decomp (not trusted from the implementer's summary):
CF-1's unconditional 0x001F post-world arm, the monarch/patron/self
field sources, the per-relationship SF-7 gate, the swear/break/kick
targets, and the ACE-zeroed-field honesty all match retail. The
mandatory live-mount probe PASSED against the real installed DATs
(the anti-fixture-green safeguard the implementer could not run) —
including the doubled-0x10000492 scoped dual-resolution that is exactly
where a live-only defect could have hidden.
The single SHOULD-FIX is a LOW-severity invented-visual: FA5 greys an
offline vassal's name (OfflineNameColor), which retail does not do
— retail's offline indication is exclusively the authored 0x100004AA
marker toggle, with the name left at normal color. The AD-82 addendum
mentions OfflineNameColor but frames it as "already covered by the
retail-authored 0x100004AA marker," which understates it: the
name-grey is an additional cue retail never paints. This does not block
the connected gate (data + the authored marker are correct); it needs
either removal or an honest register entry.
Live-mount probe — actual output (MANDATORY, reported explicitly)
Run: ACDREAM_PROBE_LIVE_MOUNT=1,
ACDREAM_DAT_DIR=%USERPROFILE%\Documents\Asheron's Call,
dotnet test tests/AcDream.App.Tests -c Release --no-build --filter "FullyQualifiedName~SocialPanelLiveMountProbe". The App test project was
rebuilt first (with dependencies) since this fresh worktree's artifacts
predated FA5.
Result: Passed! - Failed: 0, Passed: 1, Skipped: 0 (exit 0).
SocialPanelLiveMountProbeTests.ProbeLiveMountShapes [84 ms].
The probe is NOT a no-op green — against live DATs it asserts (all held):
Assert.Equal(2, passupCount)—0x10000492occurs exactly twice under the allegiance page.- The scoped dual-resolution:
monarchScopedPassupfound under0x10000490(under monarch field0x10000255),patronScopedPassupfound under patron field0x1000025A, both non-null ANDAssert.NotSame— proving the twoFindDescendantsites resolve to DIFFERENT real instances. A flat/unscoped lookup would silently pick one for both roles; this catches it. - Vassal ListBox
0x10000260template resolves via the productionRowTemplateResolver; row fields0x10000268/0x10000269(UiText),0x100004AA(offline marker) all resolve. - Checkbox
0x10000262(UiButton) + labelID_PlayerOption_IgnoreAllegianceRequestsresolves non-empty. - All five allegiance strings resolve non-empty:
ID_Allegiance_MonarchLabel,PatronSlashMonarchLabel,SwearConfirmation,BreakConfirmation,KickConfirmation. - A full production
SocialAllegiancePageController.Bind()against the live layout emits zero "not found" console warnings.
The raw dump confirms 0x10000490 is authored under BOTH the monarch
field (0x10000255, at (210,0 90x36)) and the patron field
(0x1000025A, at (220,0 80x36)); FA5's monarch-experience lookup
scopes to the monarch instance, and its patron-experience lookup recurses
from the patron field (finding the patron instance) — matching retail's
own GetChildRecursiveTemplate(sub-block, …) vs
GetChildRecursive(m_pPatronField, …) split (see item 2).
FA5 unit suite (targeted, this build):
SocialPanelControllerTests + GameplayConfirmationControllerTests =
36 passed / 0 failed.
Per-item verification (re-derived from primary source)
1. CF-1 — the 0x001F subscription (highest stakes) — VERIFIED
Unconditional post-world arm (the core claim). pseudo-c:157175,
gmAllegianceUI::RecvNotice_PlayerDescReceived @0x00490D40:
if (m_bAwaitingUpdate == 0) { m_bAwaitingUpdate = 1; IncrementBusyCount(); }
CM_Allegiance::Event_UpdateRequest(1); // 00490d59 — OUTSIDE the if
The Event_UpdateRequest(1) at 00490d59 sits OUTSIDE the if guard
(which only wraps the busy-count increment), so retail sends 0x001F(1)
unconditionally, exactly as the implementer claims.
SocialAllegiancePageController.RedeclareAfterWorldEntry() mirrors this:
it calls SetUpdateSubscription(true) with no latch comparison, and
advances the latch only on Accepted (so a dropped send retries).
Three arming points, all confirmed against the decomp (every
Event_UpdateRequest site enumerated):
PostInit @0x004911C6 → 1 (Bind's SetPageVisible(true) attempt,
pre-world no-op in production), OnVisibilityChanged visible
@0x004912DD → 1 / hidden @0x00491311 → 0 (edge-triggered
SetPageVisible), PlayerDescReceived @0x00490D59 → 1
(RedeclareAfterWorldEntry, unconditional). A fourth retail arm exists —
OnQualityChanged @0x00490DC9 → 1 (Patron/Monarch instance-id change,
fa-panel-structure §6.3) — NOT modeled by FA5; covered instead by
acdream's 0x0020-seed + Revision polling. Given the subscription
model (data streams while subscribed), this is a benign
data-refresh-mechanism substitution, not a functional gap.
Genuinely avoids MF-3-REOPEN. Reconnect seams traced end-to-end:
pre-world LiveSessionResetBindings.SessionDialogs → ResetSessionTransientUi → SocialPanelController.ResetSessionDeclaration → _allegiance.ResetPageVisibleLatch() (clears latch, no send);
post-world EnteredWorld.RestoreLayout → RedeclareSocialPanelAfterWorldEntry → SocialPanelController.RedeclareAfterWorldEntry → _allegiance.RedeclareAfterWorldEntry() (unconditional send). So a
reconnect while the panel is CLOSED still re-subscribes.
The test genuinely exercises the world gate.
Reconnect_ReDeclaresSubscription_AfterWorldEntry_EvenWhilePanelClosed
binds with the window never shown (_visible=false), clears the latch,
then asserts RedeclareAfterWorldEntry records subscription:True. This
only passes because the path is unconditional — a conjunction-gated shape
(UpdateAllegiancePageVisibility) would compute false && … = false,
and with the latch already false would send nothing, failing the
assertion. Confirmed it would fail against an edge-triggered shape.
0x027B is NOT wired for data. FA5's sole subscription surface is
SetUpdateSubscription (0x001F); no info-request send exists in the
controller. Class doc and gate-script both state 0x027B/0x027C are
text-only chat (FA2 MF-2). Confirmed.
2. Monarch/patron/self blocks (SF-7 rewrite) — VERIFIED (all field sources)
Re-decompiled UpdatePlayerData @0x00491330, UpdateMonarchData @0x00491B40, UpdatePatronData @0x004917C0 fresh:
0x10000251= the ALLEGIANCE name, not the viewer's.00491397:m_pAllegianceName ← _allegiance.m_AllegianceNameviaAddVariable_String. FA5RefreshSelfBlockusessnapshot.AllegianceName. ✓- Self followers
0x10000252=_total_vassals.00491506:AddVariable_Int(_total_vassals) → m_pPlayerFollowers. FA5snapshot.TotalVassals. ✓ - **Self rank
0x10000253=InqInt(qualities, 0x1e)(AllegianceRank)GetTitle.**00491578:CBaseQualities::InqInt(&var_1f8[0xe], 0x1e, …)thenAllegianceData::GetTitle. FA5 substitutessnapshot.Rank(documented data-source substitution). ✓
- Monarch gate = hide when
monarch==viewer || monarch==0.00491c0f. FA5hasMonarch = monarch is {} m && m.CharacterId != selfGuid(null when no monarch). Matches under all cases. ✓ - Hidden branch blanks name AND followers.
00491f8c(m_pMonarchName ← "") and00491fcd(m_pMonarchFollowers ← ""). FA5 blanks both. ✓ - Shown followers =
_total_members - 1.00491eac:ecx_43 = ebx_3->_total_members - 1. FA5TotalMembers - 1(guarded). ✓ (test asserts "2" forTotalMembers=3). - patron==monarch →
PatronSlashMonarchLabel+ reveal0x10000490sub-block +0x10000492= viewer's CpTithed.00491c60(else ofGetPatron != monarch): sets label,GetChildRecursive(0x10000490) ->SetVisible(1)00491cbc,GetChildRecursiveTemplate(sub-block, 0x10000492)withGetData(viewer).cp_tithed. FA5 mapspatronIsMonarch → PatronSlashMonarchLabel,subBlock.Visible=true,monarchExperiencePassedUp ← Member(self).CpTithed. ✓ patron!=monarch →MonarchLabel+ hide sub-block (00491dc5). ✓ - Enabled ← IsLoggedIn (
SetState(1)vsSetState(0xd)00491f0c). FA5_monarchField.Enabled = monarchData.IsLoggedIn. ✓ - Patron gate = hide when
patron==0 || patron==monarch.004918b1. FA5hasPatron = patron is {} p && (monarch is not {} m || p.CharacterId != m.CharacterId); negation matches retail in all four cases (verified case-by-case). ✓ Shown: name =GetFullName(004918e0); Enabled ← IsLoggedIn (0049197d); experienceGetChildRecursive(m_pPatronField, 0x10000492)= viewer's own tithed (004919b9GetData(viewer)). FA5 matches (patronExperiencePassedUp ← Member(self).CpTithed). ✓
The doubled-0x10000492 scoping matches retail exactly: monarch's under
the sub-block, patron's recursively under the patron field. FA5's two
FindDescendant sites (Bind, lines 315-319) reproduce this precisely.
3. Vassal list — VERIFIED
RuntimeAllegianceState.AllegianceView.GetVassals returns
AllegianceProfileLookups.FindVassals order — the FA1-assembled
REVERSE-wire order (each record prepended, doc comment cites lane C
§4.4). FA5 RefreshRoster materializes new List<>(Vassals(self))
and iterates with no re-sort; RebuildRoster uses
FlushPreservingScroll() (NOT Flush()), so the FA3 scroll-reset
carry-forward is NOT reintroduced. The allocating GetVassals call runs
only inside the snapshot.Revision != _lastRosterRevision gate — not
per-frame (honoring FA2's blast SF-7 caution). ✓
4. Swear/break/kick targets + type-1 confirmation — VERIFIED
- Swear = WORLD selection.
UpdateSwearButton @0x004908E0gates onGetPatron(self)==0 && selectedID!=0 && selectedID!=self && IsPlayer(selectedID) && GetData(selectedID)==0, whereACCWeenieObject::selectedIDis the world selection. FA5 readsSelection.SelectedObjectIdat click, resolves viaResolveWorldObjectName(=Objects.Get(guid)?.GetAppropriateName(), the sameClientObjectTableresolverToolbarRuntimeBindings.ResolveNameuses). ✓ The one omission —IsPlayer()— is registered AD-84 (same class as AD-83). ✓ - No SetSelectedObject for Allegiance's list (fa-panel-structure
§6.2): Allegiance's
ListenToElementMessageidMessage-4 reads the row's0x10000001intom_iidSelectedVassalonly — noACCWeenieObject::SetSelectedObject(Fellowship's DOES call it). FA5's panel-local_selectedVassalGuidwith no world-sync is faithful, and the FA4 world→panel sync is NOT reintroduced for Allegiance. ✓ - Break = current patron (
UpdateBreakButton @0x004909D0:GetPatron!=0;MakeBreakConfirmationDialog @0x00492BF0). FA5OnBreakClick → Patron(self) → Break(patron.CharacterId). ✓ - Kick = panel-local selected vassal (
MakeKickConfirmationDialog @0x00492E10,m_iidSelectedVassal). FA5OnKickClick → _selectedVassalGuid → Kick. ✓ - Type-1 confirmation reaches the generic controller.
RetailUiRuntime.HandleConfirmationRequestforwards every request straight toGameplayConfirmationController(no intercept — FA4's type-4 removal never touched type-1). New testAllegianceSwearRequest_Type1_OpensDialog_MessageVerbatim_AndSendsAcceptOnCloseverifies aConfirmationType 1opens a verbatim dialog and sends accept on close. ✓
5. ACE-zeroed fields (AD-86) — VERIFIED (citations accurate)
references/ACE is not checked out in this worktree, so lane C §5.1 is
the authority. AD-86's cited lines match it field-for-field:
AllegianceHierarchy.cs officers :74-75, officerTitles :78-83,
motd/motdSetBy :86-89, nameLastSetTime/isLocked/approvedVassal
:62-64/:153-155, broadcast counters :53-56; AllegianceData.cs
timeOnline/allegianceAge :59-60,86-89,111-112. Retail's own
gmAllegianceUI has no widget for any of them (fa-panel-structure §3.3),
so dropping them past the parse layer matches retail's presentation. The
register row, blast column, and lane-C pointer are all accurate.
6. §6 DISCIPLINE compliance — VERIFIED
- Resolver on every Build. Main panel Build passes
resolver.Resolve(RetailUiRuntime.cs:2715); the vassal rowRowTemplateResolverbuilds each template withstrings.Resolve(:2765). ✓ - Doubled
0x10000492scoped under each parent (both sites found): monarch undermonarchIsPatronSubBlock(0x10000490), patron underpatronField— probe-confirmedNotSame. ✓ - No lazy
0x0. AllFindDescendantfailures null-check + log; no magenta-placeholder id path. ✓ BlankSentinelis compare-only and never displayed. Used solely inSetProvider'slastValue == sentinelValue/lastValue = sentinelValue; the rendered text comes fromBlankLineProvider([new UiText.Line(" ", …)], a literal single space). The NUL-repair commit7e394cbfrestored the two spaces; byte-swept all 11 FA5-touched files (src/tests/docs): NUL=0, control-char=0. ✓
7. Retail anchors + gate-script §FA5 — VERIFIED
Five+ anchors spot-checked and confirmed: Event_UpdateRequest site
inventory (157175/157212/157414/157455/157495); UpdateSwearButton
enable rule; UpdateBreakButton; UpdateVassalsData @0x00492340
(offline marker SetVisible(1) when !IsLoggedIn @004925b1; row
identity via SetAttribute_InstanceID(0x10000001)); the Update()
ordering (self→monarch→patron→vassals→swear→break→kick,
pseudo-c:158677+) matches FA5's Tick.
The gate-script §FA5 correctly avoids the false-route the reviewer
flagged: it explicitly states the panel populates via 0x001F→0x0020
on tab-show, and says "Do not use @allegiance info as a trigger for
anything in this section." The FA3 section (already corrected in FA3's
fix round) is consistent — it tells the user FA3 does NOT send 0x001F
and @allegiance info won't reveal the panel. Step 4 correctly describes
the unconditional post-world resend as the MF-3-REOPEN analogue; steps
5-8 pre-empt the AD-85 "just a number" and SF-7 per-relationship false
reports. No wrong step.
SHOULD-FIX (numbered)
SF-1 (LOW) — offline vassal name-grey is an invented visual not in
retail; register or remove it. UpdateVassalsData @0x004924c3 sets the
vassal name (0x10000268) via GetFullName with no color argument;
retail's only offline indication is the dedicated 0x100004AA marker
toggle (@004925b1: SetVisible(1) offline / (0) online).
SocialAllegiancePageController.UpdateRow (SocialAllegiancePageController.cs:708)
additionally renders the offline name in OfflineNameColor (0.6,0.6,0.6,1) — a grey retail never paints. The AD-82 FA5 addendum
mentions OfflineNameColor but frames it as "already covered by the
retail-authored 0x100004AA marker," which understates it: the marker is
faithful; the name-grey is a separate additional cue on top of it.
Fix: either (a) render vassal names in TextColor unconditionally
(drop OfflineNameColor), letting the authored 0x100004AA marker carry
the offline state exactly as retail does — the more faithful option — OR
(b) correct the AD-82 addendum to honestly register the offline
name-grey as an adaptive cue rather than claiming no invented tints are
used. Does not block the connected gate (data + marker are correct).
Minor observations (NOT fixes — no action required)
OnQualityChanged/quality-handler re-subscription arms (Patron0x19/Monarch0x1Ainstance-id change →Event_UpdateRequest(1),00490dc9, fa-panel-structure §6.3) are not modeled. Benign: acdream's subscription-plus-0x0020-revision-polling refreshes the data through a different mechanism. Not a functional gap.SetTalkFocusEnabled(4/5/6, …)(patron/monarch/vassal chat-focus toggles, tail of each Update* function) are not modeled — a chat-integration nicety outside the panel-display scope.- Monarch label
LinesProvideris reassigned unconditionally eachTickin the shown branch (RefreshMonarchBlock:555) — a per-frame closure/list allocation, unlike the guardedSetLineused for name/followers. Blast-lens perf nit (rarely changes; could be guarded); not a mechanism concern. - NIT: AD-86's prose says "seven fields" but the parenthetical lists
nine (7
AllegianceProfile+ 2AllegianceData). Cosmetic; the field list and citations are complete/correct.
What verified clean (explicit)
- Live-mount probe: PASSED against real DATs (1/1), including the
scoped doubled-
0x10000492NotSameassertion and full productionBind()with zero "not found". - CF-1's unconditional post-world
0x001Farm is byte-faithful toRecvNotice_PlayerDescReceivedand genuinely closes MF-3-REOPEN; the decisive reconnect test exercises the world-closed gate. - All monarch/patron/self field sources match a fresh decompile of
UpdatePlayerData/UpdateMonarchData/UpdatePatronData. - Vassal roster: FA1 reverse order preserved, no re-sort,
FlushPreservingScroll(no scroll-reset), revision-gated (not per-frame). - Swear=world-selection / Break=patron / Kick=panel-local; no
SetSelectedObjectfor Allegiance; type-1 confirmation flows to the generic controller. - AD-84/AD-85/AD-86 + AD-82 addendum are accurate (AD-86's ACE citations match lane C §5.1 line-for-line).
- §6: resolver on every Build; doubled ids scoped; no lazy
0x0;BlankSentinelcompare-only; zero NUL/control-char corruption across all 11 FA5 files after7e394cbf. - Gate-script §FA5 states the true
0x001F-not-@allegiance-infoexpectation — no false-route.