Refactor runtime bootstrap and ship control flows

This commit is contained in:
2026-04-03 01:12:26 -04:00
parent 0bb72bee35
commit 706e1cda8f
129 changed files with 9588 additions and 3548 deletions

View File

@@ -7,7 +7,6 @@ public sealed class CreatePlayerOrganizationHandler(WorldService worldService) :
public override void Configure()
{
Post("/api/player-faction/organizations");
AllowAnonymous();
}
public override async Task HandleAsync(PlayerOrganizationCommandRequest request, CancellationToken cancellationToken)

View File

@@ -12,7 +12,6 @@ public sealed class DeletePlayerDirectiveHandler(WorldService worldService) : En
public override void Configure()
{
Delete("/api/player-faction/directives/{directiveId}");
AllowAnonymous();
}
public override async Task HandleAsync(DeletePlayerDirectiveRequest request, CancellationToken cancellationToken)

View File

@@ -12,7 +12,6 @@ public sealed class DeletePlayerOrganizationHandler(WorldService worldService) :
public override void Configure()
{
Delete("/api/player-faction/organizations/{organizationId}");
AllowAnonymous();
}
public override async Task HandleAsync(DeletePlayerOrganizationRequest request, CancellationToken cancellationToken)

View File

@@ -7,7 +7,6 @@ public sealed class GetPlayerFactionHandler(WorldService worldService) : Endpoin
public override void Configure()
{
Get("/api/player-faction");
AllowAnonymous();
}
public override async Task HandleAsync(CancellationToken cancellationToken)

View File

@@ -7,7 +7,6 @@ public sealed class UpdatePlayerOrganizationMembershipHandler(WorldService world
public override void Configure()
{
Put("/api/player-faction/organizations/{organizationId}/membership");
AllowAnonymous();
}
public override async Task HandleAsync(PlayerOrganizationMembershipCommandRequest request, CancellationToken cancellationToken)

View File

@@ -7,7 +7,6 @@ public sealed class UpdatePlayerStrategicIntentHandler(WorldService worldService
public override void Configure()
{
Put("/api/player-faction/strategic-intent");
AllowAnonymous();
}
public override async Task HandleAsync(PlayerStrategicIntentCommandRequest request, CancellationToken cancellationToken)

View File

@@ -7,7 +7,6 @@ public sealed class UpsertPlayerAssignmentHandler(WorldService worldService) : E
public override void Configure()
{
Put("/api/player-faction/assets/{assetId}/assignment");
AllowAnonymous();
}
public override async Task HandleAsync(PlayerAssetAssignmentCommandRequest request, CancellationToken cancellationToken)

View File

@@ -8,7 +8,6 @@ public sealed class UpsertPlayerAutomationPolicyHandler(WorldService worldServic
{
Post("/api/player-faction/automation-policies");
Put("/api/player-faction/automation-policies/{automationPolicyId}");
AllowAnonymous();
}
public override async Task HandleAsync(PlayerAutomationPolicyCommandRequest request, CancellationToken cancellationToken)

View File

@@ -8,7 +8,6 @@ public sealed class UpsertPlayerDirectiveHandler(WorldService worldService) : En
{
Post("/api/player-faction/directives");
Put("/api/player-faction/directives/{directiveId}");
AllowAnonymous();
}
public override async Task HandleAsync(PlayerDirectiveCommandRequest request, CancellationToken cancellationToken)

View File

@@ -8,7 +8,6 @@ public sealed class UpsertPlayerPolicyHandler(WorldService worldService) : Endpo
{
Post("/api/player-faction/policies");
Put("/api/player-faction/policies/{policyId}");
AllowAnonymous();
}
public override async Task HandleAsync(PlayerPolicyCommandRequest request, CancellationToken cancellationToken)

View File

@@ -8,7 +8,6 @@ public sealed class UpsertPlayerProductionProgramHandler(WorldService worldServi
{
Post("/api/player-faction/production-programs");
Put("/api/player-faction/production-programs/{productionProgramId}");
AllowAnonymous();
}
public override async Task HandleAsync(PlayerProductionProgramCommandRequest request, CancellationToken cancellationToken)

View File

@@ -8,7 +8,6 @@ public sealed class UpsertPlayerReinforcementPolicyHandler(WorldService worldSer
{
Post("/api/player-faction/reinforcement-policies");
Put("/api/player-faction/reinforcement-policies/{reinforcementPolicyId}");
AllowAnonymous();
}
public override async Task HandleAsync(PlayerReinforcementPolicyCommandRequest request, CancellationToken cancellationToken)

View File

@@ -1,3 +1,5 @@
using static SpaceGame.Api.Shared.Runtime.ShipBehaviorKinds;
namespace SpaceGame.Api.PlayerFaction.Runtime;
public sealed class PlayerFactionRuntime
@@ -180,7 +182,7 @@ public sealed class PlayerAutomationPolicyRuntime
public string ScopeKind { get; set; } = "player-faction";
public string? ScopeId { get; set; }
public bool Enabled { get; set; } = true;
public string BehaviorKind { get; set; } = "idle";
public string BehaviorKind { get; set; } = Idle;
public bool UseOrders { get; set; }
public string? StagingOrderKind { get; set; }
public int MaxSystemRange { get; set; }
@@ -242,7 +244,7 @@ public sealed class PlayerDirectiveRuntime
public string? HomeStationId { get; set; }
public string? SourceStationId { get; set; }
public string? DestinationStationId { get; set; }
public string BehaviorKind { get; set; } = "idle";
public string BehaviorKind { get; set; } = Idle;
public bool UseOrders { get; set; }
public string? StagingOrderKind { get; set; }
public string? ItemId { get; set; }

View File

@@ -0,0 +1,9 @@
namespace SpaceGame.Api.PlayerFaction.Simulation;
public interface IPlayerStateStore
{
bool TryGetPlayerFaction(string playerId, out PlayerFactionRuntime playerFaction);
PlayerFactionRuntime GetOrAddPlayerFaction(string playerId, Func<PlayerFactionRuntime> factory);
IReadOnlyCollection<PlayerFactionRuntime> GetPlayerFactions();
void Clear();
}

View File

@@ -0,0 +1,270 @@
namespace SpaceGame.Api.PlayerFaction.Simulation;
public sealed class PlayerFactionProjectionService
{
public PlayerFactionSnapshot? ToSnapshot(PlayerFactionRuntime? player)
{
if (player is null)
{
return null;
}
return new PlayerFactionSnapshot(
player.Id,
player.Label,
player.SovereignFactionId,
player.Status,
player.CreatedAtUtc,
player.UpdatedAtUtc,
new PlayerAssetRegistrySnapshot(
player.AssetRegistry.ShipIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
player.AssetRegistry.StationIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
player.AssetRegistry.CommanderIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
player.AssetRegistry.ClaimIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
player.AssetRegistry.ConstructionSiteIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
player.AssetRegistry.PolicySetIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
player.AssetRegistry.MarketOrderIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
player.AssetRegistry.FleetIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
player.AssetRegistry.TaskForceIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
player.AssetRegistry.StationGroupIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
player.AssetRegistry.EconomicRegionIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
player.AssetRegistry.FrontIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
player.AssetRegistry.ReserveIds.OrderBy(id => id, StringComparer.Ordinal).ToList()),
new PlayerStrategicIntentSnapshot(
player.StrategicIntent.StrategicPosture,
player.StrategicIntent.EconomicPosture,
player.StrategicIntent.MilitaryPosture,
player.StrategicIntent.LogisticsPosture,
player.StrategicIntent.DesiredReserveRatio,
player.StrategicIntent.AllowDelegatedCombatAutomation,
player.StrategicIntent.AllowDelegatedEconomicAutomation,
player.StrategicIntent.Notes),
player.Fleets.Select(fleet => new PlayerFleetSnapshot(
fleet.Id,
fleet.Label,
fleet.Status,
fleet.Role,
fleet.CommanderId,
fleet.FrontId,
fleet.HomeSystemId,
fleet.HomeStationId,
fleet.PolicyId,
fleet.AutomationPolicyId,
fleet.ReinforcementPolicyId,
fleet.AssetIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
fleet.TaskForceIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
fleet.DirectiveIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
fleet.UpdatedAtUtc)).ToList(),
player.TaskForces.Select(taskForce => new PlayerTaskForceSnapshot(
taskForce.Id,
taskForce.Label,
taskForce.Status,
taskForce.Role,
taskForce.FleetId,
taskForce.CommanderId,
taskForce.FrontId,
taskForce.PolicyId,
taskForce.AutomationPolicyId,
taskForce.AssetIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
taskForce.DirectiveIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
taskForce.UpdatedAtUtc)).ToList(),
player.StationGroups.Select(group => new PlayerStationGroupSnapshot(
group.Id,
group.Label,
group.Status,
group.Role,
group.EconomicRegionId,
group.PolicyId,
group.AutomationPolicyId,
group.StationIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
group.DirectiveIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
group.FocusItemIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
group.UpdatedAtUtc)).ToList(),
player.EconomicRegions.Select(region => new PlayerEconomicRegionSnapshot(
region.Id,
region.Label,
region.Status,
region.Role,
region.SharedEconomicRegionId,
region.PolicyId,
region.AutomationPolicyId,
region.SystemIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
region.StationGroupIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
region.DirectiveIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
region.UpdatedAtUtc)).ToList(),
player.Fronts.Select(front => new PlayerFrontSnapshot(
front.Id,
front.Label,
front.Status,
front.Priority,
front.Posture,
front.SharedFrontLineId,
front.TargetFactionId,
front.SystemIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
front.FleetIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
front.ReserveIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
front.DirectiveIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
front.UpdatedAtUtc)).ToList(),
player.Reserves.Select(reserve => new PlayerReserveGroupSnapshot(
reserve.Id,
reserve.Label,
reserve.Status,
reserve.ReserveKind,
reserve.HomeSystemId,
reserve.PolicyId,
reserve.AssetIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
reserve.FrontIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
reserve.UpdatedAtUtc)).ToList(),
player.Policies.Select(policy => new PlayerFactionPolicySnapshot(
policy.Id,
policy.Label,
policy.ScopeKind,
policy.ScopeId,
policy.PolicySetId,
policy.AllowDelegatedCombat,
policy.AllowDelegatedTrade,
policy.ReserveCreditsRatio,
policy.ReserveMilitaryRatio,
policy.TradeAccessPolicy,
policy.DockingAccessPolicy,
policy.ConstructionAccessPolicy,
policy.OperationalRangePolicy,
policy.CombatEngagementPolicy,
policy.AvoidHostileSystems,
policy.FleeHullRatio,
policy.BlacklistedSystemIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
policy.Notes,
policy.UpdatedAtUtc)).ToList(),
player.AutomationPolicies.Select(policy => new PlayerAutomationPolicySnapshot(
policy.Id,
policy.Label,
policy.ScopeKind,
policy.ScopeId,
policy.Enabled,
policy.BehaviorKind,
policy.UseOrders,
policy.StagingOrderKind,
policy.MaxSystemRange,
policy.KnownStationsOnly,
policy.Radius,
policy.WaitSeconds,
policy.PreferredItemId,
policy.Notes,
policy.RepeatOrders.Select(ToShipOrderTemplateSnapshot).ToList(),
policy.UpdatedAtUtc)).ToList(),
player.ReinforcementPolicies.Select(policy => new PlayerReinforcementPolicySnapshot(
policy.Id,
policy.Label,
policy.ScopeKind,
policy.ScopeId,
policy.ShipKind,
policy.DesiredAssetCount,
policy.MinimumReserveCount,
policy.AutoTransferReserves,
policy.AutoQueueProduction,
policy.SourceReserveId,
policy.TargetFrontId,
policy.Notes,
policy.UpdatedAtUtc)).ToList(),
player.ProductionPrograms.Select(program => new PlayerProductionProgramSnapshot(
program.Id,
program.Label,
program.Status,
program.Kind,
program.TargetShipKind,
program.TargetModuleId,
program.TargetItemId,
program.TargetCount,
program.CurrentCount,
program.StationGroupId,
program.ReinforcementPolicyId,
program.Notes,
program.UpdatedAtUtc)).ToList(),
player.Directives.Select(directive => new PlayerDirectiveSnapshot(
directive.Id,
directive.Label,
directive.Status,
directive.Kind,
directive.ScopeKind,
directive.ScopeId,
directive.TargetEntityId,
directive.TargetSystemId,
directive.TargetPosition is null ? null : ToDto(directive.TargetPosition.Value),
directive.HomeSystemId,
directive.HomeStationId,
directive.SourceStationId,
directive.DestinationStationId,
directive.BehaviorKind,
directive.UseOrders,
directive.StagingOrderKind,
directive.ItemId,
directive.PreferredNodeId,
directive.PreferredConstructionSiteId,
directive.PreferredModuleId,
directive.Priority,
directive.Radius,
directive.WaitSeconds,
directive.MaxSystemRange,
directive.KnownStationsOnly,
directive.PatrolPoints.Select(ToDto).ToList(),
directive.RepeatOrders.Select(ToShipOrderTemplateSnapshot).ToList(),
directive.PolicyId,
directive.AutomationPolicyId,
directive.Notes,
directive.CreatedAtUtc,
directive.UpdatedAtUtc)).ToList(),
player.Assignments.Select(assignment => new PlayerAssignmentSnapshot(
assignment.Id,
assignment.AssetKind,
assignment.AssetId,
assignment.FleetId,
assignment.TaskForceId,
assignment.StationGroupId,
assignment.EconomicRegionId,
assignment.FrontId,
assignment.ReserveId,
assignment.DirectiveId,
assignment.PolicyId,
assignment.AutomationPolicyId,
assignment.Role,
assignment.Status,
assignment.UpdatedAtUtc)).ToList(),
player.DecisionLog.Select(entry => new PlayerDecisionLogEntrySnapshot(
entry.Id,
entry.Kind,
entry.Summary,
entry.RelatedEntityKind,
entry.RelatedEntityId,
entry.OccurredAtUtc)).ToList(),
player.Alerts.Select(alert => new PlayerAlertSnapshot(
alert.Id,
alert.Kind,
alert.Severity,
alert.Summary,
alert.AssetKind,
alert.AssetId,
alert.RelatedDirectiveId,
alert.Status,
alert.CreatedAtUtc)).ToList());
}
private static ShipOrderTemplateSnapshot ToShipOrderTemplateSnapshot(ShipOrderTemplateRuntime template) =>
new(
template.Kind,
template.Label,
template.TargetEntityId,
template.TargetSystemId,
template.TargetPosition is null ? null : ToDto(template.TargetPosition.Value),
template.SourceStationId,
template.DestinationStationId,
template.ItemId,
template.NodeId,
template.ConstructionSiteId,
template.ModuleId,
template.WaitSeconds,
template.Radius,
template.MaxSystemRange,
template.KnownStationsOnly);
private static Vector3Dto ToDto(Vector3 vector) => new(vector.X, vector.Y, vector.Z);
}

View File

@@ -1,3 +1,6 @@
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
using static SpaceGame.Api.Shared.Runtime.ShipBehaviorKinds;
namespace SpaceGame.Api.PlayerFaction.Simulation;
internal sealed class PlayerFactionService
@@ -6,58 +9,61 @@ internal sealed class PlayerFactionService
private const int MaxAlerts = 32;
private const string PlayerFactionDomainId = "player-faction";
internal static bool IsPlayerFaction(SimulationWorld world, string factionId) =>
world.PlayerFaction is not null && string.Equals(world.PlayerFaction.SovereignFactionId, factionId, StringComparison.Ordinal);
internal static bool IsPlayerFaction(IPlayerStateStore playerStateStore, string factionId) =>
playerStateStore.GetPlayerFactions().Any(player =>
string.Equals(player.SovereignFactionId, factionId, StringComparison.Ordinal));
internal PlayerFactionRuntime EnsureDomain(SimulationWorld world)
internal PlayerFactionRuntime? TryGetDomain(IPlayerStateStore playerStateStore, string playerId)
{
if (world.PlayerFaction is not null)
{
return world.PlayerFaction;
}
return playerStateStore.TryGetPlayerFaction(playerId, out var player) ? player : null;
}
internal PlayerFactionRuntime EnsureDomain(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId)
{
var sovereignFaction = world.Factions.OrderBy(faction => faction.Id, StringComparer.Ordinal).FirstOrDefault()
?? throw new InvalidOperationException("Cannot create a player faction domain without any factions in the world.");
world.PlayerFaction = new PlayerFactionRuntime
var player = playerStateStore.GetOrAddPlayerFaction(playerId, () => new PlayerFactionRuntime
{
Id = PlayerFactionDomainId,
Label = $"{sovereignFaction.Label} Command",
SovereignFactionId = sovereignFaction.Id,
CreatedAtUtc = world.GeneratedAtUtc,
UpdatedAtUtc = world.GeneratedAtUtc,
};
});
EnsureBaseStructures(world, world.PlayerFaction);
SyncRegistry(world, world.PlayerFaction);
return world.PlayerFaction;
EnsureBaseStructures(world, player);
SyncRegistry(world, player);
return player;
}
internal void Update(SimulationWorld world, float _deltaSeconds, ICollection<SimulationEventRecord> events)
internal void Update(SimulationWorld world, IPlayerStateStore playerStateStore, float _deltaSeconds, ICollection<SimulationEventRecord> events)
{
if (world.PlayerFaction is null && world.Factions.Count == 0)
if (playerStateStore.GetPlayerFactions().Count == 0)
{
return;
}
var player = EnsureDomain(world);
EnsureBaseStructures(world, player);
SyncRegistry(world, player);
PrunePlayerState(world, player);
RefreshGeopoliticalOrganizationContext(world, player);
ReconcileOrganizationAssignments(world, player);
ReconcileDirectiveScopes(player);
RefreshProductionPrograms(world, player);
ApplyStrategicIntegration(world, player);
ApplyPolicies(world, player);
ApplyAssignmentsAndDirectives(world, player, events);
RefreshAlerts(world, player);
player.UpdatedAtUtc = DateTimeOffset.UtcNow;
foreach (var player in playerStateStore.GetPlayerFactions())
{
EnsureBaseStructures(world, player);
SyncRegistry(world, player);
PrunePlayerState(world, player);
RefreshGeopoliticalOrganizationContext(world, player);
ReconcileOrganizationAssignments(world, player);
ReconcileDirectiveScopes(player);
RefreshProductionPrograms(world, player);
ApplyStrategicIntegration(world, player);
ApplyPolicies(world, player);
ApplyAssignmentsAndDirectives(world, player, events);
RefreshAlerts(world, player);
player.UpdatedAtUtc = DateTimeOffset.UtcNow;
}
}
internal PlayerFactionRuntime CreateOrganization(SimulationWorld world, PlayerOrganizationCommandRequest request)
internal PlayerFactionRuntime CreateOrganization(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, PlayerOrganizationCommandRequest request)
{
var player = EnsureDomain(world);
var player = EnsureDomain(world, playerStateStore, playerId);
var id = CreateDomainId(request.Kind, request.Label, ExistingOrganizationIds(player));
var nowUtc = DateTimeOffset.UtcNow;
@@ -172,9 +178,9 @@ internal sealed class PlayerFactionService
return player;
}
internal PlayerFactionRuntime DeleteOrganization(SimulationWorld world, string organizationId)
internal PlayerFactionRuntime DeleteOrganization(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string organizationId)
{
var player = EnsureDomain(world);
var player = EnsureDomain(world, playerStateStore, playerId);
RemoveOrganization(player, organizationId);
player.Assignments.RemoveAll(assignment =>
assignment.FleetId == organizationId ||
@@ -190,9 +196,9 @@ internal sealed class PlayerFactionService
return player;
}
internal PlayerFactionRuntime UpdateOrganizationMembership(SimulationWorld world, string organizationId, PlayerOrganizationMembershipCommandRequest request)
internal PlayerFactionRuntime UpdateOrganizationMembership(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string organizationId, PlayerOrganizationMembershipCommandRequest request)
{
var player = EnsureDomain(world);
var player = EnsureDomain(world, playerStateStore, playerId);
var kind = ResolveOrganizationKind(player, organizationId);
switch (kind)
{
@@ -241,9 +247,9 @@ internal sealed class PlayerFactionService
return player;
}
internal PlayerFactionRuntime UpsertDirective(SimulationWorld world, string? directiveId, PlayerDirectiveCommandRequest request)
internal PlayerFactionRuntime UpsertDirective(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string? directiveId, PlayerDirectiveCommandRequest request)
{
var player = EnsureDomain(world);
var player = EnsureDomain(world, playerStateStore, playerId);
var directive = directiveId is null
? null
: player.Directives.FirstOrDefault(candidate => string.Equals(candidate.Id, directiveId, StringComparison.Ordinal));
@@ -318,9 +324,9 @@ internal sealed class PlayerFactionService
return player;
}
internal PlayerFactionRuntime DeleteDirective(SimulationWorld world, string directiveId)
internal PlayerFactionRuntime DeleteDirective(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string directiveId)
{
var player = EnsureDomain(world);
var player = EnsureDomain(world, playerStateStore, playerId);
player.Directives.RemoveAll(directive => directive.Id == directiveId);
foreach (var assignment in player.Assignments.Where(assignment => assignment.DirectiveId == directiveId))
{
@@ -332,9 +338,9 @@ internal sealed class PlayerFactionService
return player;
}
internal PlayerFactionRuntime UpsertPolicy(SimulationWorld world, string? policyId, PlayerPolicyCommandRequest request)
internal PlayerFactionRuntime UpsertPolicy(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string? policyId, PlayerPolicyCommandRequest request)
{
var player = EnsureDomain(world);
var player = EnsureDomain(world, playerStateStore, playerId);
var policy = policyId is null
? null
: player.Policies.FirstOrDefault(candidate => string.Equals(candidate.Id, policyId, StringComparison.Ordinal));
@@ -403,9 +409,9 @@ internal sealed class PlayerFactionService
return player;
}
internal PlayerFactionRuntime UpsertAutomationPolicy(SimulationWorld world, string? automationPolicyId, PlayerAutomationPolicyCommandRequest request)
internal PlayerFactionRuntime UpsertAutomationPolicy(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string? automationPolicyId, PlayerAutomationPolicyCommandRequest request)
{
var player = EnsureDomain(world);
var player = EnsureDomain(world, playerStateStore, playerId);
var policy = automationPolicyId is null
? null
: player.AutomationPolicies.FirstOrDefault(candidate => string.Equals(candidate.Id, automationPolicyId, StringComparison.Ordinal));
@@ -461,9 +467,9 @@ internal sealed class PlayerFactionService
return player;
}
internal PlayerFactionRuntime UpsertReinforcementPolicy(SimulationWorld world, string? reinforcementPolicyId, PlayerReinforcementPolicyCommandRequest request)
internal PlayerFactionRuntime UpsertReinforcementPolicy(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string? reinforcementPolicyId, PlayerReinforcementPolicyCommandRequest request)
{
var player = EnsureDomain(world);
var player = EnsureDomain(world, playerStateStore, playerId);
var policy = reinforcementPolicyId is null
? null
: player.ReinforcementPolicies.FirstOrDefault(candidate => string.Equals(candidate.Id, reinforcementPolicyId, StringComparison.Ordinal));
@@ -495,9 +501,9 @@ internal sealed class PlayerFactionService
return player;
}
internal PlayerFactionRuntime UpsertProductionProgram(SimulationWorld world, string? productionProgramId, PlayerProductionProgramCommandRequest request)
internal PlayerFactionRuntime UpsertProductionProgram(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string? productionProgramId, PlayerProductionProgramCommandRequest request)
{
var player = EnsureDomain(world);
var player = EnsureDomain(world, playerStateStore, playerId);
var program = productionProgramId is null
? null
: player.ProductionPrograms.FirstOrDefault(candidate => string.Equals(candidate.Id, productionProgramId, StringComparison.Ordinal));
@@ -527,9 +533,9 @@ internal sealed class PlayerFactionService
return player;
}
internal PlayerFactionRuntime UpsertAssignment(SimulationWorld world, string assetId, PlayerAssetAssignmentCommandRequest request)
internal PlayerFactionRuntime UpsertAssignment(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string assetId, PlayerAssetAssignmentCommandRequest request)
{
var player = EnsureDomain(world);
var player = EnsureDomain(world, playerStateStore, playerId);
var assignment = player.Assignments.FirstOrDefault(candidate =>
string.Equals(candidate.AssetId, assetId, StringComparison.Ordinal) &&
string.Equals(candidate.AssetKind, request.AssetKind, StringComparison.Ordinal));
@@ -586,9 +592,9 @@ internal sealed class PlayerFactionService
return player;
}
internal PlayerFactionRuntime UpdateStrategicIntent(SimulationWorld world, PlayerStrategicIntentCommandRequest request)
internal PlayerFactionRuntime UpdateStrategicIntent(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, PlayerStrategicIntentCommandRequest request)
{
var player = EnsureDomain(world);
var player = EnsureDomain(world, playerStateStore, playerId);
player.StrategicIntent.StrategicPosture = request.StrategicPosture;
player.StrategicIntent.EconomicPosture = request.EconomicPosture;
player.StrategicIntent.MilitaryPosture = request.MilitaryPosture;
@@ -602,9 +608,9 @@ internal sealed class PlayerFactionService
return player;
}
internal ShipRuntime? EnqueueDirectShipOrder(SimulationWorld world, string shipId, ShipOrderCommandRequest request)
internal ShipRuntime? EnqueueDirectShipOrder(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string shipId, ShipOrderCommandRequest request)
{
var player = EnsureDomain(world);
var player = EnsureDomain(world, playerStateStore, playerId);
if (!player.AssetRegistry.ShipIds.Contains(shipId))
{
return null;
@@ -625,6 +631,8 @@ internal sealed class PlayerFactionService
{
Id = $"order-{ship.Id}-{Guid.NewGuid():N}",
Kind = request.Kind,
SourceKind = ShipOrderSourceKind.Player,
SourceId = playerId,
Priority = request.Priority,
InterruptCurrentPlan = request.InterruptCurrentPlan,
Label = request.Label,
@@ -643,11 +651,11 @@ internal sealed class PlayerFactionService
KnownStationsOnly = request.KnownStationsOnly ?? false,
});
AddDecision(player, "ship-order-enqueued", $"Queued {request.Kind} for {ship.Definition.Label}.", "ship", shipId);
AddDecision(player, "ship-order-enqueued", $"Queued {request.Kind} for {ship.Definition.Name}.", "ship", shipId);
player.UpdatedAtUtc = DateTimeOffset.UtcNow;
ship.ControlSourceKind = "player-order";
ship.ControlSourceId = ship.OrderQueue
.Where(order => !order.Id.StartsWith("ai-order-", StringComparison.Ordinal))
.Where(order => order.SourceKind == ShipOrderSourceKind.Player)
.OrderByDescending(order => order.Priority)
.ThenBy(order => order.CreatedAtUtc)
.Select(order => order.Id)
@@ -659,9 +667,9 @@ internal sealed class PlayerFactionService
return ship;
}
internal ShipRuntime? RemoveDirectShipOrder(SimulationWorld world, string shipId, string orderId)
internal ShipRuntime? RemoveDirectShipOrder(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string shipId, string orderId)
{
var player = EnsureDomain(world);
var player = EnsureDomain(world, playerStateStore, playerId);
if (!player.AssetRegistry.ShipIds.Contains(shipId))
{
return null;
@@ -676,21 +684,21 @@ internal sealed class PlayerFactionService
var removed = ship.OrderQueue.RemoveAll(order => order.Id == orderId);
if (removed > 0)
{
AddDecision(player, "ship-order-removed", $"Removed order {orderId} from {ship.Definition.Label}.", "ship", shipId);
AddDecision(player, "ship-order-removed", $"Removed order {orderId} from {ship.Definition.Name}.", "ship", shipId);
player.UpdatedAtUtc = DateTimeOffset.UtcNow;
}
ship.ControlSourceKind = ship.OrderQueue.Any(order => !order.Id.StartsWith("ai-order-", StringComparison.Ordinal))
ship.ControlSourceKind = ship.OrderQueue.Any(order => order.SourceKind == ShipOrderSourceKind.Player)
? "player-order"
: "player-manual";
ship.ControlSourceId = ship.OrderQueue
.Where(order => !order.Id.StartsWith("ai-order-", StringComparison.Ordinal))
.Where(order => order.SourceKind == ShipOrderSourceKind.Player)
.OrderByDescending(order => order.Priority)
.ThenBy(order => order.CreatedAtUtc)
.Select(order => order.Id)
.FirstOrDefault();
ship.ControlReason = ship.OrderQueue
.Where(order => !order.Id.StartsWith("ai-order-", StringComparison.Ordinal))
.Where(order => order.SourceKind == ShipOrderSourceKind.Player)
.OrderByDescending(order => order.Priority)
.ThenBy(order => order.CreatedAtUtc)
.Select(order => order.Label ?? order.Kind)
@@ -702,9 +710,9 @@ internal sealed class PlayerFactionService
return ship;
}
internal ShipRuntime? ConfigureDirectShipBehavior(SimulationWorld world, string shipId, ShipDefaultBehaviorCommandRequest request)
internal ShipRuntime? ConfigureDirectShipBehavior(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string shipId, ShipDefaultBehaviorCommandRequest request)
{
var player = EnsureDomain(world);
var player = EnsureDomain(world, playerStateStore, playerId);
if (!player.AssetRegistry.ShipIds.Contains(shipId))
{
return null;
@@ -723,7 +731,7 @@ internal sealed class PlayerFactionService
directive = new PlayerDirectiveRuntime
{
Id = directiveId,
Label = $"Direct control {ship.Definition.Label}",
Label = $"Direct control {ship.Definition.Name}",
ScopeKind = "ship",
ScopeId = shipId,
Kind = "direct-control",
@@ -732,7 +740,7 @@ internal sealed class PlayerFactionService
player.Directives.Add(directive);
}
directive.Label = $"Direct control {ship.Definition.Label}";
directive.Label = $"Direct control {ship.Definition.Name}";
directive.Kind = "direct-control";
directive.ScopeKind = "ship";
directive.ScopeId = shipId;
@@ -746,7 +754,7 @@ internal sealed class PlayerFactionService
directive.HomeStationId = request.HomeStationId;
directive.SourceStationId = request.HomeStationId;
directive.DestinationStationId = null;
directive.ItemId = request.PreferredItemId;
directive.ItemId = request.ItemId;
directive.PreferredNodeId = request.PreferredNodeId;
directive.PreferredConstructionSiteId = request.PreferredConstructionSiteId;
directive.PreferredModuleId = request.PreferredModuleId;
@@ -793,7 +801,7 @@ internal sealed class PlayerFactionService
ship.ControlSourceKind = "player-directive";
ship.ControlSourceId = directive.Id;
ship.ControlReason = directive.Label;
AddDecision(player, "ship-behavior-configured", $"Configured {request.Kind} for {ship.Definition.Label}.", "ship", shipId);
AddDecision(player, "ship-behavior-configured", $"Configured {request.Kind} for {ship.Definition.Name}.", "ship", shipId);
player.UpdatedAtUtc = directive.UpdatedAtUtc;
ship.NeedsReplan = true;
ship.LastReplanReason = "player-behavior-configured";
@@ -826,7 +834,7 @@ internal sealed class PlayerFactionService
{
Id = "player-core-automation",
Label = "Core Automation",
BehaviorKind = "idle",
BehaviorKind = Idle,
});
}
@@ -1035,7 +1043,7 @@ internal sealed class PlayerFactionService
var changed = ApplyDirectiveToShip(commander, ship, directive, automation, assignment);
if (changed && directive is not null)
{
events.Add(new SimulationEventRecord("ship", ship.Id, "player-directive", $"{ship.Definition.Label} aligned to player directive {directive.Label}.", DateTimeOffset.UtcNow, "player", "universe", ship.Id));
events.Add(new SimulationEventRecord("ship", ship.Id, "player-directive", $"{ship.Definition.Name} aligned to player directive {directive.Label}.", DateTimeOffset.UtcNow, "player", "universe", ship.Id));
}
}
@@ -1246,13 +1254,13 @@ internal sealed class PlayerFactionService
? "player-directive"
: automation is not null
? "player-automation"
: ship.OrderQueue.Any(order => !order.Id.StartsWith("ai-order-", StringComparison.Ordinal))
: ship.OrderQueue.Any(order => order.SourceKind == ShipOrderSourceKind.Player)
? "player-order"
: "player-manual";
var desiredControlSourceId = directive?.Id
?? automation?.Id
?? ship.OrderQueue
.Where(order => !order.Id.StartsWith("ai-order-", StringComparison.Ordinal))
.Where(order => order.SourceKind == ShipOrderSourceKind.Player)
.OrderByDescending(order => order.Priority)
.ThenBy(order => order.CreatedAtUtc)
.Select(order => order.Id)
@@ -1260,7 +1268,7 @@ internal sealed class PlayerFactionService
var desiredControlReason = directive?.Label
?? automation?.Label
?? ship.OrderQueue
.Where(order => !order.Id.StartsWith("ai-order-", StringComparison.Ordinal))
.Where(order => order.SourceKind == ShipOrderSourceKind.Player)
.OrderByDescending(order => order.Priority)
.ThenBy(order => order.CreatedAtUtc)
.Select(order => order.Label ?? order.Kind)
@@ -1342,7 +1350,7 @@ internal sealed class PlayerFactionService
HomeStationId = directive?.HomeStationId ?? ship.DefaultBehavior.HomeStationId,
AreaSystemId = directive?.TargetSystemId ?? directive?.HomeSystemId ?? ship.DefaultBehavior.AreaSystemId ?? ship.SystemId,
TargetEntityId = directive?.TargetEntityId,
PreferredItemId = directive?.ItemId ?? automation?.PreferredItemId ?? ship.DefaultBehavior.PreferredItemId,
ItemId = directive?.ItemId ?? automation?.PreferredItemId ?? ship.DefaultBehavior.ItemId,
PreferredNodeId = directive?.PreferredNodeId ?? ship.DefaultBehavior.PreferredNodeId,
PreferredConstructionSiteId = directive?.PreferredConstructionSiteId ?? ship.DefaultBehavior.PreferredConstructionSiteId,
PreferredModuleId = directive?.PreferredModuleId ?? ship.DefaultBehavior.PreferredModuleId,
@@ -1375,6 +1383,8 @@ internal sealed class PlayerFactionService
{
Id = aiOrderId!,
Kind = directive.StagingOrderKind!,
SourceKind = ShipOrderSourceKind.Player,
SourceId = directive.Id,
Priority = Math.Max(0, directive.Priority),
InterruptCurrentPlan = true,
Label = directive.Label,
@@ -1447,7 +1457,7 @@ internal sealed class PlayerFactionService
target.HomeStationId = source.HomeStationId;
target.AreaSystemId = source.AreaSystemId;
target.TargetEntityId = source.TargetEntityId;
target.PreferredItemId = source.PreferredItemId;
target.ItemId = source.ItemId;
target.PreferredNodeId = source.PreferredNodeId;
target.PreferredConstructionSiteId = source.PreferredConstructionSiteId;
target.PreferredModuleId = source.PreferredModuleId;
@@ -1468,7 +1478,7 @@ internal sealed class PlayerFactionService
&& string.Equals(left.HomeStationId, right.HomeStationId, StringComparison.Ordinal)
&& string.Equals(left.AreaSystemId, right.AreaSystemId, StringComparison.Ordinal)
&& string.Equals(left.TargetEntityId, right.TargetEntityId, StringComparison.Ordinal)
&& string.Equals(left.PreferredItemId, right.PreferredItemId, StringComparison.Ordinal)
&& string.Equals(left.ItemId, right.ItemId, StringComparison.Ordinal)
&& string.Equals(left.PreferredNodeId, right.PreferredNodeId, StringComparison.Ordinal)
&& string.Equals(left.PreferredConstructionSiteId, right.PreferredConstructionSiteId, StringComparison.Ordinal)
&& string.Equals(left.PreferredModuleId, right.PreferredModuleId, StringComparison.Ordinal)
@@ -1501,6 +1511,8 @@ internal sealed class PlayerFactionService
private static bool ShipOrdersEqual(ShipOrderRuntime left, ShipOrderRuntime right) =>
string.Equals(left.Id, right.Id, StringComparison.Ordinal)
&& string.Equals(left.Kind, right.Kind, StringComparison.Ordinal)
&& left.SourceKind == right.SourceKind
&& string.Equals(left.SourceId, right.SourceId, StringComparison.Ordinal)
&& left.Priority == right.Priority
&& left.InterruptCurrentPlan == right.InterruptCurrentPlan
&& string.Equals(left.Label, right.Label, StringComparison.Ordinal)
@@ -1716,7 +1728,7 @@ internal sealed class PlayerFactionService
{
program.CurrentCount = world.Ships.Count(ship =>
ship.FactionId == player.SovereignFactionId &&
string.Equals(ship.Definition.Kind, program.TargetShipKind, StringComparison.Ordinal));
string.Equals(GetShipCategory(ship.Definition), program.TargetShipKind, StringComparison.Ordinal));
}
else
{
@@ -2113,7 +2125,7 @@ internal sealed class PlayerFactionService
{
var available = world.Ships.Count(ship =>
ship.FactionId == player.SovereignFactionId &&
string.Equals(ship.Definition.Kind, policy.ShipKind, StringComparison.Ordinal));
string.Equals(GetShipCategory(ship.Definition), policy.ShipKind, StringComparison.Ordinal));
if (available < policy.DesiredAssetCount)
{
player.Alerts.Add(new PlayerAlertRuntime

View File

@@ -0,0 +1,26 @@
namespace SpaceGame.Api.PlayerFaction.Simulation;
public sealed class PlayerStateStore : IPlayerStateStore
{
private readonly Dictionary<string, PlayerFactionRuntime> _playerFactions = new(StringComparer.Ordinal);
public bool TryGetPlayerFaction(string playerId, out PlayerFactionRuntime playerFaction) =>
_playerFactions.TryGetValue(playerId, out playerFaction!);
public PlayerFactionRuntime GetOrAddPlayerFaction(string playerId, Func<PlayerFactionRuntime> factory)
{
if (_playerFactions.TryGetValue(playerId, out var existing))
{
return existing;
}
var created = factory();
_playerFactions[playerId] = created;
return created;
}
public IReadOnlyCollection<PlayerFactionRuntime> GetPlayerFactions() =>
_playerFactions.Values.ToList();
public void Clear() => _playerFactions.Clear();
}