feat(app): FA2 -- fellowship/allegiance command routing for graphical + headless hosts

Both LiveSocialSessionBindings construction sites updated together
(LiveSessionRuntimeFactory.cs, HeadlessSessionHost.cs) so the single
GameEventWiring.WireAll registration site serves both hosts identically
(the K-slice unification). CurrentGameRuntimeCommandAdapter implements
IRuntimeFellowshipCommands/IRuntimeAllegianceCommands over the App
command bus (LiveSessionCommandRouter gains 12 new *RuntimeCmd records
+ registrations + LiveSessionCommandBindings send delegates), mirroring
DirectGameRuntimeCommandAdapter's direct-session shape including the
identical Quit leader-hand-off rule read from RuntimeFellowshipState.
CurrentGameRuntimeAdapter (the graphical IGameRuntimeView/
IGameRuntimeCommands composite) exposes the two new views/command
groups. Headless's DirectGameRuntimeCommandAdapter needed no changes --
it already implements both new interfaces from the Runtime-layer
commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-12 01:32:18 +02:00
parent 369729f06a
commit cced83b483
8 changed files with 363 additions and 3 deletions

View file

@ -26,7 +26,9 @@ internal sealed class CurrentGameRuntimeCommandAdapter
IRuntimeInventoryStateCommands,
IRuntimeSpellbookCommands,
IRuntimeCharacterCommands,
IRuntimeSocialCommands
IRuntimeSocialCommands,
IRuntimeFellowshipCommands,
IRuntimeAllegianceCommands
{
private readonly LiveSessionController _session;
private readonly LiveSessionHost _sessionHost;
@ -36,6 +38,7 @@ internal sealed class CurrentGameRuntimeCommandAdapter
private readonly RuntimeCharacterState _character;
private readonly RuntimeActionState _actions;
private readonly RuntimeLocalPlayerMovementState _movement;
private readonly RuntimeFellowshipState _fellowship;
private readonly SelectionInteractionController _selection;
private readonly IGameRuntimeEventSink _events;
@ -48,6 +51,7 @@ internal sealed class CurrentGameRuntimeCommandAdapter
RuntimeCharacterState character,
RuntimeActionState actions,
RuntimeLocalPlayerMovementState movement,
RuntimeFellowshipState fellowship,
SelectionInteractionController selection,
IGameRuntimeEventSink events)
{
@ -61,6 +65,8 @@ internal sealed class CurrentGameRuntimeCommandAdapter
?? throw new ArgumentNullException(nameof(actions));
_movement = movement
?? throw new ArgumentNullException(nameof(movement));
_fellowship = fellowship
?? throw new ArgumentNullException(nameof(fellowship));
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
_events = events ?? throw new ArgumentNullException(nameof(events));
}
@ -793,6 +799,249 @@ internal sealed class CurrentGameRuntimeCommandAdapter
command.Name);
}
// ── Fellowship / Allegiance (Campaign FA slice FA2, 2026-08-12) ────────
public RuntimeCommandResult Create(
RuntimeGenerationToken expectedGeneration,
string fellowshipName,
bool shareXp)
{
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
if (string.IsNullOrWhiteSpace(fellowshipName))
{
return EmitResult(
RuntimeCommandDomain.Fellowship,
operation: 0,
RuntimeCommandStatus.Rejected);
}
_commands.Publish(new FellowshipCreateRuntimeCmd(fellowshipName, shareXp));
return EmitResult(
RuntimeCommandDomain.Fellowship,
operation: 0,
RuntimeCommandStatus.Accepted);
}
public RuntimeCommandResult Recruit(
RuntimeGenerationToken expectedGeneration,
uint targetGuid)
{
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
if (targetGuid == 0u)
{
return EmitResult(
RuntimeCommandDomain.Fellowship,
operation: 1,
RuntimeCommandStatus.Rejected,
targetGuid);
}
_commands.Publish(new FellowshipRecruitRuntimeCmd(targetGuid));
return EmitResult(
RuntimeCommandDomain.Fellowship,
operation: 1,
RuntimeCommandStatus.Accepted,
targetGuid);
}
public RuntimeCommandResult Dismiss(
RuntimeGenerationToken expectedGeneration,
uint targetGuid)
{
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
if (targetGuid == 0u)
{
return EmitResult(
RuntimeCommandDomain.Fellowship,
operation: 2,
RuntimeCommandStatus.Rejected,
targetGuid);
}
_commands.Publish(new FellowshipDismissRuntimeCmd(targetGuid));
return EmitResult(
RuntimeCommandDomain.Fellowship,
operation: 2,
RuntimeCommandStatus.Accepted,
targetGuid);
}
/// <summary>
/// Retail's Quit-button leader hand-off (lane B §2.5/§3.6) — see
/// <see cref="DirectGameRuntimeCommandAdapter.Quit"/>'s identical rule.
/// </summary>
public RuntimeCommandResult Quit(
RuntimeGenerationToken expectedGeneration,
bool disband)
{
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
if (_fellowship.RequiresLeaderHandoffBeforeQuit(
_view.Lifecycle.PlayerGuid,
disband,
out uint newLeaderGuid))
{
_commands.Publish(new FellowshipAssignNewLeaderRuntimeCmd(newLeaderGuid));
}
_commands.Publish(new FellowshipQuitRuntimeCmd(disband));
return EmitResult(
RuntimeCommandDomain.Fellowship,
operation: 3,
RuntimeCommandStatus.Accepted);
}
public RuntimeCommandResult AssignLeader(
RuntimeGenerationToken expectedGeneration,
uint newLeaderGuid)
{
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
if (newLeaderGuid == 0u)
{
return EmitResult(
RuntimeCommandDomain.Fellowship,
operation: 4,
RuntimeCommandStatus.Rejected,
newLeaderGuid);
}
_commands.Publish(new FellowshipAssignNewLeaderRuntimeCmd(newLeaderGuid));
return EmitResult(
RuntimeCommandDomain.Fellowship,
operation: 4,
RuntimeCommandStatus.Accepted,
newLeaderGuid);
}
public RuntimeCommandResult SetOpen(
RuntimeGenerationToken expectedGeneration,
bool isOpen)
{
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
_commands.Publish(new FellowshipChangeOpennessRuntimeCmd(isOpen));
return EmitResult(
RuntimeCommandDomain.Fellowship,
operation: 5,
RuntimeCommandStatus.Accepted);
}
public RuntimeCommandResult SetPanelOpen(
RuntimeGenerationToken expectedGeneration,
bool panelOpen)
{
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
_commands.Publish(new FellowshipUpdateRequestRuntimeCmd(panelOpen));
return EmitResult(
RuntimeCommandDomain.Fellowship,
operation: 6,
RuntimeCommandStatus.Accepted);
}
public RuntimeCommandResult Swear(
RuntimeGenerationToken expectedGeneration,
uint patronGuid)
{
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
if (patronGuid == 0u)
{
return EmitResult(
RuntimeCommandDomain.Allegiance,
operation: 0,
RuntimeCommandStatus.Rejected,
patronGuid);
}
_commands.Publish(new AllegianceSwearRuntimeCmd(patronGuid));
return EmitResult(
RuntimeCommandDomain.Allegiance,
operation: 0,
RuntimeCommandStatus.Accepted,
patronGuid);
}
public RuntimeCommandResult Break(
RuntimeGenerationToken expectedGeneration,
uint targetGuid)
{
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
if (targetGuid == 0u)
{
return EmitResult(
RuntimeCommandDomain.Allegiance,
operation: 1,
RuntimeCommandStatus.Rejected,
targetGuid);
}
_commands.Publish(new AllegianceBreakRuntimeCmd(targetGuid));
return EmitResult(
RuntimeCommandDomain.Allegiance,
operation: 1,
RuntimeCommandStatus.Accepted,
targetGuid);
}
public RuntimeCommandResult Kick(
RuntimeGenerationToken expectedGeneration,
uint vassalGuid)
{
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
if (vassalGuid == 0u)
{
return EmitResult(
RuntimeCommandDomain.Allegiance,
operation: 2,
RuntimeCommandStatus.Rejected,
vassalGuid);
}
_commands.Publish(new AllegianceKickRuntimeCmd(vassalGuid));
return EmitResult(
RuntimeCommandDomain.Allegiance,
operation: 2,
RuntimeCommandStatus.Accepted,
vassalGuid);
}
public RuntimeCommandResult RequestInfo(
RuntimeGenerationToken expectedGeneration,
string playerName)
{
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
_commands.Publish(new AllegianceInfoRequestRuntimeCmd(playerName ?? string.Empty));
return EmitResult(
RuntimeCommandDomain.Allegiance,
operation: 3,
RuntimeCommandStatus.Accepted);
}
public RuntimeCommandResult SetUpdateSubscription(
RuntimeGenerationToken expectedGeneration,
bool on)
{
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
_commands.Publish(new AllegianceUpdateRequestRuntimeCmd(on));
return EmitResult(
RuntimeCommandDomain.Allegiance,
operation: 4,
RuntimeCommandStatus.Accepted);
}
private RuntimeCommandStatus Validate(
RuntimeGenerationToken expectedGeneration,
bool requireWorld)