feat(runtime): FA2 -- RuntimeFellowshipState + RuntimeAllegianceState sibling J-owners

Two new sibling Runtime owners under GameRuntime per the Slice-J
pattern (D2): RuntimeFellowshipState is session-scoped (a new
RuntimeGenerationResetStage.Fellowship clears it at every generation
reset, matching the ExternalContainer precedent); RuntimeAllegianceState
survives reconnect behind a HasServerSeed-style one-way latch and
participates in NO reset stage (its data persists like a real
disconnect does not sever allegiance membership).

Fellowship: full-update REPLACE, incremental-fellow UPSERT, self-vs-
other quit/dismiss removal (self clears the whole snapshot), disband
clear, and retail's leader hand-off rule for the Quit button
(RequiresLeaderHandoffBeforeQuit -- the current leader quitting WITHOUT
disbanding must send 0x0290 AssignNewLeader before 0x00A3, lane B
§2.5/§3.6).

Allegiance: seeded by AllegianceUpdate (0x0020, always self) and,
self-gated on TargetGuid == playerGuid(), by AllegianceInfoResponse
(0x027C); wraps the FA1-assembled flat AllegianceMemberRecord list
directly (AllegianceTree was deleted at FA1 -- nothing left to wrap).

Both apply the full 8-edit J-owner template: construction + fault
points + Owner/View properties + CaptureOwnership + a new
GameRuntimeTeardownStage pair (FellowshipDisposed/AllegianceDisposed,
stage count 11->13) + RuntimeGameplayOwnershipSnapshot inclusion +
RuntimeStateCheckpoint/trace fields. IRuntimeFellowshipCommands/
IRuntimeAllegianceCommands added to IGameRuntimeCommands and
implemented on DirectGameRuntimeCommandAdapter. No IRuntimeEventObserver
member added (D2) -- consumers poll Snapshot.Revision.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-12 01:31:54 +02:00
parent 1c40104896
commit 369729f06a
19 changed files with 1949 additions and 35 deletions

View file

@ -32,15 +32,23 @@ public enum RuntimeGenerationResetStage
Friends = 9,
Squelch = 10,
NegotiatedChannels = 11,
BeginEntityRetirement = 12,
RetireEntities = 13,
DrainHostProjection = 14,
CompleteCanonicalEntities = 15,
CompleteHostProjection = 16,
ChatIdentity = 17,
PlayerSnapshots = 18,
PlayerIdentity = 19,
Complete = 20,
/// <summary>
/// Campaign FA slice FA2 (2026-08-12): the fellowship roster is
/// session-scoped (a disconnect drops you from the fellowship
/// server-side) — clear it here, alongside the other social-list
/// stages. Allegiance is deliberately NOT a reset stage — it survives
/// reconnect (see <see cref="RuntimeAllegianceState"/>'s class doc).
/// </summary>
Fellowship = 12,
BeginEntityRetirement = 13,
RetireEntities = 14,
DrainHostProjection = 15,
CompleteCanonicalEntities = 16,
CompleteHostProjection = 17,
ChatIdentity = 18,
PlayerSnapshots = 19,
PlayerIdentity = 20,
Complete = 21,
}
public readonly record struct RuntimeGenerationResetSnapshot(
@ -88,6 +96,7 @@ public sealed class RuntimeGenerationReset
private readonly RuntimeEntityObjectLifetime _entityObjects;
private readonly RuntimeCharacterState _character;
private readonly RuntimeLocalPlayerIdentityState _identity;
private readonly RuntimeFellowshipState _fellowship;
private ResetState? _state;
private RuntimeGenerationToken _lastCompletedGeneration;
private bool _hasCompletedGeneration;
@ -102,7 +111,8 @@ public sealed class RuntimeGenerationReset
RuntimeLocalPlayerMovementState movement,
RuntimeEntityObjectLifetime entityObjects,
RuntimeCharacterState character,
RuntimeLocalPlayerIdentityState identity)
RuntimeLocalPlayerIdentityState identity,
RuntimeFellowshipState fellowship)
{
_transit = transit ?? throw new ArgumentNullException(nameof(transit));
_communication = communication
@ -118,6 +128,8 @@ public sealed class RuntimeGenerationReset
?? throw new ArgumentNullException(nameof(character));
_identity = identity
?? throw new ArgumentNullException(nameof(identity));
_fellowship = fellowship
?? throw new ArgumentNullException(nameof(fellowship));
}
public RuntimeGenerationToken? ActiveRetiringGeneration =>
@ -285,6 +297,9 @@ public sealed class RuntimeGenerationReset
state,
_communication.ResetNegotiatedChannels);
break;
case RuntimeGenerationResetStage.Fellowship:
Advance(state, _fellowship.ResetSession);
break;
case RuntimeGenerationResetStage.BeginEntityRetirement:
_ = _entityObjects.BeginSessionClear();
state.Retirements = _entityObjects