docs/ISSUES.md #384 records the live-run evidence trail (six connected
runs, the 0.005 m distance diagnostic, the confirmation-arrival diagnostic
that never fires) behind AllegianceGateEnabled=false.
docs/architecture/retail-divergence-register.md AD-87 records the honest
divergence this deferral creates: the allegiance half of the FA6 bot gate
is written and wired but unverified end-to-end over the wire, unlike the
fellowship half which is proven live.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Six live runs against local ACE (testaccount/+Acdream as Leader,
testaccount2/+Horan as Recruit) converged on a clean split:
- FELLOWSHIP two-session gate PASSES live, reproduced in three separate
runs. The decisive cross-session assertion (the Recruit bot's own
RuntimeFellowshipState — a separate process's canonical Runtime owner,
not the Leader's local echo — flipping IsInFellowship=true,
MemberCount=2, LeaderGuid=<Leader>) holds every time. This ships as the
automated gate.
- ALLEGIANCE swear never completes: ACE returns nothing at all to
Event_SwearAllegiance (0x001D) — no 0x0274 confirmation, no 0x0020 tree
update, no WeenieError — even at 0.005 m separation (run6's distance
diagnostic ruled out retail's 2.0 m swear-distance gate). Ambiguous
between an FA1 wire-builder defect, an ACE-side rule this test pair
trips, or a drop; disambiguating needs an ACE server console this
harness doesn't have. Filed as docs/ISSUES.md #384 and
docs/architecture/retail-divergence-register.md AD-87.
AllegianceGateEnabled (static readonly, not const, to avoid a CS0162
unreachable-code build error from branching on a literal) gates every
allegiance-dependent stage in BOTH policy classes off by default:
Leader's WaitForVassal (skipped straight to the reconnect+teardown that
only need fellowship state), Recruit's Swear/WaitSwornSeed/Break/
WaitBrokenSeed (same). All of that code stays fully written and wired —
flipping the flag re-enables it for a follow-up investigation once #384
closes. WaitReconnectReseed on both sides now asserts fellowship-only
re-seeding when the flag is off, preserving the reconnect-idempotence
proof independent of the allegiance blocker.
The two live-run diagnostics added while investigating #384 (the
confirmation-arrival log line in HeadlessSessionHost's
OnConfirmationRequest, and LogDistanceToPatron in the Recruit policy) are
kept as permanent, clearly-labeled evidence for whoever reopens#384 —
neither is "TEMP, strip later."
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The second live gate run exposed a real environmental hazard: this shared
ACE dev instance has a THIRD player character online (+Je, guid
0x50000001), and after @teleallto it ended up nearer to the Leader bot than
the actual Recruit bot (+Horan, 0x5000000B). RuntimeFriendlyTargetQuery.
FindClosestOtherPlayer — "nearest ANY other player" — picked +Je, and the
fellowship recruit sent to it obviously never completed (confirmed live:
WaitRecruited/WaitForRecruit both timed out, both bots quarantined and
gracefully logged out cleanly).
RuntimeFriendlyTargetQuery.FindPlayerByName resolves the nearest player
whose streamed name matches exactly, with 3 new conformance tests
(preferring the named player over a closer stranger, returning null when
absent, and case-sensitivity/hidden/no-draw/self rejection).
FellowshipAllegianceGateCoordinator (AcDream.Headless.Policies) is a small
same-process, no-locking (single update thread) carrier for the Recruit
bot's own discovered character name — set by its own HeadlessSessionHost
the instant CharacterList selection resolves it, which IS D8's "discover it
live" mechanism, not a hard-coded value. Constructed once per
HeadlessProcessHost and threaded through HeadlessBotPolicyFactory.Create
into the Leader policy, which now name-matches instead of taking whichever
player entity happens to be closest.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first live two-bot run exposed a real gap: retail always confirms an
incoming allegiance swear to the PATRON (0x0274 Character.ConfirmationRequest,
type 1) before ACE sends 0x0020/0x01C8 to either party
(docs/research/2026-08-11-fa-allegiance-wire.md §3.3) — and unlike
fellowship's FellowshipAutoAcceptRequests (which ACE honors server-side,
never even sending a confirmation), there is no auto-accept character option
for allegiance. HeadlessSessionHost wired OnConfirmationRequest to null, so
a headless bot silently dropped every incoming confirmation and the swear
never completed — both bots timed out waiting for TotalVassals/patron to
seed, confirmed live against ACE (both quarantined cleanly with graceful
per-character logout, proving the self-terminating design and existing
graceful-shutdown path both work correctly; this was an FA6 capability gap,
not an FA1-FA5 wire/state defect).
HeadlessSessionHost now latches the single outstanding confirmation
(matching retail's own one-dialog-at-a-time shape) and exposes
PendingConfirmation/RespondToConfirmation, cleared on every reconnect since
a stale context id would be meaningless post-reconnect. The gate's Leader
policy polls and blind-accepts any pending confirmation on every tick before
its own stage switch — the v1 substitute for a human clicking Accept, safe
because the gate's two sessions are its own known bots.
HeadlessBotPolicyFactory.Create takes two new optional delegate parameters
(default null, so cannot break other policy ids); the Leader gate policy
requires them non-null via a defensive constructor check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
two-bot gate
Adds the infrastructure docs/plans/2026-08-11-fellowship-allegiance-campaign.md
D8 and docs/research/2026-08-11-fa-acdream-seams.md §6.2 call for:
- HeadlessBotPolicyDescriptor gains an optional typed Role
(HeadlessBotPolicyRole.Leader/Recruit) so two sessions selecting the SAME
policy id run different scripts — fellowship leader/allegiance patron vs
fellowship recruit/allegiance vassal.
- HeadlessBotPolicyFactory.Create widens from Create(string id) to
Create(HeadlessBotPolicyDescriptor, GameRuntime) — the gate policies need
RuntimeFriendlyTargetQuery, which (like its RuntimeHostileTargetQuery
sibling) takes the concrete GameRuntime rather than the narrower
IGameRuntimeView a policy's own Tick receives (IRuntimeEntityView's
snapshot carries no name/PWD-bitfield). The single call site
(HeadlessSessionHost.cs) already has the constructed runtime in scope, so
no new constructor parameter or cross-session coordinator was needed.
- FellowshipAllegianceLeaderBotPolicy / FellowshipAllegianceRecruitBotPolicy:
a full stage-machine pair covering proximity (retail's admin @teleallto —
"teleport everyone online to me" — needs no cross-session name sharing,
unlike @teleto <name>; D8's proximity requirement is load-bearing, recruit
fails without it), fellowship create+recruit, the D4 0x00A6 panel-open
declaration with a vitals-presence assertion, the decisive two-session
assertions (the RECRUIT bot's own RuntimeFellowshipState/
RuntimeAllegianceState flipping — not the Leader's local echo), a
mid-flow reconnect on both bots proving FA2's reset-and-reseed semantics
over the real wire, and teardown (disband / break) with matching
decisive-clear assertions.
Every pre-FA6 policy (idle, lifecycle-smoke, observer-movement,
portal-route-smoke, jump-probe) is unaffected; the widened factory
signature is the only touch point.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RuntimeHostileTargetQuery only classifies hostile monsters (via
CombatTargetPolicy.IsHostileMonster) — the FA6 two-bot fellowship/allegiance
headless gate needs the OTHER bot's server guid as a FRIENDLY target instead.
RuntimeFriendlyTargetQuery.FindClosestOtherPlayer mirrors the hostile query's
shape exactly (same hidden/no-draw filtering, same landblock-absolute
distance metric), substituting the retail PWD-bitfield IsPlayer bit (0x8,
via the existing EntityCollisionFlagsExt.FromPwdBitfield decoder) for hostile
classification. TryGetName resolves the streamed WeenieHeader name for
reporting/logging.
4 new conformance tests mirror RuntimeHostileTargetQueryTests's fixture
pattern: cross-landblock distance, hidden/no-draw/self/non-player rejection,
null-without-player-or-target, and unresolved-guid name lookup.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The FA5 mechanism review found the offline-vassal name-grey
(OfflineNameColor) is an invented visual: retail's UpdateVassalsData
@004924c3 writes the vassal name with no colour change, and the offline
cue is EXCLUSIVELY the authored 0x100004AA marker (already wired,
SetVisible per online state). Removed OfflineNameColor; the vassal name
always renders in the normal white. The Allegiance page now carries NO
invented tint (unlike Fellowship's registered leader/selection tints).
Pinned by Allegiance_OfflineCue_IsTheMarkerOnly_NameStaysWhite (marker
visible iff offline, name always white). AD-82's FA5 addendum corrected
(it had described the now-removed grey as 'covered by the marker'); AD-86
count corrected seven -> nine.
Full Release suite: 13,297 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Suite-accounting SHOULD-FIX: the FA5 ledger/commit cite FA4's INTERMEDIATE
13,285 figure as the baseline and claim +11 net, but FA4 CLOSED at 13,286
(its 'Final full suite' figure) and the real net is +10 (verified per-file
[Fact] counts: SocialPanelControllerTests 22->31, Confirmation 4->5, probe
1->1) -- the ledger's own itemization already sums to +10, contradicting
its +11 headline. End figure 13,296/4/0 is itself correct; documentation
fix only.
Verified clean: all three Callbacks/Bindings construction sites pass the
widened Allegiance binding; every production accessor fed from a real seam;
the 0x001F and 0x00A6 toggles are independent edge-triggered latches with
no cross-talk (38 Fellowship tests green); ResolveWorldObjectName reuses
the Toolbar's ClientObjectTable read and ShowConfirmation is a pre-existing
shared method with no Fellowship collision; @allegiance info/0x027C path
untouched (52 Core.Net + 16 Runtime allegiance tests green); FA5 makes zero
Runtime changes; register 63->66 rows accurate (AD-84/85/86 + AD-82
addendum); NUL-fix correct and no residual control bytes in any of the 11
touched files.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A tool-layer artifact from the original FA5 commit (7ed79eaf) silently
replaced the leading and trailing ASCII space (0x20) in
`BlankSentinel = " blank "` with NUL (0x00) bytes -- verified byte-for-
byte via PowerShell (two NUL bytes total in the whole file, both
adjacent to the literal's "blank" text). C# tolerates an embedded NUL in
a string literal (it compiles and runs fine, since the constant is only
ever used as an internal dedup sentinel, never rendered), so this never
surfaced as a build or test failure -- caught only by an incidental
`file`/`grep -a` binary-content check while re-reviewing the finished
slice. Replaced the two NUL bytes with the intended spaces at the exact
byte offsets; swept every other file this slice touched (Runtime/App/
tests/docs) for the same corruption and found none.
No behavior change: full solution suite still 13,296 passed / 4 skipped
/ 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Register:
- AD-84 -- Swear button's missing "target is a player" enable-rule gate,
same class as AD-83's Recruit-button gap.
- AD-85 -- the unported StringInfo variable-substitution engine (AD-81's
same root cause) extended to the Allegiance page's numeric-only
followers/rank/experience-passed-up fields and its three local
confirmation dialogs (verbatim-or-bare-name, never invented).
- AD-86 -- ACE's deliberate zeroing of seven AllegianceProfile/
AllegianceData fields (officers, officer titles, MOTD, MOTD-set-by,
name-last-set-time, lock, approved vassal, timeOnline, allegianceAge),
dropped past acdream's own parse layer to match retail's own
gmAllegianceUI, which has no widget for any of them either.
- AD-82 addendum: the vassal-row click-target-only selection shares
point (3)'s limitation, but NOT the invented leader/selection tints
(point 1/2) or the Fellowship-only world-selection sync (point 4) --
Allegiance's list-selection message has no SetSelectedObject call.
Gate script: new docs/research/2026-08-12-campaign-fa-test-script.md
SFA5 section, mirroring SFA4's structure -- the CF-1 subscription steps
(including the reconnect-while-closed MF-3-REOPEN analogue), the SF-7
per-relationship monarch/patron steps, vassal-list steps, swear/break/
kick with their confirmations, the ACE-zeroed-field honesty note, and
full "what to report"/"explicitly not in scope" lists.
Plan ledger: FA5 row filled in against 7ed79eaf with per-item summary,
directly-measured totals (13,296/4/0, +11 net from FA4's 13,285/4/0),
and the two primary-source resolutions this slice needed beyond the
research docs (the self-rank field's live buffed-quality source, and
"your follower count" == _total_vassals, confirmed by a fresh targeted
decompile of UpdatePlayerData rather than inferred).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Campaign FA slice FA5. The Allegiance page (0x10000291) goes from FA3's
empty-state shell to fully live, wired against FA1's parser and FA2's
RuntimeAllegianceState/IRuntimeAllegianceCommands (both already shipped
the full command surface, including SetUpdateSubscription).
CF-1 (the corrected data subscription): 0x001F AllegianceUpdateRequest,
not 0x027B, is the panel's data source (0x027B/0x027C are text-only chat
per FA2 MF-2). Wired at retail's three arming points -- Bind's PostInit
attempt (almost always a pre-world no-op), the post-world EnteredWorld
seam (RedeclareAfterWorldEntry, UNCONDITIONAL -- does not check the
current latch, matching retail's own PlayerDescReceived arm and avoiding
the exact MF-3-REOPEN bug class FA4 hit for 0x00A6), and the visible
branch (SetPageVisible, edge-triggered, folded into
SocialPanelController's existing window-shown+active-tab conjunction
alongside Fellowship's 0x00A6).
Monarch/patron/self blocks: per-relationship empty-state gate (fix-round
SF-7) replacing FA3's coarse HasProfile-only gate -- the monarch block
hides when there is no monarch OR the monarch is the viewer; the patron
block hides when there is no patron OR the patron is the monarch (in
which case the monarch block's 0x10000490 sub-block reveals and its
label swaps to PatronSlashMonarchLabel). Field sources decompiled fresh
from gmAllegianceUI::UpdatePlayerData/UpdateMonarchData/UpdatePatronData:
0x10000251 is the ALLEGIANCE's own name (not the viewer's), follower
counts are TotalVassals/TotalMembers-1 directly off the wire, and the
"experience passed up" text (0x10000492, doubled -- scoped FindDescendant
under each of its two parents) is the viewer's own CpTithed under the
monarch/patron blocks and each vassal's own CpTithed in their row.
Vassal roster: flat list built via UiTemplateListBox.FlushPreservingScroll
in the FA4 roster-diff pattern (guid-set diff, in-place update on an
unchanged set), rendering in the bindings' own already-reversed order.
Swear/break/kick: each opens a local confirmation dialog
(RetailDialogFactory via ShowConfirmation) before sending, mirroring
retail's MakeSwearConfirmationDialog family -- Swear targets the WORLD
selection (via the same ClientObjectTable name resolver
ToolbarRuntimeBindings.ResolveName already uses), Break targets the
current patron, Kick targets the panel-local selected vassal row (no
world-selection sync for Allegiance, unlike Fellowship). The
server-driven "accept incoming swear" (ConfirmationType 1) needed no new
code -- GameplayConfirmationController already handles every type
generically; a new test verifies it explicitly.
Runtime/composition plumbing: DeferredGameRuntimeStateCommands gains
Allegiance{Swear,Break,Kick,SetUpdateSubscription}; SocialRuntimeBindings
gains the Allegiance view/command projections; SocialPanelController.
Callbacks.AllegianceSnapshot widens to a full
SocialAllegiancePageController.Bindings record, mirroring FA4's
Fellowship widening.
Tests: SocialPanelControllerTests.cs gains 10 tests covering the SF-7
gate (4), roster population, swear/break/kick wiring (3), and the CF-1
subscription arming points (2); GameplayConfirmationControllerTests.cs
gains the type-1 verification test.
Also extends SocialPanelLiveMountProbeTests.cs (production-mount
assertions: scoped 0x10000492 resolution, the vassal row template, the
checkbox, confirmation-dialog string resolution, and a full production
Bind() pass) -- not yet run against live DATs in this worktree (no
Documents/Asheron's Call present here).
Release build green; full solution suite 13,296 passed / 4 skipped / 0
failed (13,300 total), up from FA4's 13,285/4/0 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The re-fix moves the 0x00A6 re-declaration off the pre-world reset seam
and onto the post-world EnteredWorld seam, and stops the widget latch from
advancing on a dropped publish. Verified in the diff:
- SetPageVisible advances _pageVisible ONLY on RuntimeCommandStatus.Accepted
(the widget-level root of the REOPEN); a dropped Inactive publish leaves
the latch clear so the in-world attempt is not deduplicated.
- ResetSessionDeclaration (pre-world) now only clears the latch;
RedeclareAfterWorldEntry (new) does the re-evaluation, wired through
RetailUiRuntime.RedeclareSocialPanelAfterWorldEntry into
LiveSessionRuntimeFactory's EnteredWorld RestoreLayout delegate.
Seam ordering traced and confirmed inverse of the pre-world SessionDialogs
stage: StartCore runs ResetHostBeforeStart (pre-world reset, latch clear)
at :555, then ActivateCommands :639, _inWorld=true :642, and
ApplyEnteredWorld :644 -> LiveSessionHost.ApplyEnteredWorld ->
RestoreLayout delegate -> RedeclareAfterWorldEntry. So SetPanelOpen's
requireWorld gate is Accepted and 0x00A6 publishes on the fresh server.
Idempotent and load-bearing (the social panel isn't state-managed
visibility, so RestoreLayout fires no OnShown edge).
Tests model the world gate (fake returns Accepted only when in-world) and
would fail against pre-fix behavior: the widget test's second attempt is
deduplicated if the latch advances unconditionally; the reconnect test's
DoesNotContain-after-reset fails if the pre-world declaration is
reintroduced (the coordinator's RED-verification). Binary confirmed
post-fix (new tests reference RedeclareAfterWorldEntry); 3/3 new + 58/58
touched classes green. 13,286/4/0 reconciles (+1, 0 deletions).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The FA4 fix round's MUST-FIX 3 placed the 0x00A6 reconnect re-arm at the
wrong lifecycle point (re-review 8bbceff5): ResetSessionTransientUi runs
via the SessionDialogs reset stage BEFORE _inWorld=true, so SetPanelOpen
(world-gated, Validate requireWorld:true) returned Inactive and published
nothing — yet _pageVisible was latched true anyway, so no later hook
re-declared and fellow vitals stayed frozen for the whole new session.
The unit test passed only because the fake recorded unconditionally.
Two-part fix, both retail-faithful mechanisms not suppressions:
- SocialFellowshipPageController.SetPageVisible advances the edge-trigger
latch ONLY when the declaration is Accepted (published), so a dropped
pre-world send leaves the latch clear and a later attempt retries.
- ResetSessionDeclaration (pre-world) now ONLY clears the latch; the new
RedeclareAfterWorldEntry fires from the LiveSession EnteredWorld seam
(wired via RestoreLayout, idempotent if a persisted layout already
re-showed the page) so a still-open Fellowship page re-declares 0x00A6
in world and vitals resume.
Regression pins that actually catch it (the prior test could not):
- SetPageVisible_DoesNotLatch_WhenDeclarationDropped_SoItRetriesInWorld
(widget-level root, world-gated fake);
- Reconnect_ReDeclares0x00A6_AfterWorldEntry_NotDuringPreWorldReset +
Reconnect_StaysSilent_WhenFellowshipPageIsNotActuallyOpen (panel-level,
world-gated). RED-verified: reintroducing the pre-world declaration
fails the reconnect test.
Full Release suite: 13,286 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Re-derived each disposition from the actual fix diffs (290f9b58/5499f058/
df000306/300d8189/55b17e15/1d743277/f041b09b), not the commit claims.
CLOSED (4/5 MUST-FIX, all 9 SHOULD-FIX, all 4 NIT, blast SF-1):
- MUST-FIX 1: (int)((double)pct*100.0) truncation + 6->44%/8->34% pinning
cases + gate step corrected.
- MUST-FIX 2: intercept deleted, every type routes to the generic
controller, type-4 dialog test added; type-1 allegiance path unaffected
(was never intercepted).
- MUST-FIX 4: world->panel selection sync reproduces retail's found/
fallback arms; AD-82 records the deferred generic UiTemplateListBox
selection-model port honestly -- minimal-observable-contract, not a
hidden gap.
- MUST-FIX 5: AD-82/AD-83 well-formed; AD-78 count corrected to 34/16.
- D6/D7/SF-8 dimming (audited from source): 34 dimmed / 16 live is
correct, not split-the-difference. FellowshipShareLoot has NO client
value-reader (only an editor/display surface; 0x00A2 sends shareXP
alone; ACE authors loot server-side) -> dimmed faithful.
FellowshipShareXP is genuinely read by the Create click -> Live right.
REOPEN (MUST-FIX 3): the 0x00A6 reconnect re-arm is placed at a pre-world
reset seam. ResetSessionTransientUi runs via the SessionDialogs reset
stage at ResetHostBeforeStart / retired-scope teardown -- both BEFORE
_inWorld=true and before command activation for the new generation -- and
SetPanelOpen requires world, so the re-declaration returns Inactive and
nothing is published, yet _pageVisible is still set true and no
post-world-entry hook re-evaluates. The new server never receives 0x00A6
and fellow vitals stay frozen -- the exact bug the fix targets. The unit
test passes only because its fake command records unconditionally.
Recommend moving the re-declaration to an in-world seam (EnteredWorld).
Totals/probe: 109/109 touched App test classes green on post-fix
binaries; live-mount probe PASS 1/1; +13/0-deletion delta and 13,285/4/0
corroborated on the touched projects and by arithmetic (not re-run
end-to-end).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
FA4 row now names all six fix-round commits (290f9b58, 5499f058, df000306,
300d8189, 55b17e15, 1d743277) alongside the original landing's three, and
records: build/test green at every commit; the +13/0-deletion test delta
broken down per file; the final directly-measured 13,285 passed / 4
skipped / 0 failed (13,289 total); every MUST-FIX/SHOULD-FIX/NIT applied;
and the corrected dimmed-row arithmetic (35 -> 31 FA4-original -> 34
fix-round final, net one row). Also corrects item (4) of the
"contradictions/deferrals" list, which called the missing Recruit
is-a-player register row an acceptable inline comment -- MUST-FIX 5 named
that the wrong call; it is now register row AD-83.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Register (docs/architecture/retail-divergence-register.md):
- AD-78: the Character-tab dimmed count had drifted stale through two
campaigns (still read "35" after FA4 shipped 31; now 34 after the fix
round's three reversions). Addendum explains the full D6/SF-8 chain.
Blast review's own SHOULD-FIX 1.
- AD-82 (new): the invented leader-tint/selection-tint colors, the
name-text-only row click target, and the page-local (not generic
UiTemplateListBox) world->panel selection sync -- MUST-FIX 4's
disposition plus two items MUST-FIX 5 named as owed rows.
- AD-83 (new): the Recruit button's missing "target is a player" gate,
previously an inline comment, not a register row -- MUST-FIX 5's third
item. Section header bumped 61 -> 63 active rows.
Gate script (docs/research/2026-08-12-campaign-fa-test-script.md):
- SF-7: fixed step 3's self-contradiction ("only Quit" then "Disband and
Open should ALSO be enabled").
- MUST-FIX 3: new reconnect step after the existing close/reopen step.
- MUST-FIX 4: new world-selection step under the recruit/dismiss/quit
section.
- MUST-FIX 1: new HARD-check step for the 6/8-fellow 44%/34% truncation
(distinct from the existing SOFT 9-member ACE-divergence note).
- MUST-FIX 2 correction: the old invite steps tested whether acdream's
CLIENT gates the dialog on the option bits -- a mechanism that never
existed in retail and no longer exists in acdream. Rewritten to test
the corrected behavior (the dialog always shows regardless of the
target's own checkbox state) and to explain what ACE-side filtering
would look like if the local server implements it, so a tester doesn't
misattribute ACE's behavior to a client bug.
- Renumbered steps 9-22 to 9-25 to fit the two new steps; updated the
"what to report" section's step cross-references and rewrote its
invite/dimming bullets to match the corrected mechanism.
Plan (docs/plans/2026-08-11-fellowship-allegiance-campaign.md):
- D7 addendum: SF-8's further correction (FellowshipShareLoot reverts
too; only FellowshipShareXP survives as genuinely live).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SocialPanelLiveMountProbeTests wrote the four checkbox labels and the two
Open/Close captions to the console with no assertion, yet the FA4 ledger's
live-DAT paragraph cited them as verified -- the same finding FA3's own
mechanism SF-3 raised for a different table ("printed but never asserted
-- deserves a real assertion, not just a hope"). Now asserts each label is
non-null/non-empty and the two captions equal the exact retail strings
"Open"/"Close". Env-gated (ACDREAM_PROBE_LIVE_MOUNT=1, real installed
DATs) -- inert in this session's build/test run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The corrected plan D6 (docs/plans/2026-08-11-fellowship-allegiance-campaign.md)
established that retail's client reads neither IgnoreFellowshipRequests nor
FellowshipAutoAcceptRequests on the fellowship-invite path -- both are pure
server-side filters with no client consumer, exactly like the two
allegiance bits they were always meant to parallel. Their claimed consumer
(RetailUiRuntime.TryAutoRespondToFellowshipInvite) is deleted in a sibling
commit this fix round. Both rows revert from Live to StoreOnly.
Mechanism review SF-8 additionally found FellowshipShareLoot's claimed
consumer -- "a second live checkbox surface on the fellowship page" -- is
not a consumer at all: nothing in acdream reads the stored value back
(FormatStatsText uses snapshot.ShareXp only; the 0x00A2 Create builder
carries shareXP alone), and the live-DAT dump confirms its checkbox is a
child of the NOT-in-fellowship frame -- invisible whenever you actually
have a fellowship to loot-share within. A second EDITOR of a value is not
a CONSUMER of it under AD-78's own "drives nothing observable client-side"
definition. FellowshipShareLoot reverts too.
Only FellowshipShareXP survives as genuinely live -- the Create-flow click
reads it directly as the sent shareXP wire bit. Net: 35 (pre-FA4) -> FA4
shipped 31 -> fix round reverts three -> 34 of 50 dimmed / 16 live, ONE
net un-dim from the pre-FA4 baseline, not four. Updated the class doc's
derivation table, the conformance test's ExpectedStoreOnlyIds set, and the
31/19 count assertions to 34/16.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MUST-FIX 3 -- SocialFellowshipPageController.SetPageVisible is edge-triggered
on a bool that survives a generation reset unchanged while the panel stays
open, so a reconnect never re-sends 0x00A6 and fellow vitals freeze for the
rest of the new session. SocialPanelController.ResetSessionDeclaration
clears the fellowship page's latch (SocialFellowshipPageController.
ResetPageVisibleLatch, this commit's counterpart) and re-evaluates the
existing "window shown AND Fellowship active" conjunction, wired into
RetailUiRuntime.ResetSessionTransientUi -- a seam that already runs on
every generation reset. A still-open Fellowship page re-declares; a closed
or other-tab page correctly stays silent.
SF-4 -- SocialPanelController's constructor subscribed an anonymous lambda
to UiTabPanel.ActivePageChanged with no way to remove it; Dispose only set
a flag. A tab switch after Dispose still reached
UpdateFellowshipPageVisibility and issued a Runtime command, since Tick's
own _disposed guard doesn't cover this event path. Stored the handler as a
field and unsubscribe it in Dispose.
Also adds the panel-level D4 conjunction test mechanism SF-5 flagged as
missing (the only prior D4 test exercised the PAGE controller's own
edge-trigger directly, never SocialPanelController's "window shown AND
Fellowship active" logic or its ActivePageChanged subscription).
Per docs/research/2026-08-12-fa4-review-mechanism.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MUST-FIX 1 -- D5's percentage conversion rounds where retail truncates.
gmFellowshipUI::UpdateFellowStats @0x0048ECC9 forms pct*100.0f on the x87
stack then calls _ftol2 (MSVC's round-to-truncate helper), never
MathF.Round. The stored floats for 6 and 8 fellows are 0.44999998807907104
and 0.3499999940395355 (byte-read from the PDB-paired binary), so retail's
own products truncate to 44/34, not 45/35 -- and (int)(pct*100f) alone does
not fix it, since 0.45f*100f already rounds UP to exactly 45.0f in single
precision. Fixed as (int)((double)pct * 100.0), forming the product the
same wider-than-single-precision way retail's x87 does. Pinned with new
[InlineData] cases for both sizes.
MUST-FIX 4 -- gmFellowshipUI::UpdateFellowSelection @0x0048F0F0 (the
world->panel arm of retail's two-directional selection coupling) was never
ported; only the panel->world arm (SelectFellow) shipped. Selecting a
fellow in the WORLD left Dismiss/Assign-Leader disabled and showed no row
highlight. SyncSelectionFromWorld/SetSelectedFellow reproduce the
observable contract (button-enable + a row tint) against this
controller's own guid-keyed row dictionary instead of porting retail's
generic ListBox SetAttribute_InstanceID/SetSelectedItem primitive (scoped
disposition recorded at register row AD-82).
Also in this pass over the controller:
- SF-1: cache the fellowship-name LinesProvider; only reassign on an
actual name change (was allocating once per Tick, even while hidden).
- SF-2/SF-3: track true membership in _memberGuids, independent of which
rows finished building. Fixes an unbounded DAT-locked rebuild retry
when a row template permanently fails to build, and fixes Recruit's
"already a fellow" check reading render rows instead of membership.
- N-0: the Open/Close caption now flips optimistically on click, matching
retail's pre-toggle-before-server-echo (lane B feature 11).
- N-1/N-2/N-3: doc-only notes on the meter-child-text gap, the max>0
guard, and Tick's two-read non-atomicity.
- ResetPageVisibleLatch: the fellowship-controller half of MUST-FIX 3
(see the SocialPanelController commit for the panel-level half).
Per docs/research/2026-08-12-fa4-review-mechanism.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RetailUiRuntime.TryAutoRespondToFellowshipInvite auto-declined/auto-accepted
fellowship invites based on IgnoreFellowshipRequests/FellowshipAutoAcceptRequests
before the dialog ever reached GameplayConfirmationController. Byte-verified
across Handle_Character__ConfirmationRequest @0x005640A0,
RecvNotice_FellowshipRequest @0x00490880, and MakeFellowRequestDialog
@0x00490620 (whose only guard is m_fellowRequestContext) plus a whole-file
sweep of both option accessors: retail's client reads neither bit on any
confirmation path. ACE filters both bits server-side, so the interceptor was
dead code against a correct ACE and actively harmful against a drifting one
(IgnoreFellowshipRequests defaults true, so it would silently swallow real
invites with no dialog and no chat line).
HandleConfirmationRequest now routes every confirmation type, including 4,
straight to the generic controller -- exactly like retail. No tests existed
for the deleted interceptor (nothing to remove); added a test proving the
type-4 dialog renders the server message verbatim (not "Continue?"-suffixed)
and sends accept/decline through the generic path.
Per the corrected plan D6 (docs/plans/2026-08-11-fellowship-allegiance-campaign.md).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
D6 asserted the client consumes IgnoreFellowshipRequests/
FellowshipAutoAcceptRequests on the invite path; the FA4 mechanism review
(913e35cd MUST-FIX 2) byte-verified retail reads NEITHER bit client-side
(Handle_Character__ConfirmationRequest @0x005640A0, RecvNotice_
FellowshipRequest @0x00490880, MakeFellowRequestDialog @0x00490620) — ACE
filters both server-side. Same class as the D2 reset-lifetime correction.
D6/D7 corrected in-place with dated addenda: the client-side intercept is
removed, the invite dialog always shows, and the two un-dims revert
(dimmed 31->33). D8 records the user-provided second account
(testaccount2/testpassword2) and the recruit-proximity requirement.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A second mechanism-lens review landed on the same path at 6849b457 while
this one was in progress and was overwritten by 913e35cd. Its text is
recoverable from git and is now cited from a new appendix, its two unique
findings are carried forward, and the two places the reviews disagree are
adjudicated from primary source.
Carried forward:
- SF-9: AD-78's register row still says "35 of 50 rows dimmed" (the D7
addendum landed in the class doc, not the row's Where column).
- N-0: the Open/Close caption does not optimistically pre-toggle; lane B
feature 11 records that retail's handler pre-toggles _open_fellow
locally before sending 0x0291.
Adjudicated:
- _ftol2 vs MathF.Round: 6849b457 filed it a NIT ("round and truncation
agree on every table value"). That holds for the DECIMAL literals, not
the stored floats -- 0x007C91D4 = 0.44999998807907104 and 0x007E72BC =
0.3499999940395355, so retail truncates 44.999998/34.999999 to 44/34
while acdream rounds to 45/35. Stays MUST-FIX 1.
- D6 invite auto-response: 6849b457 passed it as verified-clean after
confirming the code matches the plan. The binary says retail has no
such client-side read on any confirmation path. Stays MUST-FIX 2.
The reviews agree on the reconnect D4 hole, the missing panel-level D4
conjunction test, the leader-tint register omission, and the live-DAT
probe result.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Findings persisted before any fixer dispatch, per the campaign's §7
review protocol.
MUST-FIX, in the order they were derived:
1. D5's percentage conversion rounds where retail truncates. Byte-decoded
0x0048ECC9..0x0048ECD8 (fld pct; fmul [0x007A5170]=100.0f; call
_ftol2 @0x005DE394 -- the fld/fst/fistp/fild truncation dance), so a
6-fellow roster displays 45% where retail shows 44%, and 8 fellows
shows 35% vs 34%. The table itself IS byte-exact; only the ->int
conversion diverges, and it is not fixable by a plain cast because
0.45f*100f already rounds up to 45.0f in single precision.
2. D6's client-side invite intercept has no retail anchor. Read in full:
Handle_Character__ConfirmationRequest @0x005640A0 (bare jump table),
RecvNotice_FellowshipRequest @0x00490880, MakeFellowRequestDialog
@0x00490620 (only guard is m_fellowRequestContext), plus a whole-file
sweep of both option accessors -- zero reads on any confirmation path.
The code comment cites ACE's Fellowship.cs as "retail". ACE filters
both bits server-side, so the intercept is dead against a correct
server and harmful against a drifting one -- and IgnoreFellowshipRequests
defaults to TRUE client-side.
3. D4 never re-declares 0x00A6 after a generation reset: the edge-
triggered _pageVisible latch survives reconnect, so fellow vitals stay
frozen for the whole new session. ResetSessionTransientUi is the seam.
4. gmFellowshipUI::UpdateFellowSelection @0x0048F0F0 is not ported --
selecting a fellow in the WORLD leaves Dismiss/Leader disabled and no
row ever shows selected; the plan's contracted UiTemplateListBox
selection model + 0x1000000D row instance-id were not added.
5. Three shipped deviations have no register row (invite intercept, gold
leader tint, name-text-only row selection); the Recruit is-a-player
gate's "inline comment, not a register row" call is also wrong.
Re-derived rather than trusted: the live-mount probe was re-run against
the installed DATs (every ledger element/string claim CONFIRMED, Bind()
warning-free), the GetEvenSplitXPPctg table was byte-read from the
PDB-paired binary, FlushPreservingScroll's shrink semantics were traced
through UiScrollablePanel/UiScrollable (sound), and the five touched
test classes pass 93/93.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mechanism-faithfulness lens on 357d2032/5bdd0528/38f08314. Every
wire-touching mechanism verifies retail-faithful: the D4 0x00A6 gate
(all five in-session transitions + idempotence + no-send-while-
disconnected), the leader-quit 0x0290-before-0x00A3 hand-off routing,
the D5 byte-exact even-split table, the D6 type-4 auto-response +
Runtime mutual exclusion, the D7 four-row un-dim (35->31 conformance),
scroll preservation across a rebuild, create-flow refusal-by-enabled-
state, and the button-enable rules. Live-mount probe PASSES 1/1 against
real DATs; FA4 suites 75/75 App + 28/28 Runtime under --no-build.
MUST-FIX: the leader-gold-tint adaptation has no divergence-register row
(AD-80/AD-81 don't cover it). SHOULD-FIX: AD-78's stale 35-of-50 count;
D4 not re-armed across a reconnect while the panel stays open; no
panel-level test pins the D4 conjunction. NITs: caption pre-toggle,
_ftol2-vs-Round (both non-blocking).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All nine blast axes verified clean at the code level. Single fix:
the AD-78 register row still reads "35 of 50 rows dimmed" after FA4's
D7 flipped four rows to Live (now 31 of 50) -- the class doc and
conformance test were updated, the binding register row was not.
Plus one minor non-blocking observation on GetMembers' per-vitals-tick
allocation profile.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Register: AD-80 files the D5 XP-share display divergence between
retail's byte-decoded table (acdream renders it verbatim) and the
currently-targeted ACE server's slightly different actual grant (.3 vs
.3111111 at 9 fellows, no 10-fellow row, wrong out-of-range default) --
an ACE-vs-retail gap, not an acdream-vs-retail one, filed because it is
directly user-visible through this panel. AD-81 files the two unported
retail text-composition primitives the fellowship page's mechanism
needs (StringInfo variable substitution, ACCharGenData::FormatName) and
what acdream renders instead (plain numeric composites, the raw typed
name). AD-78's derivation table gains its D7 addendum: 4 of the 35
store-only rows (IgnoreFellowshipRequests/FellowshipAutoAcceptRequests/
FellowshipShareXP/FellowshipShareLoot) moved to the Live bullet with
their new consumers named.
Gate script: new §FA4 section covering create (name + shareXP), the
open/close caption swap, button-enable rules, and the D5 display -- all
solo-testable -- plus roster/recruit/dismiss/leader-handoff/invite-
dialog steps marked [TWO-CLIENT] with an honest note that they defer to
FA6's bot-vs-ACE gate if a second account isn't available for this
connected gate. Corrects FA3's now-stale "these six buttons/four
checkboxes are INERT" claims in steps 11-12 to point at the new
section instead of leaving a wrong claim in place.
Ledger: FA4 row CODE-COMPLETE with both commit SHAs, the reconciled
13,238->13,272 (+34) test-count arithmetic, the live-DAT verification
summary (ACDREAM_PROBE_LIVE_MOUNT=1 against real installed DATs,
including the structural finding that retail's own frame-visibility
swap already gates the Create-flow controls away from the roster view
with no extra code needed), and the four scoped
deferrals/simplifications this slice made (the StringInfo/FormatName
gap, the proportional-share omission, the Recruit button's
superset-of-retail enable rule).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Roster: SocialFellowshipPageController now builds one row per fellow
from the authored template (0x21000030/0x10000281, live-DAT verified),
diffing the member GUID set on each revision-gated Tick -- an unchanged
set updates every row's bound widgets in place (no ListBox mutation, so
scroll position is untouched by construction); only a real join/leave/
disband triggers a rebuild, via UiTemplateListBox.FlushPreservingScroll
(FA3 carry-forward 1, both the widget-level fix and the controller-level
diff). Health/stamina/mana meters bind Fill+Label; the leader's name
tints gold (lane A's row template has no dedicated leader marker, so
this is a flagged adaptation, not a ported mechanism). Row-click
selection (SelectFellow) drives Dismiss/Leader targeting and the world
selection (SelectionChangeSource.Social).
D4: SocialPanelController now tracks "is the social window shown AND is
Fellowship the active tab" via UiTabPanel.ActivePageChanged +
OnShown/OnHidden, and calls SetPageVisible on every transition, which
sends 0x00A6 (idempotent, no-op while disconnected) -- the prerequisite
ACE gates its 0x02C0 vitals stream on.
Create flow: the inline name field (0x1000026F, an authored Editable
UiField -- live-DAT verified) gates the Create button's enabled state
exactly like retail (empty name = disabled = the whole refusal
mechanism, no separate error text); FellowshipShareXP's live value is
read at click time.
Actions + confirmations: Recruit/Dismiss/Quit/Disband/AssignLeader/
SetOpen all route through DeferredGameRuntimeStateCommands (new
Fellowship* methods) rather than a raw WorldSession send, so Quit
correctly picks up RuntimeFellowshipState's leader hand-off rule.
Button enable states port gmFellowshipUI::UpdateButtons verbatim. The
Open/Close button's caption swaps between the two DAT-resolved strings
cached once at Bind (never per-tick -- DatCollection is not safe to
touch unprotected from the render loop). RetailUiRuntime intercepts a
type-4 confirmation request before it reaches the generic
GameplayConfirmationController: IgnoreFellowshipRequests auto-declines,
FellowshipAutoAcceptRequests auto-accepts, neither set falls through to
the existing dialog machinery unchanged (D6).
D5 display: the per-fellow stats line uses retail's byte-decoded
even-split percentage table verbatim (1.0/.../.3111111/.28, default
0.0); the proportional branch omits the percentage rather than
inventing a formula (no acdream ExperienceToRaiseLevel table exists
yet). Both StringInfo variable substitution (row/stats/vitals text) and
ACCharGenData::FormatName (create-flow name canonicalization) are
unported prerequisites, so row text renders as plain numeric composites
-- register rows AD-80/AD-81 (docs commit).
D7: un-dims IgnoreFellowshipRequests/FellowshipAutoAcceptRequests
(consumed by the D6 auto-decline/accept) and FellowshipShareXP/
FellowshipShareLoot (consumed by Create + the page's own second
checkbox surface) on the Character tab -- 4 of 35 store-only rows
promoted to Live (31 remain dimmed).
Carry-forwards from the FA3 re-review, folded into this slice's
contract:
- UiTemplateListBox.FlushPreservingScroll -- preserves scroll offset
across a rebuild instead of resetting to 0 (Flush's existing
contract, unchanged, for Friends/Squelch).
- RowTemplateResolver -- the FA3 caching row-template resolver
extracted from a MountSocialPanel local function into its own
hermetically-testable class; now shared by Friends/Squelch/
Fellowship's row families.
- Friends/Squelch scrollbars now resolve via the built
UiTemplateListBox.ScrollbarElementId (DAT property 0x72) instead of
a hardcoded literal, matching ConfigOptionsPageController's own OP6
precedent.
- The Fellowship roster path never advances its revision latch on a
partial resolver failure until the NEXT real membership change --
never a per-frame retry loop.
Live-DAT verified (ACDREAM_PROBE_LIVE_MOUNT=1, extended
SocialPanelLiveMountProbeTests): the name field builds as UiField, all
11 buttons/checkboxes resolve, the row template's 5 checked fields
resolve to the right widget types, every checkbox label/tooltip and the
Open/Close captions resolve to real retail strings ("Open"/"Close"),
and a full production-path Bind() against live DATs produces zero
"not found" warnings.
App tests: +30 (7 UiTemplateListBox/RowTemplateResolver unit tests, 23
SocialFellowshipPageControllerTests covering roster diff/rebuild,
button enable rules, checkbox wiring, create-flow gating, D4
idempotency, and D5 formatting) plus 2 CharacterOptionsPageController
counts updated for the D7 un-dim (35->31 dimmed, 15->19 live).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
IRuntimeFellowshipView.GetMembers() gives the fellowship panel a way to
enumerate the whole roster (TryGetMember alone needs the guid first,
which a UI roster build doesn't have yet). Implemented on
RuntimeFellowshipState.FellowshipView as a materialized snapshot under
the same lock every other read there uses.
SelectionChangeSource.Social covers a fellowship-roster row click
(gmFellowshipUI's list-selection arm calls the same
ACCWeenieObject::SetSelectedObject primitive every other selection
origin uses -- lane B docs/research/2026-08-11-fa-panel-structure.md
§6.2/§2.8).
Runtime tests: +4 (RuntimeFellowshipStateTests.GetMembers_*).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Narrow re-review of the FA3 fix round (9afa05b5/35c40a9b/ae772709/
a5553904/c5f73744) against this doc's 2 MUST-FIX and 9 SHOULD-FIX. Every
disposition re-derived from the diffs, not from the commit messages. All
11 correctly applied; nothing reopened; no new MUST-FIX or SHOULD-FIX.
Dispositions worth naming: MF-1/MF-2 were fixed as script REWRITES that
state the verified truth (Allegiance is both the default and the
left-most tab; @allegiance info prints chat data AND the blocks stay
hidden, report neither) rather than deletions, and MF-2's report-bullet
is correctly inverted to "blocks becoming VISIBLE is the anomaly".
SF-3's assertions pin more than asked (which page is visible, not just
that one is). SF-7 landed as a binding FA5 acceptance line plus the
forward hazard about Tick()'s unconditional LinesProvider reassignment,
the right altitude for a shell slice.
Claims verified rather than accepted: SF-1's no-register-row precedent
(grepped -- no row exists for any Toggle*Panel close-on-second-press, so
the precedent is real); the #383 timestamp correction (b4edee97
2026-08-11 09:19, e71e5a96 06:25, 74c3d85d 2026-08-12 02:58 = ~17h39m
and ~20h33m, previous day); OnShown/OnHidden really are driven by
RetailWindowHandle.NotifyVisibility and correctly do NOT fire for a
window mounted Visible=false; UiTemplateListBox.Scroll forces the
extent-seeded viewport so pre-row scrollbar wiring is sound; and the new
template cache is safe because Build is the pure builder -- only
BuildFromInfos (tests-only) mutates the ElementInfo it is handed.
Gates re-run on the post-fix Release binaries: live-mount probe passes
against the installed DATs with the promoted assertions showing
Allegiance Visible=True (other three False) and 0x10000492 count = 2;
AcDream.App.Tests 4,876 passed / 3 skipped / 0 failed, exactly +5 over
the pre-fix 4,871 and exactly the ledger's App figure, so the 13,238/4/0
+5 reconciles at the only project this round touched. Blast radius is 14
files, all FA3's own plus doc-only edits to UiTemplateListBox and
MountSocialPanel.
Carry-forwards (non-blocking): the Flush scroll-position reset will bite
harder in FA4's per-vitals-tick roster rebuild; the production template
cache has no test (both long-roster tests use the fake resolver); the
scrollbar element ids are literals where ScrollbarElementId carries the
authored value (cohort-wide nit); and the allRowsResolved retry rebuilds
per frame on a permanently unresolvable template (bounded and cheap).
FA3's remaining obligation is unchanged: the user's connected gate,
against a script that no longer contains two instructions guaranteed to
produce false defect reports.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Records the FA3 fix-round closure: both dual-lens reviews' findings
(mechanism 2 MUST-FIX/9 SHOULD-FIX, blast 1 MUST-FIX/6 SHOULD-FIX/1 NIT)
applied across four commits (9afa05b5/35c40a9b/ae772709/a5553904).
Baseline 13,233/4/0 (13,237 total) -> fix-round 13,238/4/0 (13,242
total), +5 tests, arithmetic verified directly against all 9 test
project totals. FA3 still owes the user's connected gate against the
now-corrected script.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mechanism MUST-FIX 1: the connected-gate script's steps 1/9/10 carried
the REFUTED x-order guess forward — claiming Friends was drawn left-most
and that the authored default (Allegiance) was somehow NOT the left-most
tab. The real authored order (fixture + live-mount probe, corroborated
by each page's own P0x57 action-map id) is Allegiance (x=0, DEFAULT),
Fellowship, Friends, Squelch — the default tab IS the left-most tab.
Fixed steps 1, 9, 10, and the "What to report" bullet that repeated the
wrong claim.
Mechanism MUST-FIX 2: step 14 sent the user to `@allegiance info` as a
trigger that would supposedly reveal the monarch/patron blocks, and told
them to report it if it didn't — the trigger CANNOT fire post-FA2
(0x0020 AllegianceUpdate is the only inbound writer of this panel's
data; 0x027C, the @allegiance info response, stopped seeding it in
4272ad0e) and FA3 sends no 0x001F subscription at all (FA5 scope). The
script primed the user to file a false defect. Rewritten to state the
true FA3 expectation: @allegiance info prints real data to chat, the
panel blocks stay hidden regardless, for the whole gate — report
NEITHER half as a bug; the actual anomaly to watch for is the blocks
becoming visible at all.
Mechanism SHOULD-FIX 4: step 11's Fellowship checkbox count hedge
("three... a fourth may also be present") replaced with the settled
count (four).
Blast NIT 8 / gate note: added two steps the original script never
exercised — a long-roster Friends/Squelch scroll check (exactly where
blast MF-1's scrollbar-wiring fix bites, and a short test roster would
never surface it) and an honest restore-open-across-relaunch
observation step (the social panel follows the SAME restore-open
convention every sibling main panel already has — Options/Spellbook/
Character/Inventory/Vitae — stated up front so it isn't mistaken for a
bug mid-gate).
Blast SHOULD-FIX 6: docs/ISSUES.md #383 said the two drifted fixtures
were committed "days ago" — git says otherwise: ~18h and ~21h before the
FA3 regeneration run, the previous day. Corrected, and added the
mechanism reviewer's no-drift finding for the NEW social-panel fixture
(cross-checked against the live probe on every axis, zero drift) —
narrows the issue to exactly the two pre-existing OP-era fixtures.
Blast SHOULD-FIX 7: the §10 addendum in fa-panel-structure.md had five
`**` bold markers (odd count) — an orphaned trailing marker bled bold
formatting into the following section. Dropped the orphan; the addendum
now bolds only its lead sentence and the inline "Allegiance" callout,
both balanced pairs.
Mechanism SHOULD-FIX 1 (research-doc half): filed unknown U11 in §8 —
what a repeat F3/F4 press does when the panel is open on the OTHER tab
is not established from retail decomp (no OnAction consumer exists for
either action in the binary); acdream's own OpenSpellbook-precedent
choice is not a retail port.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mechanism SHOULD-FIX 4: SocialFellowshipPageController's class doc said
0x1000026B holds "all three visible option checkboxes" — the fixture and
the decomp both authors FOUR (Ignore Fellowship Requests / Auto-Accept
Requests / Share XP / Share Loot, 0x10000270-0x10000273). Corrected.
Mechanism SHOULD-FIX 9: SocialPanelRowText.FindDeepest's doc promised
"the deepest UiText descendant" but the implementation returns the LAST
match in pre-order traversal order, which only equals the deepest when
the subtree is a single chain. Both real row templates ARE single
chains today, so behavior is unaffected — the doc now describes what the
code actually does instead of a stronger guarantee it doesn't implement.
Blast SHOULD-FIX 5: UiTemplateListBox.Flush()'s doc only mentioned the
ContentHeight reset; UiScrollablePanel.ClearContent() also resets scroll
position to 0, which the sibling UiItemList.Flush() (same method name,
different semantics) does NOT do. Documented explicitly, including the
UX cost this creates for a scrolled-in Friends/Squelch roster once its
scrollbar is wired (this fix round's blast MF-1) — flagged for whoever
revisits Friends/Squelch scrolling next rather than silently fixed as an
unasked behavior change.
Mechanism SHOULD-FIX 3: SocialPanelLiveMountProbeTests printed two
headline findings (the 0x10000492-authored-twice count, page exclusivity
after ActivateTabBehavior) without ever asserting them — a future
importer regression collapsing/dropping an instance, or breaking
exclusivity, could not fail this test. Both are now real assertions
(Assert.Equal(2, passupCount); exactly one page Visible and it is
Allegiance).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mechanism SHOULD-FIX 2 / blast SHOULD-FIX 4 (same finding, both reviews):
SocialAllegiancePageController.Tick() closed over a local `lines` on
every call — `() => lines` allocated a display class plus a delegate on
EVERY frame, unconditionally, at ~2,000 allocations/second at the
profile's measured FPS, in a hot loop the Modern Runtime slices spent
whole commits driving to 0 B/frame. Hoisted two static readonly
Func<IReadOnlyList<UiText.Line>> providers (BlankLineProvider/
NoLinesProvider); Tick() now assigns the cached delegate reference —
zero allocation while idle or active.
Mechanism SHOULD-FIX 7: FA3's empty-state gate is coarser than the
retail mechanism it is contracted against — gmAllegianceUI::UpdateMonarchData
@0x00491B40 hides the monarch/patron blocks per-relationship (monarch
block also hides when the monarch IS the viewer; patron block on the
analogous test), while this shell gates both blocks on the single
HasProfile flag. Not a MUST-FIX for FA3 (MF-2 in the same review means
HasProfile is effectively always false for the whole FA3 gate, so
nothing wrong is visible during this slice's own gate) — recorded
instead as an explicit FA5 acceptance line in the plan's FA5 row so the
gap cannot be silently lost, plus a class-doc note that FA5's real
monarch/patron population must also change Tick()'s unconditional
LinesProvider reassignment in the same commit or its content will be
overwritten the next frame.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Campaign FA slice FA3 dual-lens review fix round.
Blast MUST-FIX 1: SocialFriendsPageController/SocialSquelchPageController
never wired their ListBox's own sibling scrollbar (Friends
0x10000517->0x10000518, Squelch 0x1000053E->0x10000543) — the two lists
had NO scroll driver at all (no wheel fallback exists on
UiScrollablePanel), making any roster past the visible extent completely
unreachable, not just awkward to scroll. Both controllers now wire
`scrollbar.Model = listBox.Scroll` scoped to their own page root, exactly
like the four existing UiTemplateListBox consumers
(Character/Chat/Config/KeyboardConfig). New tests prove the wiring AND
that a >panel-height roster is actually reachable through it.
Blast SHOULD-FIX 2/3: the Friends/Squelch resolver re-ran
LayoutImporter.ImportInfos (a full DAT tree walk) under the shared DAT
lock on EVERY row, every revision, even while the panel was closed —
RetailUiRuntime.MountSocialPanel's TemplateResolver now caches each row
template's ElementInfo the first time it is resolved and never
re-Imports for that template id again. SocialPanelController additionally
gates the Friends/Squelch Tick-driven rebuild on the panel's own
visibility via the (previously unused) IRetainedPanelController
OnShown/OnHidden hooks, so no DAT-locked rebuild work runs at all while
the panel is closed. SocialFriendsPageController/SocialSquelchPageController
also now only advance _lastRevision after every row resolves — a
transient resolver miss no longer latches an empty roster until the next
server-side change; it retries on the next Tick instead.
Mechanism SHOULD-FIX 8: SocialPanelController.Tick() now returns early
once disposed, matching every other J-slice teardown discipline (it was
being ticked unconditionally forever since RetailUiRuntime never nulls
the field).
Mechanism SHOULD-FIX 1: IsShowingAllegiance's doc claimed the F3/F4
close-on-second-press semantics were "retail's Toggle-action semantics" —
re-derived and confirmed NO retail OnAction consumer exists for either
action anywhere in the binary. Relabeled as acdream's own
OpenSpellbook-precedent convention; no register row added, following the
same no-row precedent OpenSpellbook and every other non-toolbar Toggle
panel already sets. Unknown filed as U11 in the panel-structure research
doc's §8 table.
Tests: 5 new (2 scrollbar-wiring pins, 2 long-roster-reachable-via-
scrollbar, 1 hidden-panel-does-not-rebuild/shown-panel-catches-up); 2
existing tests updated for the new visibility gate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Campaign FA slice FA3 mechanism-faithfulness review of 0a9ca2f1 /
74c3d85d / b6a25110 / d7e1cffd.
The load-bearing tab-table correction VERIFIED CORRECT three independent
ways: the authored 0x2E array re-parsed straight out of the committed
fixture (Allegiance 0x1000028C -> 0x10000291 is the sole IsDefault entry),
each button's caption resolved from the installed DATs via the live-mount
probe, and each page's own P0x57 + RegisterElementClass type re-read in
the pseudo-C. A fourth corroboration the addendum missed: under the
corrected pairing the default tab is also the LEFT-MOST tab (x=0).
MUST-FIX (both in the user's connected-gate script, neither code):
1. The script still carries the REFUTED x-order -- step 9 states the strip
as "Friends, Allegiance, Fellowship, Squelch" and step 1 primes the user
to expect Friends left-most. Real geometry: Allegiance x=0, Fellowship
x=72, Friends x=144, Squelch x=206.
2. Step 14 tells the user `@allegiance info` should reveal the
monarch/patron blocks and to "report if they do not" -- FA2's own fix
round deliberately stopped 0x027C from seeding RuntimeAllegianceState,
ApplyUpdate (0x0020) is the only writer of _hasProfile, and FA3 sends
no 0x001F. The script steers the user into a false defect report.
SHOULD-FIX: unsupported "retail's Toggle-action semantics" claim on F3/F4
(no P0x57 read site, no OnAction handler, folded gmPanelUI global-message
stub -- the rule is acdream's OpenSpellbook precedent, not retail);
per-frame closure allocation in SocialAllegiancePageController.Tick; two
probe findings printed but never asserted (0x10000492 count, page
exclusivity); "three checkboxes" is four; AD-79 enumerates seven controls
but its cited test pins six; the two page controllers do not name AD-79;
the allegiance empty state is gated on HasProfile rather than retail's
per-relationship rule (TryGetMonarch/TryGetPatron already exist);
Tick() ignores _disposed; FindDeepest's doc overstates its guarantee.
Verified clean: every §6 Campaign-OP lesson (string resolver on both
Builds, scoped lookups -- I enumerated ALL duplicate ids and found three
previously-uncalled-out cross-page repeats, tab activation, no 0x0-extent
lazy children, cross-layout templates so the same-layout skip cannot
apply, no hand-rolled viewport); U6 genuinely closed (page 0x10000292 has
exactly two children); Flush/ClearContent resets ContentHeight; the J4.1
owners are borrowed by reference and clear in place; catalog id 12
byte-verified and the toolbar seam tolerates it via the same path four
existing non-toolbar panels take; window-frame policy byte-identical to
Options. data_794358 BYTE-VERIFIED in the PDB-paired binary as UTF-16LE
" " (one space, not empty) -- lane A's L" " reading and FA3's BlankLine
both correct. Live-mount probe passes against the installed DATs with no
fixture drift; 22 FA3 tests and 4,871 App tests / 3 skips / 0 failures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MUST-FIX 1: the Friends/Squelch lists have NO scroll driver. Both
controllers set TemplateResolver but never wire the authored scrollbar's
Model (0x10000518 / 0x10000543, both direct siblings of their ListBox
under the page root per FA3's own fixture), unlike all four existing
UiTemplateListBox consumers. There is no wheel fallback -- wheel scroll
lives only on UiText, not UiScrollablePanel -- so Scroll has no driver at
all. Visible at authored size: the 400/430-tall ListBoxes sit at y=40 in
a 362-tall panel, so rows past ~322px are off-panel AND unreachable.
AD-79 covers the inert BUTTONS, not a dead scrollbar.
SHOULD-FIX: the revision-driven rebuild does N live DAT imports under the
shared DatLock while the panel is CLOSED (first repeating consumer of a
resolver every other caller invokes once at Bind); Refresh() consumes the
revision before building so one resolver miss latches an empty list;
per-frame closure allocation in the hidden allegiance page; Flush()'s doc
omits its scroll reset and diverges from the sibling UiItemList.Flush()
it shares a name with; #383's "days ago" is really ~18-21h per git;
the SS10 addendum's trailing ** is orphaned.
Verified clean: catalog/window-name consumers all degrade safely
(SetPanelOpen(12) is a no-op, opacity controller correctly scoped by
#379); persistence omission is the documented cohort behavior; F3/F4
plumbing predates FA3 entirely (UI.Abstractions untouched) and the
apparent bare-F3 duplicate is in the non-production AcdreamCurrentDefaults;
mount order respects the DialogFactory constraint; SocialRuntimeBindings
is required-positional with one construction site; the generator is
env-gated and only the new fixture landed; +18 reconciles exactly
(23 targeted + 289 blast-radius regression tests pass on the FA3 binary);
AD-79 well-formed with the count bumped 58->59; and the corrected tab
table was independently re-read from the fixture's own TabTable
(0x1000028C -> 0x10000291, IsDefault=true -- Allegiance IS the default).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
docs/research/2026-08-11-fa-panel-structure.md §10's coordinator
addendum inferred the social panel's button-to-page pairing from
authored x-order, landing on Friends as the implied default tab. The
FA3 fixture dump read the real authored 0x2E tab table: Allegiance
(button 0x1000028C) is the actual default, corroborated independently
by each page's own P0x57 lining up with the real F3/F4 ActionMap ids.
Same convention as the FA1/FA2 fix-round addenda already in this
campaign's docs -- correct in place, keep the original text visible.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- docs/research/2026-08-12-campaign-fa-test-script.md: the user's
connected-gate script for FA3 -- open paths (F3/F4 + tab switch),
gmPanelUI exclusivity vs sibling panels, tab switching, all four
pages' expected shells/empty states, Friends/Squelch read-only
expectations + the D1 INERT buttons, what to report, what's
explicitly out of scope (FA4/FA5/FA6/D1's deferred wire).
- Plan ledger: FA3 row filled in with commit SHAs, totals (13,233/4/0,
13,237 total, +18 over FA2's close), the tab-table correction finding,
and the reverted unrelated fixture drift note. Campaign status line
updated from "FA3 in flight" to implementation-complete pending
review + gate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- RetailLayoutFixtureGenerator dumps LayoutDesc 0x2100006E slot
0x1000018F to the committed social_panel_2100006E_1000018F.json
fixture, closing lane-A unknowns U3/U4/U6/U7/U10 with real DAT data
(geometry/media/fonts/base refs, the fellowship empty/full frame
containment, the panel/page P0x57 properties, the row-template arrays).
- SocialPanelLiveMountProbeTests exercises the PRODUCTION mount path
against the live DATs (ACDREAM_PROBE_LIVE_MOUNT=1): the tab host
resolves as UiTabPanel with its 4-entry table, all four pages resolve,
the fellowship frame pair and allegiance signature elements resolve,
0x10000492 is confirmed authored twice under the allegiance page, and
every tab button caption is non-empty (the #375 resolver class).
- SocialPanelControllerTests pins the fixture-driven conformance: the
real (coordinator-addendum-correcting) tab table and default entry,
the Fellowship/Allegiance empty-state gates, Friends/Squelch row
population and revision-driven rebuilds, and the D1 INERT-button
contract (AD-79) via
FriendsAndSquelchActionButtons_AreClickable_ButHaveNoHandler.
- RetailPanelCatalogTests gains the SocialPanel id/window-name/
Mounted-not-Toolbar pins (lane A §6.1: no toolbar button).
Note: the ACDREAM_REGENERATE_UI_FIXTURES=1 run used to produce the new
fixture also touched keyboard_config_21000009.json and
options_2100002B.json on this machine (unrelated installed-DAT drift,
likely from local DAT-editing tooling) -- both were reverted to HEAD
before this commit; only the new social panel fixture is included.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Campaign FA slice FA3: retail's four-tab social panel (LayoutDesc
0x2100006E slot 0x1000018F, RetailPanelCatalog id 12), built on the OP3
OptionsPanelController recipe -- Type-8 tab host, ActivateTabBehavior,
per-page scoped controllers.
- SocialPanelController mounts the tab host and wires the close button;
F3 (ToggleAllegiancePanel) and F4 (ToggleFellowshipPanel) open the
panel and switch to their own tab, sharing the same gmPanelUI
one-active-panel exclusivity every sibling main panel already has.
- The live-DAT tab table CORRECTS the coordinator addendum's x-order
guess: button 0x1000028C ("Allegiance") pairs with page 0x10000291 and
is the authored DEFAULT entry, not Friends -- each button's own page
id and its own P0x57 (matching the F3/F4 ActionMap ids on the
Allegiance/Fellowship pages specifically) both corroborate the real
pairing. See SocialPanelController's class doc for the full table.
- SocialFellowshipPageController swaps the two authored empty/full
frames (0x1000026B/0x10000275) on RuntimeFellowshipState's
IsInFellowship -- both frames' full containment (name box, create
button, checkboxes vs. roster list, six buttons) was confirmed by the
live-mount probe, so a single Visible toggle per frame is the whole
swap (closes lane-A unknown U6).
- SocialAllegiancePageController hides the monarch/patron blocks and
blanks their name text to a literal space when
RuntimeAllegianceState.Snapshot.HasProfile is false, using SCOPED
FindDescendant lookups (the panel authors 0x10000492 twice, once per
block).
- SocialFriendsPageController/SocialSquelchPageController bind their
ListBoxes read-only to RuntimeCommunicationState's existing J4.1
Friends/Squelch owners (names only), rebuilding on revision change.
Their action buttons are honest INERT (D1) -- register row AD-79.
- UiTemplateListBox gains Flush() (lane A/D's "Gap found" prerequisite)
so a poll-and-rebuild list can shrink between refreshes.
- RetailPanelCatalog.SocialPanel = 12, byte-verified from the live slot's
own P0x10000029; listed in Mounted only (no toolbar button -- lane A
§6.1: the open path is keyboard-only).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The narrow re-review (cc1a319c) closed FA2 with no reopen; its
carry-forward corrected the FA5 row: the allegiance page's data
subscription is 0x001F AllegianceUpdateRequest(on) at retail's three
arming points, not the text-only 0x027B info request.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All 2 MUST-FIX and all 6 SHOULD-FIX dispositions re-derived in the actual
fix-round diffs (4272ad0e, ded23067, ed8b3ec9); every blast disposition
spot-verified. Nothing skipped, no fix introduced a new mechanism defect.
MF-1: RuntimeGenerationResetStage.Allegiance added and drained, the owner
clears the profile AND drops HasServerSeed, the inverted test flipped. Stage
enumeration verified consistent everywhere -- every reference outside the
enum's own file is by NAME, nothing serializes the ordinal, so the +1 shift
is inert. MF-2: ApplyInfoResponseSelf, the delegate hole and the self-gate
are all gone (0 whole-tree hits); the test was rewritten to pin text-only
output for self and other guids alike.
SF-3's RecalculateEvenXPSplitting port checked line-for-line against lane B
2.10, including the deliberate leaderless-table departure -- lane B 7.4 says
verbatim "treat a leaderless table as leave _even_xp_split at 1", so the
citation is accurate. SF-4's 900s gate confirmed to have real data (FA1 does
parse 0x02BE field 8) and to gate only the new-guid branch. SF-1/2/5/6 all
land as specified.
Blast: the teardown table re-derived for every N in 0..13 (case 9 was
genuinely one flag over); the new reflection walk pins every intermediate
stage; seam-doc and plan addenda are dated and accurate; the corrected 11/10
counts are right. Audited blast SF-6's no-register-row conclusion and AGREE
-- clear-at-reset plus 0x0020-only seeding means acdream now matches retail,
so no deviation remains for a row to name.
Suite claim 13,201/4/0 -> 13,215/4/0 (+14) reproduced exactly by counting
discovered cases per file. Targeted post-fix Release runs: 95/95 Runtime,
67/67 Core.Net.
CF-1 (FA5, not a reopen): nothing re-subscribes 0x001F now that the reset
clears the owner, and the plan's FA5 row cites 0x027B for the panel-show
path -- which after MF-2 is text-only and feeds nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plan (docs/plans/2026-08-11-fellowship-allegiance-campaign.md):
- D2 gets the D9-style dated strike/addendum recording the corrected
allegiance reset semantics (clears at every generation reset; the
HasServerSeed latch only gates pre-seed rendering WITHIN a session) with
the three-way evidence citation: the retail OnEndCharacterSession hook,
the RuntimeCharacterOptionsState precedent's actual clear-and-relatch
behavior, and the no-character-selector connect path
(SessionPlayerComposition.cs:1127).
- The architecture blurb and FA2's slice-map contract row get matching
strike/addendum corrections so the "fellowship session-scoped,
allegiance survives reconnect" claim does not survive uncorrected
anywhere in the plan.
- FA2's ledger row: fix-round commit SHAs, corrected delegate-hole/
wrapper counts (blast SHOULD-FIX 3: 10 not 15, 11 not 12), the
allegiance register-row re-evaluation conclusion (blast SHOULD-FIX 6 --
no row needed, MF-1's fix retires the deviation entirely), and the
reconciled fix-round test totals (13,201/4/0 -> 13,215/4/0, +14,
arithmetic exact per file).
Seam map (docs/research/2026-08-11-fa-acdream-seams.md), per the FA1
fix-round's established in-place-correction convention:
- SS1.3 and SS9's dispatcher-replaces-not-chains correction is now dated
and cites the actual GameEventDispatcher.Dispatch behavior, matching the
code comment already landed in GameEventWiring.cs.
- SS2.3 gets the 0x01C9/0x01CA disposition it was missing (correctly
left unregistered -- dead COMDAT-fold no-ops per FA1) so FA3 does not
have to re-derive it or "fix" the gap.
- The SS8 seam-map table's Allegiance-owner row and the executive-summary
ownership bullet both get the "survives reconnect" claim struck with a
dated correction to "session-scoped, clears at every generation reset".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Remaining SHOULD-FIX findings from the FA2 mechanism/blast reviews:
Mechanism SF-3/SF-4 -- RuntimeFellowshipState.ApplyUpdateFellow now ports
Fellowship::RecalculateEvenXPSplitting @0x005B92E0 (called from retail's
AddFellow/UpdateFellow/RemoveFellow on every upsert/removal, but never
from a full update -- that carries the server's own authoritative flag
verbatim, lane B 6.2) and Fellowship::AddFellow @0x005B9480's
locked/departed admission gate (a brand-new guid is refused while
_locked unless it appears in the 0x02BE field-8 _fellows_departed table
within 900s, @0x005B94A5). ApplyFullUpdate now stores update.Departed
instead of discarding it. A TimeProvider dependency (defaulting to
TimeProvider.System, matching the RuntimeCharacterOptionsState precedent)
makes the 900s grace window testable.
Mechanism SF-5 -- RuntimeAllegianceState's TryGetMember/TryGetPatron/
GetVassals now reuse ClientCommandResponses.AllegianceProfileLookups
(promoted private -> internal, AcDream.Runtime added to Core.Net's
InternalsVisibleTo) instead of re-implementing the retail walk a second
time.
Mechanism SF-6 -- RuntimeStateCheckpoint's Fellowship/Allegiance
parameters are no longer trailing-optional. `default(RuntimeFellowshipSnapshot)`/
`default(RuntimeAllegianceSnapshot)` zero-init Name/AllegianceName to
null, and C# does not allow a non-constant `new(...)` as an optional
parameter's default value (CS1736) even when the struct declares an
explicit parameterless constructor -- so the only way to guarantee a
non-null default was to make the parameters required. Both snapshot types
still gained an explicit parameterless constructor for callers that want
an empty-but-safe `new()`.
Blast SF-4 -- LiveSessionEventRouterTests gains
FellowshipQuit_RoutesSelfGuidToClearAndOtherGuidToRemove, wiring real
RuntimeFellowshipState/RuntimeAllegianceState owners through the one
production registration site and dispatching a real 0x00A3 envelope for
both a self-quit and an other-quit -- the one non-trivial lambda in the
slice (the self-guid source that decides "remove one member" vs "clear
the whole snapshot") was previously untested; every other router test
defaults Fellowship/Allegiance to null.
Blast SF-5 -- RuntimeFellowshipState.ResetSession dropped its disposed
guard to match the precedent its own doc comment names
(RuntimeInventoryState.ResetExternalContainer,
RuntimeCommunicationState.ResetNegotiatedChannels -- both bare delegations
with no disposal guard); the reset transaction is retryable and disposal
is terminal, so a throwing guard could never converge on retry.
RuntimeAllegianceState.ResetSession (new this fix round) matches the same
shape from the start.
Blast SF-7 -- IRuntimeAllegianceView.GetVassals' per-call List<> allocation
is now documented as an intentional exception to the file's "Snapshot +
TryGet*, no allocation" view convention (C# cannot yield-return from
inside a lock).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>