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

@@ -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