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

@ -28,6 +28,8 @@ public sealed class DirectGameRuntimeCommandAdapter
IRuntimeSpellbookCommands,
IRuntimeCharacterCommands,
IRuntimeSocialCommands,
IRuntimeFellowshipCommands,
IRuntimeAllegianceCommands,
IRuntimeInteractionTransport
{
private sealed class CommandRoute(
@ -80,6 +82,8 @@ public sealed class DirectGameRuntimeCommandAdapter
public IRuntimeSpellbookCommands Spellbook => this;
public IRuntimeCharacterCommands Character => this;
public IRuntimeSocialCommands Social => this;
public IRuntimeFellowshipCommands Fellowship => this;
public IRuntimeAllegianceCommands Allegiance => this;
public ILiveSessionCommandRouting CreateRoute(WorldSession session)
{
@ -795,6 +799,262 @@ public sealed class DirectGameRuntimeCommandAdapter
command.Name);
}
// ── Fellowship / Allegiance (Campaign FA slice FA2, 2026-08-12) ────────
public RuntimeCommandResult Create(
RuntimeGenerationToken expectedGeneration,
string fellowshipName,
bool shareXp)
{
RuntimeCommandStatus gate =
Validate(expectedGeneration, out WorldSession? session);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
if (string.IsNullOrWhiteSpace(fellowshipName))
{
return EmitUnsupported(
RuntimeCommandDomain.Fellowship,
operation: 0,
RuntimeCommandStatus.Rejected);
}
session!.SendFellowshipCreate(fellowshipName, shareXp);
return EmitResult(
RuntimeCommandDomain.Fellowship,
operation: 0,
RuntimeCommandStatus.Accepted);
}
public RuntimeCommandResult Recruit(
RuntimeGenerationToken expectedGeneration,
uint targetGuid)
{
RuntimeCommandStatus gate =
Validate(expectedGeneration, out WorldSession? session);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
if (targetGuid == 0u)
{
return EmitUnsupported(
RuntimeCommandDomain.Fellowship,
operation: 1,
RuntimeCommandStatus.Rejected,
targetGuid);
}
session!.SendFellowshipRecruit(targetGuid);
return EmitResult(
RuntimeCommandDomain.Fellowship,
operation: 1,
RuntimeCommandStatus.Accepted,
targetGuid);
}
public RuntimeCommandResult Dismiss(
RuntimeGenerationToken expectedGeneration,
uint targetGuid)
{
RuntimeCommandStatus gate =
Validate(expectedGeneration, out WorldSession? session);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
if (targetGuid == 0u)
{
return EmitUnsupported(
RuntimeCommandDomain.Fellowship,
operation: 2,
RuntimeCommandStatus.Rejected,
targetGuid);
}
session!.SendFellowshipDismiss(targetGuid);
return EmitResult(
RuntimeCommandDomain.Fellowship,
operation: 2,
RuntimeCommandStatus.Accepted,
targetGuid);
}
/// <summary>
/// Retail's Quit-button leader hand-off (lane B §2.5/§3.6): the current
/// leader quitting WITHOUT disbanding sends <c>0x0290</c> (assign a
/// non-leader fellow) BEFORE <c>0x00A3</c> disband=0.
/// </summary>
public RuntimeCommandResult Quit(
RuntimeGenerationToken expectedGeneration,
bool disband)
{
RuntimeCommandStatus gate =
Validate(expectedGeneration, out WorldSession? session);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
if (_runtime.FellowshipOwner.RequiresLeaderHandoffBeforeQuit(
_runtime.PlayerIdentity.ServerGuid,
disband,
out uint newLeaderGuid))
{
session!.SendFellowshipAssignNewLeader(newLeaderGuid);
}
session!.SendFellowshipQuit(disband);
return EmitResult(
RuntimeCommandDomain.Fellowship,
operation: 3,
RuntimeCommandStatus.Accepted);
}
public RuntimeCommandResult AssignLeader(
RuntimeGenerationToken expectedGeneration,
uint newLeaderGuid)
{
RuntimeCommandStatus gate =
Validate(expectedGeneration, out WorldSession? session);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
if (newLeaderGuid == 0u)
{
return EmitUnsupported(
RuntimeCommandDomain.Fellowship,
operation: 4,
RuntimeCommandStatus.Rejected,
newLeaderGuid);
}
session!.SendFellowshipAssignNewLeader(newLeaderGuid);
return EmitResult(
RuntimeCommandDomain.Fellowship,
operation: 4,
RuntimeCommandStatus.Accepted,
newLeaderGuid);
}
public RuntimeCommandResult SetOpen(
RuntimeGenerationToken expectedGeneration,
bool isOpen)
{
RuntimeCommandStatus gate =
Validate(expectedGeneration, out WorldSession? session);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
session!.SendFellowshipChangeOpenness(isOpen);
return EmitResult(
RuntimeCommandDomain.Fellowship,
operation: 5,
RuntimeCommandStatus.Accepted);
}
public RuntimeCommandResult SetPanelOpen(
RuntimeGenerationToken expectedGeneration,
bool panelOpen)
{
RuntimeCommandStatus gate =
Validate(expectedGeneration, out WorldSession? session);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
session!.SendFellowshipUpdateRequest(panelOpen);
return EmitResult(
RuntimeCommandDomain.Fellowship,
operation: 6,
RuntimeCommandStatus.Accepted);
}
public RuntimeCommandResult Swear(
RuntimeGenerationToken expectedGeneration,
uint patronGuid)
{
RuntimeCommandStatus gate =
Validate(expectedGeneration, out WorldSession? session);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
if (patronGuid == 0u)
{
return EmitUnsupported(
RuntimeCommandDomain.Allegiance,
operation: 0,
RuntimeCommandStatus.Rejected,
patronGuid);
}
session!.SendAllegianceSwear(patronGuid);
return EmitResult(
RuntimeCommandDomain.Allegiance,
operation: 0,
RuntimeCommandStatus.Accepted,
patronGuid);
}
public RuntimeCommandResult Break(
RuntimeGenerationToken expectedGeneration,
uint targetGuid)
{
RuntimeCommandStatus gate =
Validate(expectedGeneration, out WorldSession? session);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
if (targetGuid == 0u)
{
return EmitUnsupported(
RuntimeCommandDomain.Allegiance,
operation: 1,
RuntimeCommandStatus.Rejected,
targetGuid);
}
session!.SendAllegianceBreak(targetGuid);
return EmitResult(
RuntimeCommandDomain.Allegiance,
operation: 1,
RuntimeCommandStatus.Accepted,
targetGuid);
}
public RuntimeCommandResult Kick(
RuntimeGenerationToken expectedGeneration,
uint vassalGuid)
{
RuntimeCommandStatus gate =
Validate(expectedGeneration, out WorldSession? session);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
if (vassalGuid == 0u)
{
return EmitUnsupported(
RuntimeCommandDomain.Allegiance,
operation: 2,
RuntimeCommandStatus.Rejected,
vassalGuid);
}
session!.SendAllegianceKick(vassalGuid);
return EmitResult(
RuntimeCommandDomain.Allegiance,
operation: 2,
RuntimeCommandStatus.Accepted,
vassalGuid);
}
public RuntimeCommandResult RequestInfo(
RuntimeGenerationToken expectedGeneration,
string playerName)
{
RuntimeCommandStatus gate =
Validate(expectedGeneration, out WorldSession? session);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
session!.SendAllegianceInfoRequest(playerName ?? string.Empty);
return EmitResult(
RuntimeCommandDomain.Allegiance,
operation: 3,
RuntimeCommandStatus.Accepted);
}
public RuntimeCommandResult SetUpdateSubscription(
RuntimeGenerationToken expectedGeneration,
bool on)
{
RuntimeCommandStatus gate =
Validate(expectedGeneration, out WorldSession? session);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
session!.SendAllegianceUpdateRequest(on);
return EmitResult(
RuntimeCommandDomain.Allegiance,
operation: 4,
RuntimeCommandStatus.Accepted);
}
bool IRuntimeInteractionTransport.IsInWorld
{
get