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:
parent
1c40104896
commit
369729f06a
19 changed files with 1949 additions and 35 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -79,7 +79,13 @@ public sealed record LiveSocialSessionBindings(
|
|||
// existing caller (including tests that build a bare ChatLog with no
|
||||
// owning RuntimeCommunicationState) compiles unchanged; GameEventWiring
|
||||
// falls back to its pre-CH2 chat-only behavior when this is null.
|
||||
Action<string, RetailLogTextType>? AddText = null);
|
||||
Action<string, RetailLogTextType>? AddText = null,
|
||||
// Campaign FA slice FA2 (2026-08-12): the two sibling J-owners.
|
||||
// Trailing/optional so every existing positional caller (Headless)
|
||||
// compiles unchanged (docs/research/2026-08-11-fa-acdream-seams.md
|
||||
// §2.4 — "the established compatibility convention").
|
||||
RuntimeFellowshipState? Fellowship = null,
|
||||
RuntimeAllegianceState? Allegiance = null);
|
||||
|
||||
/// <summary>
|
||||
/// Owns every inbound subscription for one exact live session. Domain state
|
||||
|
|
@ -228,7 +234,34 @@ public sealed class LiveSessionEventRouter : ILiveSessionEventRouting
|
|||
externalContainers: inventory.ExternalContainers,
|
||||
vendor: inventory.Vendor,
|
||||
onInterfaceText: social.AddText,
|
||||
accepting: IsAccepting));
|
||||
accepting: IsAccepting,
|
||||
// Campaign FA slice FA2 (2026-08-12): fellowship/allegiance
|
||||
// sink registration — the ONE inbound registration site
|
||||
// (docs/research/2026-08-11-fa-acdream-seams.md §2.1),
|
||||
// reached identically by both hosts since LiveSessionEventRouter
|
||||
// itself is shared (LiveSessionEventRouter.Attach, K-slice
|
||||
// unification).
|
||||
onFellowshipFullUpdate: update =>
|
||||
social.Fellowship?.ApplyFullUpdate(update),
|
||||
onFellowshipUpdateFellow: update =>
|
||||
social.Fellowship?.ApplyUpdateFellow(update),
|
||||
onFellowshipQuit: quitterGuid =>
|
||||
social.Fellowship?.ApplyQuit(quitterGuid, inventory.PlayerGuid()),
|
||||
onFellowshipDismiss: dismissedGuid =>
|
||||
social.Fellowship?.ApplyDismiss(dismissedGuid, inventory.PlayerGuid()),
|
||||
onFellowshipDisband: () => social.Fellowship?.ApplyDisband(),
|
||||
onAllegianceUpdate: update =>
|
||||
social.Allegiance?.ApplyUpdate(update),
|
||||
onAllegianceInfoResponseSelf: response =>
|
||||
social.Allegiance?.ApplyInfoResponseSelf(response),
|
||||
onAllegianceUpdateDone: weenieError =>
|
||||
social.Allegiance?.ApplyUpdateDone(weenieError),
|
||||
onAllegianceUpdateAborted: weenieError =>
|
||||
social.Allegiance?.ApplyUpdateAborted(weenieError),
|
||||
onAllegianceLoginNotification: notice =>
|
||||
social.Allegiance?.ApplyLoginNotification(
|
||||
notice.CharacterGuid,
|
||||
notice.IsLoggedIn)));
|
||||
ConstructionCheckpoint();
|
||||
|
||||
// Campaign P Slice P1 (2026-07-30): burden recompute triggers —
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue