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

@ -34,8 +34,11 @@ public enum GameRuntimeTeardownStage
CharacterDisposed = 1 << 6,
InventoryDisposed = 1 << 7,
CommunicationDisposed = 1 << 8,
IdentityDisposed = 1 << 9,
EntityObjectsDisposed = 1 << 10,
// Campaign FA slice FA2 (2026-08-12): the two sibling J-owners.
FellowshipDisposed = 1 << 9,
AllegianceDisposed = 1 << 10,
IdentityDisposed = 1 << 11,
EntityObjectsDisposed = 1 << 12,
Complete =
HostLeasesReleased
| EventsDetached
@ -46,6 +49,8 @@ public enum GameRuntimeTeardownStage
| CharacterDisposed
| InventoryDisposed
| CommunicationDisposed
| FellowshipDisposed
| AllegianceDisposed
| IdentityDisposed
| EntityObjectsDisposed,
}
@ -87,6 +92,8 @@ internal enum GameRuntimeConstructionPoint
InventoryCreated,
CharacterCreated,
CommunicationCreated,
FellowshipCreated,
AllegianceCreated,
MovementCreated,
ActionsCreated,
EnvironmentCreated,
@ -102,6 +109,8 @@ internal sealed class GameRuntimeConstructionContext
public RuntimeInventoryState? Inventory { get; set; }
public RuntimeCharacterState? Character { get; set; }
public RuntimeCommunicationState? Communication { get; set; }
public RuntimeFellowshipState? Fellowship { get; set; }
public RuntimeAllegianceState? Allegiance { get; set; }
public RuntimeLocalPlayerMovementState? Movement { get; set; }
public RuntimeActionState? Actions { get; set; }
public GameRuntimeEventHub? Events { get; set; }
@ -117,7 +126,7 @@ public sealed class GameRuntime
IRuntimeEventSource,
IDisposable
{
private const int TeardownStageCount = 11;
private const int TeardownStageCount = 13;
private readonly object _lifetimeGate = new();
private readonly Dictionary<long, string> _hostLeases = [];
@ -213,6 +222,24 @@ public sealed class GameRuntime
context,
faultInjection);
// Campaign FA slice FA2 (2026-08-12): two sibling J-owners, not
// children of Communication — the reset semantics differ
// (fellowship is session-scoped, allegiance survives reconnect;
// docs/research/2026-08-11-fa-acdream-seams.md §1.3).
context.Fellowship = new RuntimeFellowshipState();
construction.Own(context.Fellowship);
Fault(
GameRuntimeConstructionPoint.FellowshipCreated,
context,
faultInjection);
context.Allegiance = new RuntimeAllegianceState();
construction.Own(context.Allegiance);
Fault(
GameRuntimeConstructionPoint.AllegianceCreated,
context,
faultInjection);
context.Movement = new RuntimeLocalPlayerMovementState();
// Campaign CH slice CH2: local jump refusals (CommenceJump/
// DoJump's WeenieError family — research doc §4.2/§6.4) reach
@ -265,7 +292,8 @@ public sealed class GameRuntime
context.Movement,
context.EntityObjects,
context.Character,
context.PlayerIdentity);
context.PlayerIdentity,
context.Fellowship);
context.Movement.AttachPhysicsPublication(
new RuntimeLocalPlayerPhysicsPublicationState(
@ -318,6 +346,8 @@ public sealed class GameRuntime
InventoryOwner = context.Inventory;
CharacterOwner = context.Character;
CommunicationOwner = context.Communication;
FellowshipOwner = context.Fellowship;
AllegianceOwner = context.Allegiance;
MovementOwner = context.Movement;
ActionOwner = context.Actions;
EnvironmentOwner = environment;
@ -421,6 +451,8 @@ public sealed class GameRuntime
public RuntimeInventoryState InventoryOwner { get; }
public RuntimeCharacterState CharacterOwner { get; }
public RuntimeCommunicationState CommunicationOwner { get; }
public RuntimeFellowshipState FellowshipOwner { get; }
public RuntimeAllegianceState AllegianceOwner { get; }
public RuntimeActionState ActionOwner { get; }
public RuntimeLocalPlayerMovementState MovementOwner { get; }
internal RuntimeLocalPlayerPhysicsPublicationState
@ -464,6 +496,8 @@ public sealed class GameRuntime
public IRuntimeCharacterView Character => CharacterOwner.View;
public IRuntimeSocialView Social => CommunicationOwner.SocialView;
public IRuntimeChatView Chat => CommunicationOwner.View;
public IRuntimeFellowshipView Fellowship => FellowshipOwner.View;
public IRuntimeAllegianceView Allegiance => AllegianceOwner.View;
public IRuntimeActionView Actions => ActionOwner.View;
public IRuntimeMovementView Movement => MovementOwner.View;
public IRuntimeWorldEnvironmentView Environment => EnvironmentOwner;
@ -490,7 +524,9 @@ public sealed class GameRuntime
Environment.Snapshot,
Environment.Ownership,
Portal.Snapshot,
Portal.Ownership);
Portal.Ownership,
Fellowship.Snapshot,
Allegiance.Snapshot);
public RuntimeLocalPlayerFrameController CreateLocalPlayerFrameController(
IRuntimeLocalPlayerFrameHost host,
@ -560,7 +596,9 @@ public sealed class GameRuntime
CharacterOwner,
CommunicationOwner,
ActionOwner,
MovementOwner),
MovementOwner,
FellowshipOwner,
AllegianceOwner),
EnvironmentOwner.CaptureOwnership(),
TransitOwner.CaptureOwnership(),
GenerationReset.CaptureSnapshot(),
@ -675,10 +713,24 @@ public sealed class GameRuntime
| GameRuntimeTeardownStage.MovementDisposed
| GameRuntimeTeardownStage.CharacterDisposed
| GameRuntimeTeardownStage.InventoryDisposed,
9 => GameRuntimeTeardownStage.Complete
9 => GameRuntimeTeardownStage.HostLeasesReleased
| GameRuntimeTeardownStage.EventsDetached
| GameRuntimeTeardownStage.SessionDisposed
| GameRuntimeTeardownStage.TransitReset
| GameRuntimeTeardownStage.ActionsDisposed
| GameRuntimeTeardownStage.MovementDisposed
| GameRuntimeTeardownStage.CharacterDisposed
| GameRuntimeTeardownStage.InventoryDisposed
| GameRuntimeTeardownStage.CommunicationDisposed
| GameRuntimeTeardownStage.FellowshipDisposed,
10 => GameRuntimeTeardownStage.Complete
& ~GameRuntimeTeardownStage.AllegianceDisposed
& ~GameRuntimeTeardownStage.IdentityDisposed
& ~GameRuntimeTeardownStage.EntityObjectsDisposed,
10 => GameRuntimeTeardownStage.Complete
11 => GameRuntimeTeardownStage.Complete
& ~GameRuntimeTeardownStage.IdentityDisposed
& ~GameRuntimeTeardownStage.EntityObjectsDisposed,
12 => GameRuntimeTeardownStage.Complete
& ~GameRuntimeTeardownStage.EntityObjectsDisposed,
_ => GameRuntimeTeardownStage.Complete,
};
@ -724,9 +776,15 @@ public sealed class GameRuntime
CommunicationOwner.Dispose();
return CommunicationOwner.CaptureOwnership().IsConverged;
case 9:
FellowshipOwner.Dispose();
return FellowshipOwner.CaptureOwnership().IsConverged;
case 10:
AllegianceOwner.Dispose();
return AllegianceOwner.CaptureOwnership().IsConverged;
case 11:
PlayerIdentity.Dispose();
return PlayerIdentity.CaptureOwnership().IsConverged;
case 10:
case 12:
EntityObjects.Dispose();
return EntityObjects.CaptureOwnership().IsConverged
&& EntityObjects.Physics.CaptureOwnership().IsConverged;
@ -747,8 +805,10 @@ public sealed class GameRuntime
6 => CharacterOwner.CaptureOwnership().IsConverged,
7 => InventoryOwner.CaptureOwnership().IsConverged,
8 => CommunicationOwner.CaptureOwnership().IsConverged,
9 => PlayerIdentity.CaptureOwnership().IsConverged,
10 => EntityObjects.CaptureOwnership().IsConverged
9 => FellowshipOwner.CaptureOwnership().IsConverged,
10 => AllegianceOwner.CaptureOwnership().IsConverged,
11 => PlayerIdentity.CaptureOwnership().IsConverged,
12 => EntityObjects.CaptureOwnership().IsConverged
&& EntityObjects.Physics.CaptureOwnership().IsConverged,
_ => true,
};