fix(net,runtime): FA2 fix-round MUST-FIX -- allegiance clears at reset, 0x027C stops seeding
Two MUST-FIX findings from the FA2 mechanism/blast reviews (docs/research/2026-08-12-fa2-review-mechanism.md, docs/research/2026-08-12-fa2-review-blast.md): MF-1 (mechanism) -- RuntimeAllegianceState survived a generation reset, contradicting retail (ClientAllegianceSystem::OnEndCharacterSession @0x00569FA0 tail-calls AllegianceProfile::Clear at the same boundary Fellowship already clears at), contradicting the precedent it cited (RuntimeCharacterOptionsState.ResetSession clears-and-relatches, it does not persist), and pinned by a test asserting the wrong behavior. Fixed: RuntimeAllegianceState.ResetSession() clears the profile and drops HasServerSeed; a new RuntimeGenerationResetStage.Allegiance stage runs it on every generation reset, mirroring RuntimeFellowshipState exactly. RuntimeGenerationResetTests' FellowshipClearsAtResetButAllegianceSurvivesReconnect inverted to FellowshipAndAllegianceBothClearAtGenerationReset. MF-2 (mechanism) / blast MF-2 -- 0x027C AllegianceInfoResponse fed the Runtime allegiance owner (self-gated). Retail's own handler for 0x027C (CM_Allegiance::DispatchUI_AllegianceInfoResponseEvent @0x006a7470) unpacks into a stack-local profile destroyed on return; the consumer (Handle_Allegiance__AllegianceInfoResponseEvent @0x0056a1d0) only prints AddTextToScroll lines. Retail's panel is fed exclusively by 0x0020 AllegianceUpdate. The removed seeding also fabricated RuntimeAllegianceSnapshot.Rank (0x027C carries no rank field) on any client whose first allegiance message was a self @allegiance info query. Fixed: dropped ApplyInfoResponseSelf, the onAllegianceInfoResponseSelf delegate hole, and the self-gate; 0x027C is text-only again, matching retail and the pre-FA2 shape. Also covers blast SHOULD-FIX 1 in the same edit to LiveSessionEventRouter.cs: the fellowship/allegiance delegate holes are now passed conditionally on the owner being supplied, so GameEventDispatcher.GetUnhandledCount reads correctly for callers without an owner (bare-ChatLog tests, a future partial host) instead of silently reading 0 for 9 event types whose parse result was discarded. RuntimeAllegianceState.cs and the two owners' Apply* mutators also move their ObjectDisposedException.ThrowIf checks inside the lock they already take (mechanism SHOULD-FIX 2) -- the prior check-then-lock shape let an inbound event on the decode thread race Dispose on the host thread and repopulate state after _disposed = true, permanently falsifying CaptureOwnership().IsConverged at teardown. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
63649c8053
commit
4272ad0ea4
9 changed files with 425 additions and 229 deletions
|
|
@ -110,11 +110,6 @@ public static class GameEventWiring
|
|||
Action<uint /*dismissedGuid*/>? onFellowshipDismiss = null,
|
||||
Action? onFellowshipDisband = null,
|
||||
Action<ClientCommandResponses.AllegianceUpdate>? onAllegianceUpdate = null,
|
||||
// Self-gated: fires only when the response's TargetGuid is the
|
||||
// local player's own guid (see the registration below) — a
|
||||
// by-name @allegiance info query on ANOTHER player must not
|
||||
// overwrite the Runtime allegiance owner's own-tree snapshot.
|
||||
Action<ClientCommandResponses.AllegianceInfoResponse>? onAllegianceInfoResponseSelf = null,
|
||||
Action<uint /*weenieError*/>? onAllegianceUpdateDone = null,
|
||||
Action<uint /*weenieError*/>? onAllegianceUpdateAborted = null,
|
||||
Action<GameEvents.AllegianceLoginNotification>? onAllegianceLoginNotification = null)
|
||||
|
|
@ -218,28 +213,31 @@ public static class GameEventWiring
|
|||
// superseded handler only comes back if the newer registration's
|
||||
// token is later disposed). The seam doc's "the dispatcher supports
|
||||
// multiple owned handlers per type — both fire" claim
|
||||
// (docs/research/2026-08-11-fa-acdream-seams.md §2.3) does not hold
|
||||
// against the actual dispatcher; a literal second Register call
|
||||
// here would have silently killed the already-live `@allegiance
|
||||
// info` chat-text output the moment a caller supplied
|
||||
// onAllegianceInfoResponseSelf. Both behaviors are folded into this
|
||||
// ONE registration instead. Self-gated on TargetGuid == playerGuid()
|
||||
// so a by-name query against ANOTHER player's allegiance never
|
||||
// overwrites the Runtime owner's own-tree snapshot; skipped
|
||||
// entirely when no playerGuid resolver was supplied (matches every
|
||||
// other playerGuid-gated site above).
|
||||
// (docs/research/2026-08-11-fa-acdream-seams.md §2.3, dated addendum
|
||||
// added) does not hold against the actual dispatcher — a second
|
||||
// Register call here would silently kill this already-live
|
||||
// `@allegiance info` chat-text output.
|
||||
//
|
||||
// FA2 fix-round MUST-FIX 2 (2026-08-12,
|
||||
// docs/research/2026-08-12-fa2-review-mechanism.md /
|
||||
// docs/research/2026-08-12-fa2-review-blast.md): this handler
|
||||
// previously ALSO forwarded to a Runtime allegiance-owner callback
|
||||
// (self-gated on TargetGuid == playerGuid()). Retail's own handler
|
||||
// for 0x027C (CM_Allegiance::DispatchUI_AllegianceInfoResponseEvent
|
||||
// @0x006a7470) unpacks into a STACK-LOCAL profile destroyed on
|
||||
// return and is consumed only by
|
||||
// Handle_Allegiance__AllegianceInfoResponseEvent @0x0056a1d0's
|
||||
// AddTextToScroll calls — retail's allegiance panel is fed
|
||||
// EXCLUSIVELY by 0x0020 AllegianceUpdate. Forwarding also fabricated
|
||||
// RuntimeAllegianceSnapshot.Rank (0x027C carries no rank field) on
|
||||
// any client whose first allegiance message was a self
|
||||
// `@allegiance info` query. Restored to text-only, matching retail.
|
||||
registrar.Register(GameEventType.AllegianceInfoResponse, e =>
|
||||
{
|
||||
var info = ClientCommandResponses.ParseAllegianceInfoResponse(e.Payload.Span);
|
||||
if (info is null) return;
|
||||
foreach (string line in ClientCommandResponses.FormatAllegianceInfoLines(info.Value))
|
||||
chat.OnSystemMessage(line, chatType: 0u);
|
||||
if (onAllegianceInfoResponseSelf is not null
|
||||
&& playerGuid is not null
|
||||
&& info.Value.TargetGuid == playerGuid())
|
||||
{
|
||||
onAllegianceInfoResponseSelf(info.Value);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Fellowship (Campaign FA slice FA2, 2026-08-12) ──────────────
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue