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

@@ -3,6 +3,7 @@ namespace SpaceGame.Api.Simulation.Core;
internal sealed class SimulationEngine
{
private readonly IBalanceService _balance;
private readonly IPlayerStateStore _playerStateStore;
private readonly OrbitalSimulationOptions _orbitalSimulation;
private readonly OrbitalStateUpdater _orbitalStateUpdater;
private readonly InfrastructureSimulationService _infrastructureSimulation;
@@ -14,9 +15,10 @@ internal sealed class SimulationEngine
private readonly ShipAiService _shipAi;
private readonly SimulationProjectionService _projection;
internal SimulationEngine(OrbitalSimulationOptions orbitalSimulation, IBalanceService balance)
internal SimulationEngine(OrbitalSimulationOptions orbitalSimulation, IBalanceService balance, IPlayerStateStore playerStateStore)
{
_balance = balance;
_playerStateStore = playerStateStore;
_orbitalSimulation = orbitalSimulation;
_orbitalStateUpdater = new OrbitalStateUpdater(_orbitalSimulation);
_infrastructureSimulation = new InfrastructureSimulationService();
@@ -42,8 +44,8 @@ internal sealed class SimulationEngine
_infrastructureSimulation.UpdateClaims(world, events);
_infrastructureSimulation.UpdateConstructionSites(world, events);
_geopolitics.Update(world, simulationDeltaSeconds, events);
_commanderPlanning.UpdateCommanders(world, simulationDeltaSeconds, events);
_playerFaction.Update(world, simulationDeltaSeconds, events);
_commanderPlanning.UpdateCommanders(world, _playerStateStore, simulationDeltaSeconds, events);
_playerFaction.Update(world, _playerStateStore, simulationDeltaSeconds, events);
_stationLifecycle.UpdateStations(world, simulationDeltaSeconds, events);
foreach (var ship in world.Ships.ToList())
@@ -76,7 +78,7 @@ internal sealed class SimulationEngine
{
foreach (var ship in world.Ships.Where(candidate => candidate.Health <= 0f).ToList())
{
CreateWreck(world, "ship", ship.Id, ship.SystemId, ship.Position, ship.Definition.CargoCapacity + (ship.Definition.MaxHealth * 0.08f));
CreateWreck(world, "ship", ship.Id, ship.SystemId, ship.Position, ship.Definition.GetTotalCargoCapacity() + (ship.Definition.Hull * 0.08f));
world.Ships.Remove(ship);
if (ship.DockedStationId is not null && world.Stations.FirstOrDefault(station => station.Id == ship.DockedStationId) is { } dockedStation)
{
@@ -94,7 +96,7 @@ internal sealed class SimulationEngine
commander.IsAlive = false;
}
events.Add(new SimulationEventRecord("ship", ship.Id, "destroyed", $"{ship.Definition.Label} was destroyed.", DateTimeOffset.UtcNow));
events.Add(new SimulationEventRecord("ship", ship.Id, "destroyed", $"{ship.Definition.Name} was destroyed.", DateTimeOffset.UtcNow));
}
foreach (var station in world.Stations.Where(candidate => candidate.Health <= 0f).ToList())

View File

@@ -32,7 +32,6 @@ internal sealed class SimulationProjectionService
BuildPolicyDeltas(world),
BuildShipDeltas(world),
BuildFactionDeltas(world),
BuildPlayerFactionDelta(world),
BuildGeopoliticsDelta(world));
public WorldSnapshot BuildSnapshot(SimulationWorld world, long sequence)
@@ -177,9 +176,9 @@ internal sealed class SimulationProjectionService
policy.BlacklistedSystemIds.OrderBy(id => id, StringComparer.Ordinal).ToList())).ToList(),
world.Ships.Select(ship => ToShipDelta(world, ship)).Select(ship => new ShipSnapshot(
ship.Id,
ship.Label,
ship.Kind,
ship.Class,
ship.Name,
ship.Purpose,
ship.Type,
ship.SystemId,
ship.LocalPosition,
ship.LocalVelocity,
@@ -225,7 +224,6 @@ internal sealed class SimulationProjectionService
faction.StrategicState,
faction.DecisionLog,
faction.Commanders)).ToList(),
ToPlayerFactionSnapshot(world.PlayerFaction),
ToGeopoliticalStateSnapshot(world.Geopolitics));
}
@@ -276,11 +274,6 @@ internal sealed class SimulationProjectionService
faction.LastDeltaSignature = BuildFactionSignature(faction, FindFactionCommander(world, faction.Id));
}
if (world.PlayerFaction is not null)
{
world.PlayerFaction.LastDeltaSignature = BuildPlayerFactionSignature(world.PlayerFaction);
}
if (world.Geopolitics is not null)
{
world.Geopolitics.LastDeltaSignature = BuildGeopoliticalSignature(world.Geopolitics);
@@ -450,23 +443,6 @@ internal sealed class SimulationProjectionService
return deltas;
}
private static PlayerFactionSnapshot? BuildPlayerFactionDelta(SimulationWorld world)
{
if (world.PlayerFaction is null)
{
return null;
}
var signature = BuildPlayerFactionSignature(world.PlayerFaction);
if (signature == world.PlayerFaction.LastDeltaSignature)
{
return null;
}
world.PlayerFaction.LastDeltaSignature = signature;
return ToPlayerFactionSnapshot(world.PlayerFaction);
}
private static GeopoliticalStateSnapshot? BuildGeopoliticsDelta(SimulationWorld world)
{
if (world.Geopolitics is null)
@@ -544,11 +520,13 @@ internal sealed class SimulationProjectionService
ship.TargetPosition.Z.ToString("0.###"),
ship.State.ToContractValue(),
string.Join(",", ship.OrderQueue
.OrderByDescending(order => order.Priority)
.OrderByDescending(GetOrderSourcePriority)
.ThenByDescending(order => order.Priority)
.ThenBy(order => order.CreatedAtUtc)
.Select(order => $"{order.Id}:{order.Kind}:{order.Status.ToContractValue()}:{order.Priority}:{order.TargetEntityId}:{order.TargetSystemId}:{order.ItemId}:{order.WaitSeconds:0.###}:{order.Radius:0.###}:{order.MaxSystemRange?.ToString(CultureInfo.InvariantCulture) ?? "none"}:{order.KnownStationsOnly}")),
.Select(order => $"{order.Id}:{order.Kind}:{order.SourceKind.ToContractValue()}:{order.SourceId}:{order.Status.ToContractValue()}:{order.Priority}:{order.TargetEntityId}:{order.TargetSystemId}:{order.ItemId}:{order.WaitSeconds:0.###}:{order.Radius:0.###}:{order.MaxSystemRange?.ToString(CultureInfo.InvariantCulture) ?? "none"}:{order.KnownStationsOnly}")),
ship.DefaultBehavior.Kind,
ship.DefaultBehavior.TargetEntityId ?? "none",
ship.DefaultBehavior.ItemId ?? "none",
ship.DefaultBehavior.TargetPosition?.X.ToString("0.###") ?? "none",
ship.DefaultBehavior.TargetPosition?.Y.ToString("0.###") ?? "none",
ship.DefaultBehavior.TargetPosition?.Z.ToString("0.###") ?? "none",
@@ -642,59 +620,6 @@ internal sealed class SimulationProjectionService
return $"{faction.Credits:0.###}|{faction.PopulationTotal:0.###}|{faction.OreMined:0.###}|{faction.GoodsProduced:0.###}|{faction.ShipsBuilt}|{faction.ShipsLost}|{faction.DefaultPolicySetId}|{assignmentSig}|{strategicSig}|{doctrineSig}|{decisionSig}|{theaterSig}|{campaignSig}|{objectiveSig}|{reservationSig}|{productionSig}";
}
private static string BuildPlayerFactionSignature(PlayerFactionRuntime player)
{
var intentSig = $"{player.StrategicIntent.StrategicPosture}:{player.StrategicIntent.EconomicPosture}:{player.StrategicIntent.MilitaryPosture}:{player.StrategicIntent.LogisticsPosture}:{player.StrategicIntent.DesiredReserveRatio:0.###}";
var registrySig = string.Join("|",
player.AssetRegistry.ShipIds.Count,
player.AssetRegistry.StationIds.Count,
player.AssetRegistry.CommanderIds.Count,
player.AssetRegistry.FleetIds.Count,
player.AssetRegistry.TaskForceIds.Count,
player.AssetRegistry.StationGroupIds.Count,
player.AssetRegistry.EconomicRegionIds.Count,
player.AssetRegistry.FrontIds.Count,
player.AssetRegistry.ReserveIds.Count);
var orgSig = string.Join("|",
player.Fleets.Count,
player.TaskForces.Count,
player.StationGroups.Count,
player.EconomicRegions.Count,
player.Fronts.Count,
player.Reserves.Count,
player.Policies.Count,
player.AutomationPolicies.Count,
player.ReinforcementPolicies.Count,
player.ProductionPrograms.Count,
player.Directives.Count,
player.Assignments.Count,
player.Alerts.Count);
var policySig = string.Join(";",
player.Policies.OrderBy(policy => policy.Id, StringComparer.Ordinal)
.Select(policy => $"{policy.Id}:{policy.ScopeKind}:{policy.ScopeId}:{policy.PolicySetId}:{policy.TradeAccessPolicy}:{policy.DockingAccessPolicy}:{policy.ConstructionAccessPolicy}:{policy.OperationalRangePolicy}:{policy.CombatEngagementPolicy}:{policy.AvoidHostileSystems}:{policy.FleeHullRatio:0.###}:{policy.UpdatedAtUtc.UtcTicks}"));
var automationSig = string.Join(";",
player.AutomationPolicies.OrderBy(policy => policy.Id, StringComparer.Ordinal)
.Select(policy => $"{policy.Id}:{policy.ScopeKind}:{policy.ScopeId}:{policy.Enabled}:{policy.BehaviorKind}:{policy.UseOrders}:{policy.StagingOrderKind}:{policy.MaxSystemRange}:{policy.KnownStationsOnly}:{policy.Radius:0.###}:{policy.WaitSeconds:0.###}:{policy.PreferredItemId}:{policy.UpdatedAtUtc.UtcTicks}"));
var directiveSig = string.Join(";",
player.Directives.OrderBy(directive => directive.Id, StringComparer.Ordinal)
.Select(directive => $"{directive.Id}:{directive.ScopeKind}:{directive.ScopeId}:{directive.Kind}:{directive.BehaviorKind}:{directive.UseOrders}:{directive.StagingOrderKind}:{directive.TargetEntityId}:{directive.TargetSystemId}:{directive.ItemId}:{directive.Priority}:{directive.UpdatedAtUtc.UtcTicks}"));
var assignmentSig = string.Join(";",
player.Assignments.OrderBy(assignment => assignment.Id, StringComparer.Ordinal)
.Select(assignment => $"{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.UtcTicks}"));
var decisionSig = string.Join(",", player.DecisionLog.Select(entry => entry.Id));
var orgDetailSig = string.Join(";",
player.Fleets.OrderBy(entry => entry.Id, StringComparer.Ordinal).Select(entry => $"fleet:{entry.Id}:{entry.FrontId}:{entry.HomeSystemId}:{entry.HomeStationId}:{entry.CommanderId}:{entry.UpdatedAtUtc.UtcTicks}")
.Concat(player.TaskForces.OrderBy(entry => entry.Id, StringComparer.Ordinal).Select(entry => $"task-force:{entry.Id}:{entry.FleetId}:{entry.FrontId}:{entry.CommanderId}:{entry.UpdatedAtUtc.UtcTicks}"))
.Concat(player.StationGroups.OrderBy(entry => entry.Id, StringComparer.Ordinal).Select(entry => $"station-group:{entry.Id}:{entry.EconomicRegionId}:{entry.UpdatedAtUtc.UtcTicks}"))
.Concat(player.EconomicRegions.OrderBy(entry => entry.Id, StringComparer.Ordinal).Select(entry => $"economic-region:{entry.Id}:{entry.SharedEconomicRegionId}:{entry.Role}:{entry.UpdatedAtUtc.UtcTicks}"))
.Concat(player.Fronts.OrderBy(entry => entry.Id, StringComparer.Ordinal).Select(entry => $"front:{entry.Id}:{entry.SharedFrontLineId}:{entry.TargetFactionId}:{entry.Priority:0.###}:{entry.UpdatedAtUtc.UtcTicks}"))
.Concat(player.Reserves.OrderBy(entry => entry.Id, StringComparer.Ordinal).Select(entry => $"reserve:{entry.Id}:{entry.HomeSystemId}:{entry.UpdatedAtUtc.UtcTicks}")));
var alertSig = string.Join(";",
player.Alerts.OrderBy(alert => alert.Id, StringComparer.Ordinal)
.Select(alert => $"{alert.Id}:{alert.Kind}:{alert.Severity}:{alert.AssetKind}:{alert.AssetId}:{alert.RelatedDirectiveId}:{alert.Status}:{alert.CreatedAtUtc.UtcTicks}"));
return $"{player.SovereignFactionId}|{player.Status}|{intentSig}|{registrySig}|{orgSig}|{policySig}|{automationSig}|{directiveSig}|{assignmentSig}|{decisionSig}|{orgDetailSig}|{alertSig}";
}
private static string BuildGeopoliticalSignature(GeopoliticalStateRuntime state)
{
var diplomacySig = string.Join(";",
@@ -882,9 +807,9 @@ internal sealed class SimulationProjectionService
return new ShipDelta(
ship.Id,
ship.Definition.Label,
ship.Definition.Kind,
ship.Definition.Class,
ship.Definition.Name,
ship.Definition.Purpose.ToDataValue(),
ship.Definition.Type.ToDataValue(),
ship.SystemId,
ToDto(ship.Position),
ToDto(ship.Velocity),
@@ -906,7 +831,7 @@ internal sealed class SimulationProjectionService
ship.DockedStationId,
ship.CommanderId,
ship.PolicySetId,
ship.Definition.CargoCapacity,
ship.Definition.GetTotalCargoCapacity(),
ToShipTravelSpeed(ship).Speed,
ToShipTravelSpeed(ship).Unit,
@@ -936,11 +861,14 @@ internal sealed class SimulationProjectionService
private static IReadOnlyList<ShipOrderSnapshot> ToShipOrderSnapshots(ShipRuntime ship) =>
ship.OrderQueue
.OrderByDescending(order => order.Priority)
.OrderByDescending(GetOrderSourcePriority)
.ThenByDescending(order => order.Priority)
.ThenBy(order => order.CreatedAtUtc)
.Select(order => new ShipOrderSnapshot(
order.Id,
order.Kind,
order.SourceKind.ToContractValue(),
order.SourceId,
order.Status.ToContractValue(),
order.Priority,
order.InterruptCurrentPlan,
@@ -962,6 +890,14 @@ internal sealed class SimulationProjectionService
order.FailureReason))
.ToList();
private static int GetOrderSourcePriority(ShipOrderRuntime order) => order.SourceKind switch
{
ShipOrderSourceKind.Player => 300,
ShipOrderSourceKind.Commander => 200,
ShipOrderSourceKind.Behavior => 100,
_ => 0,
};
private static DefaultBehaviorSnapshot ToDefaultBehaviorSnapshot(DefaultBehaviorRuntime behavior) =>
new(
behavior.Kind,
@@ -969,7 +905,7 @@ internal sealed class SimulationProjectionService
behavior.HomeStationId,
behavior.AreaSystemId,
behavior.TargetEntityId,
behavior.PreferredItemId,
behavior.ItemId,
behavior.PreferredNodeId,
behavior.PreferredConstructionSiteId,
behavior.PreferredModuleId,
@@ -1385,252 +1321,6 @@ internal sealed class SimulationProjectionService
entry.OccurredAtUtc))
.ToList();
private static PlayerFactionSnapshot? ToPlayerFactionSnapshot(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 GeopoliticalStateSnapshot? ToGeopoliticalStateSnapshot(GeopoliticalStateRuntime? state)
{
if (state is null)