Files
space-game/apps/backend/Simulation/Core/SimulationProjectionService.cs

1895 lines
86 KiB
C#

using System.Globalization;
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService;
using static SpaceGame.Api.Stations.Simulation.StationSimulationService;
namespace SpaceGame.Api.Simulation.Core;
internal sealed class SimulationProjectionService
{
private readonly OrbitalSimulationOptions _orbitalSimulation;
internal SimulationProjectionService(OrbitalSimulationOptions orbitalSimulation)
{
_orbitalSimulation = orbitalSimulation;
}
internal WorldDelta BuildDelta(SimulationWorld world, long sequence, IReadOnlyList<SimulationEventRecord> events) =>
new(
sequence,
world.TickIntervalMs,
world.OrbitalTimeSeconds,
new OrbitalSimulationSnapshot(_orbitalSimulation.SimulatedSecondsPerRealSecond),
world.GeneratedAtUtc,
false,
events,
BuildCelestialDeltas(world),
BuildNodeDeltas(world),
BuildStationDeltas(world),
BuildClaimDeltas(world),
BuildConstructionSiteDeltas(world),
BuildMarketOrderDeltas(world),
BuildPolicyDeltas(world),
BuildShipDeltas(world),
BuildFactionDeltas(world),
BuildPlayerFactionDelta(world),
BuildGeopoliticsDelta(world));
public WorldSnapshot BuildSnapshot(SimulationWorld world, long sequence)
{
PrimeDeltaBaseline(world);
return new WorldSnapshot(
world.Label,
world.Seed,
sequence,
world.TickIntervalMs,
world.OrbitalTimeSeconds,
new OrbitalSimulationSnapshot(_orbitalSimulation.SimulatedSecondsPerRealSecond),
world.GeneratedAtUtc,
world.Systems.Select(system => new SystemSnapshot(
system.Definition.Id,
system.Definition.Label,
ToDto(system.Position),
system.Definition.Stars.Select(star => new StarSnapshot(
star.Kind,
star.Color,
star.Glow,
star.Size,
star.OrbitRadius,
star.OrbitSpeed,
star.OrbitPhaseAtEpoch)).ToList(),
system.Definition.Planets.Select(planet => new PlanetSnapshot(
planet.Label,
planet.PlanetType,
planet.Shape,
planet.Moons.Select(moon => new MoonSnapshot(
moon.Label,
moon.Size,
moon.Color,
moon.OrbitRadius,
moon.OrbitSpeed,
moon.OrbitPhaseAtEpoch,
moon.OrbitInclination,
moon.OrbitLongitudeOfAscendingNode)).ToList(),
planet.OrbitRadius,
planet.OrbitSpeed,
planet.OrbitEccentricity,
planet.OrbitInclination,
planet.OrbitLongitudeOfAscendingNode,
planet.OrbitArgumentOfPeriapsis,
planet.OrbitPhaseAtEpoch,
planet.Size,
planet.Color,
planet.HasRing)).ToList())).ToList(),
world.Celestials.Select(ToCelestialDelta).Select(c => new CelestialSnapshot(
c.Id,
c.SystemId,
c.Kind,
c.OrbitalAnchor,
c.LocalSpaceRadius,
c.ParentNodeId,
c.OccupyingStructureId,
c.OrbitReferenceId)).ToList(),
world.Nodes.Select(ToNodeDelta).Select(node => new ResourceNodeSnapshot(
node.Id,
node.SystemId,
node.LocalPosition,
node.CelestialId,
node.SourceKind,
node.OreRemaining,
node.MaxOre,
node.ItemId)).ToList(),
world.Stations.Select(station => ToStationDelta(world, station)).Select(station => new StationSnapshot(
station.Id,
station.Label,
station.Category,
station.Objective,
station.SystemId,
station.LocalPosition,
station.CelestialId,
station.Color,
station.DockedShips,
station.DockedShipIds,
station.DockingPads,
station.CurrentProcesses,
station.Inventory,
station.FactionId,
station.CommanderId,
station.PolicySetId,
station.Population,
station.PopulationCapacity,
station.WorkforceRequired,
station.WorkforceEffectiveRatio,
station.StorageUsage,
station.InstalledModules,
station.MarketOrderIds)).ToList(),
world.Claims.Select(ToClaimDelta).Select(claim => new ClaimSnapshot(
claim.Id,
claim.FactionId,
claim.SystemId,
claim.CelestialId,
claim.State,
claim.Health,
claim.PlacedAtUtc,
claim.ActivatesAtUtc)).ToList(),
world.ConstructionSites.Select(site => ToConstructionSiteDelta(world, site)).Select(site => new ConstructionSiteSnapshot(
site.Id,
site.FactionId,
site.SystemId,
site.CelestialId,
site.TargetKind,
site.TargetDefinitionId,
site.BlueprintId,
site.ClaimId,
site.StationId,
site.State,
site.Progress,
site.Inventory,
site.RequiredItems,
site.DeliveredItems,
site.AssignedConstructorShipIds,
site.MarketOrderIds)).ToList(),
world.MarketOrders.Select(ToMarketOrderDelta).Select(order => new MarketOrderSnapshot(
order.Id,
order.FactionId,
order.StationId,
order.ConstructionSiteId,
order.Kind,
order.ItemId,
order.Amount,
order.RemainingAmount,
order.Valuation,
order.ReserveThreshold,
order.PolicySetId,
order.State)).ToList(),
world.Policies.Select(ToPolicySetDelta).Select(policy => new PolicySetSnapshot(
policy.Id,
policy.OwnerKind,
policy.OwnerId,
policy.TradeAccessPolicy,
policy.DockingAccessPolicy,
policy.ConstructionAccessPolicy,
policy.OperationalRangePolicy,
policy.CombatEngagementPolicy,
policy.AvoidHostileSystems,
policy.FleeHullRatio,
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.SystemId,
ship.LocalPosition,
ship.LocalVelocity,
ship.TargetLocalPosition,
ship.State,
ship.OrderQueue,
ship.DefaultBehavior,
ship.Assignment,
ship.Skills,
ship.ActivePlan,
ship.CurrentStepId,
ship.ActiveSubTasks,
ship.ControlSourceKind,
ship.ControlSourceId,
ship.ControlReason,
ship.LastReplanReason,
ship.LastAccessFailureReason,
ship.CelestialId,
ship.DockedStationId,
ship.CommanderId,
ship.PolicySetId,
ship.CargoCapacity,
ship.TravelSpeed,
ship.TravelSpeedUnit,
ship.Inventory,
ship.FactionId,
ship.Health,
ship.History,
ship.SpatialState)).ToList(),
world.Factions.Select(faction => ToFactionDelta(world, faction, FindFactionCommander(world, faction.Id))).Select(faction => new FactionSnapshot(
faction.Id,
faction.Label,
faction.Color,
faction.Credits,
faction.PopulationTotal,
faction.OreMined,
faction.GoodsProduced,
faction.ShipsBuilt,
faction.ShipsLost,
faction.DefaultPolicySetId,
faction.Doctrine,
faction.Memory,
faction.StrategicState,
faction.DecisionLog,
faction.Commanders)).ToList(),
ToPlayerFactionSnapshot(world.PlayerFaction),
ToGeopoliticalStateSnapshot(world.Geopolitics));
}
public void PrimeDeltaBaseline(SimulationWorld world)
{
foreach (var node in world.Nodes)
{
node.LastDeltaSignature = BuildNodeSignature(node);
}
foreach (var celestial in world.Celestials)
{
celestial.LastDeltaSignature = BuildCelestialSignature(celestial);
}
foreach (var station in world.Stations)
{
station.LastDeltaSignature = BuildStationSignature(world, station);
}
foreach (var claim in world.Claims)
{
claim.LastDeltaSignature = BuildClaimSignature(claim);
}
foreach (var site in world.ConstructionSites)
{
site.LastDeltaSignature = BuildConstructionSiteSignature(site);
}
foreach (var order in world.MarketOrders)
{
order.LastDeltaSignature = BuildMarketOrderSignature(order);
}
foreach (var policy in world.Policies)
{
policy.LastDeltaSignature = BuildPolicySignature(policy);
}
foreach (var ship in world.Ships)
{
ship.LastDeltaSignature = BuildShipSignature(world, ship);
}
foreach (var faction in world.Factions)
{
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);
}
}
private static IReadOnlyList<ResourceNodeDelta> BuildNodeDeltas(SimulationWorld world)
{
var deltas = new List<ResourceNodeDelta>();
foreach (var node in world.Nodes)
{
var signature = BuildNodeSignature(node);
if (signature == node.LastDeltaSignature)
{
continue;
}
node.LastDeltaSignature = signature;
deltas.Add(ToNodeDelta(node));
}
return deltas;
}
private static IReadOnlyList<CelestialDelta> BuildCelestialDeltas(SimulationWorld world)
{
var deltas = new List<CelestialDelta>();
foreach (var celestial in world.Celestials)
{
var signature = BuildCelestialSignature(celestial);
if (signature == celestial.LastDeltaSignature)
{
continue;
}
celestial.LastDeltaSignature = signature;
deltas.Add(ToCelestialDelta(celestial));
}
return deltas;
}
private static IReadOnlyList<StationDelta> BuildStationDeltas(SimulationWorld world)
{
var deltas = new List<StationDelta>();
foreach (var station in world.Stations)
{
var signature = BuildStationSignature(world, station);
if (signature == station.LastDeltaSignature)
{
continue;
}
station.LastDeltaSignature = signature;
deltas.Add(ToStationDelta(world, station));
}
return deltas;
}
private static IReadOnlyList<ClaimDelta> BuildClaimDeltas(SimulationWorld world)
{
var deltas = new List<ClaimDelta>();
foreach (var claim in world.Claims)
{
var signature = BuildClaimSignature(claim);
if (signature == claim.LastDeltaSignature)
{
continue;
}
claim.LastDeltaSignature = signature;
deltas.Add(ToClaimDelta(claim));
}
return deltas;
}
private static IReadOnlyList<ConstructionSiteDelta> BuildConstructionSiteDeltas(SimulationWorld world)
{
var deltas = new List<ConstructionSiteDelta>();
foreach (var site in world.ConstructionSites)
{
var signature = BuildConstructionSiteSignature(site);
if (signature == site.LastDeltaSignature)
{
continue;
}
site.LastDeltaSignature = signature;
deltas.Add(ToConstructionSiteDelta(world, site));
}
return deltas;
}
private static IReadOnlyList<MarketOrderDelta> BuildMarketOrderDeltas(SimulationWorld world)
{
var deltas = new List<MarketOrderDelta>();
foreach (var order in world.MarketOrders)
{
var signature = BuildMarketOrderSignature(order);
if (signature == order.LastDeltaSignature)
{
continue;
}
order.LastDeltaSignature = signature;
deltas.Add(ToMarketOrderDelta(order));
}
return deltas;
}
private static IReadOnlyList<PolicySetDelta> BuildPolicyDeltas(SimulationWorld world)
{
var deltas = new List<PolicySetDelta>();
foreach (var policy in world.Policies)
{
var signature = BuildPolicySignature(policy);
if (signature == policy.LastDeltaSignature)
{
continue;
}
policy.LastDeltaSignature = signature;
deltas.Add(ToPolicySetDelta(policy));
}
return deltas;
}
private IReadOnlyList<ShipDelta> BuildShipDeltas(SimulationWorld world)
{
var deltas = new List<ShipDelta>();
foreach (var ship in world.Ships)
{
var signature = BuildShipSignature(world, ship);
if (signature == ship.LastDeltaSignature)
{
continue;
}
ship.LastDeltaSignature = signature;
deltas.Add(ToShipDelta(world, ship));
}
return deltas;
}
private static IReadOnlyList<FactionDelta> BuildFactionDeltas(SimulationWorld world)
{
var deltas = new List<FactionDelta>();
foreach (var faction in world.Factions)
{
var commander = FindFactionCommander(world, faction.Id);
var signature = BuildFactionSignature(faction, commander);
if (signature == faction.LastDeltaSignature)
{
continue;
}
faction.LastDeltaSignature = signature;
deltas.Add(ToFactionDelta(world, faction, commander));
}
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)
{
return null;
}
var signature = BuildGeopoliticalSignature(world.Geopolitics);
if (signature == world.Geopolitics.LastDeltaSignature)
{
return null;
}
world.Geopolitics.LastDeltaSignature = signature;
return ToGeopoliticalStateSnapshot(world.Geopolitics);
}
private static CommanderRuntime? FindFactionCommander(SimulationWorld world, string factionId) =>
world.Commanders.FirstOrDefault(c =>
c.FactionId == factionId &&
string.Equals(c.Kind, CommanderKind.Faction, StringComparison.Ordinal));
private static string BuildNodeSignature(ResourceNodeRuntime node) =>
$"{node.SystemId}|{node.Position.X:0.###}|{node.Position.Y:0.###}|{node.Position.Z:0.###}|{node.CelestialId}|{node.OreRemaining:0.###}";
private static string BuildCelestialSignature(CelestialRuntime celestial) =>
$"{celestial.SystemId}|{celestial.Kind.ToContractValue()}|{celestial.Position.X:0.###}|{celestial.Position.Y:0.###}|{celestial.Position.Z:0.###}|{celestial.LocalSpaceRadius:0.###}|{celestial.ParentNodeId}|{celestial.OccupyingStructureId}|{celestial.OrbitReferenceId}";
private static string BuildStationSignature(SimulationWorld world, StationRuntime station)
{
var processes = ToStationActionProgressSnapshots(world, station);
return string.Join("|",
station.SystemId,
station.CelestialId ?? "none",
station.CommanderId ?? "none",
station.PolicySetId ?? "none",
BuildInventorySignature(station.Inventory),
string.Join(",", processes.Select(process => $"{process.Lane}:{process.Label}:{process.Progress:0.###}")),
string.Join(",", station.DockedShipIds.OrderBy(id => id, StringComparer.Ordinal)),
station.DockingPadAssignments.Count.ToString(),
station.Population.ToString("0.###"),
station.PopulationCapacity.ToString("0.###"),
station.WorkforceRequired.ToString("0.###"),
station.WorkforceEffectiveRatio.ToString("0.###"),
string.Join(",", station.InstalledModules.OrderBy(moduleId => moduleId, StringComparer.Ordinal)),
string.Join(",", station.MarketOrderIds.OrderBy(orderId => orderId, StringComparer.Ordinal)),
station.ActiveConstruction?.ModuleId ?? "none",
station.ActiveConstruction?.ProgressSeconds.ToString("0.###") ?? "0",
string.Join(",", station.ProductionLaneTimers.OrderBy(entry => entry.Key, StringComparer.Ordinal).Select(entry => $"{entry.Key}:{entry.Value:0.###}")));
}
private static string BuildClaimSignature(ClaimRuntime claim) =>
$"{claim.FactionId}|{claim.SystemId}|{claim.CelestialId}|{claim.State}|{claim.Health:0.###}|{claim.ActivatesAtUtc:O}";
private static string BuildConstructionSiteSignature(ConstructionSiteRuntime site) =>
$"{site.FactionId}|{site.SystemId}|{site.CelestialId}|{site.TargetKind}|{site.TargetDefinitionId}|{site.BlueprintId}|{site.ClaimId}|{site.StationId}|{site.State}|{site.Progress:0.###}|{BuildInventorySignature(site.Inventory)}|{BuildInventorySignature(site.RequiredItems)}|{BuildInventorySignature(site.DeliveredItems)}|{string.Join(",", site.AssignedConstructorShipIds.OrderBy(id => id, StringComparer.Ordinal))}|{string.Join(",", site.MarketOrderIds.OrderBy(id => id, StringComparer.Ordinal))}";
private static string BuildMarketOrderSignature(MarketOrderRuntime order) =>
$"{order.FactionId}|{order.StationId}|{order.ConstructionSiteId}|{order.Kind}|{order.ItemId}|{order.Amount:0.###}|{order.RemainingAmount:0.###}|{order.Valuation:0.###}|{order.ReserveThreshold?.ToString("0.###") ?? "none"}|{order.PolicySetId}|{order.State}";
private static string BuildPolicySignature(PolicySetRuntime policy) =>
$"{policy.OwnerKind}|{policy.OwnerId}|{policy.TradeAccessPolicy}|{policy.DockingAccessPolicy}|{policy.ConstructionAccessPolicy}|{policy.OperationalRangePolicy}|{policy.CombatEngagementPolicy}|{policy.AvoidHostileSystems}|{policy.FleeHullRatio:0.###}|{string.Join(",", policy.BlacklistedSystemIds.OrderBy(id => id, StringComparer.Ordinal))}";
private static string BuildShipSignature(SimulationWorld world, ShipRuntime ship) =>
string.Join("|",
ship.SystemId,
ship.Position.X.ToString("0.###"),
ship.Position.Y.ToString("0.###"),
ship.Position.Z.ToString("0.###"),
ship.Velocity.X.ToString("0.###"),
ship.Velocity.Y.ToString("0.###"),
ship.Velocity.Z.ToString("0.###"),
ship.TargetPosition.X.ToString("0.###"),
ship.TargetPosition.Y.ToString("0.###"),
ship.TargetPosition.Z.ToString("0.###"),
ship.State.ToContractValue(),
string.Join(",", ship.OrderQueue
.OrderByDescending(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}")),
ship.DefaultBehavior.Kind,
ship.DefaultBehavior.TargetEntityId ?? "none",
ship.DefaultBehavior.TargetPosition?.X.ToString("0.###") ?? "none",
ship.DefaultBehavior.TargetPosition?.Y.ToString("0.###") ?? "none",
ship.DefaultBehavior.TargetPosition?.Z.ToString("0.###") ?? "none",
ship.DefaultBehavior.WaitSeconds.ToString("0.###"),
ship.DefaultBehavior.Radius.ToString("0.###"),
ship.DefaultBehavior.MaxSystemRange.ToString(CultureInfo.InvariantCulture),
ship.DefaultBehavior.KnownStationsOnly.ToString(),
string.Join(",", ship.DefaultBehavior.RepeatOrders.Select(order =>
$"{order.Kind}:{order.TargetEntityId}:{order.TargetSystemId}:{order.ItemId}:{order.WaitSeconds:0.###}:{order.Radius:0.###}:{order.MaxSystemRange?.ToString(CultureInfo.InvariantCulture) ?? "none"}:{order.KnownStationsOnly}")),
ship.DefaultBehavior.RepeatIndex.ToString(CultureInfo.InvariantCulture),
string.Join(",", ship.KnownStationIds.OrderBy(id => id, StringComparer.Ordinal)),
ship.ControlSourceKind,
ship.ControlSourceId ?? "none",
ship.ControlReason ?? "none",
ship.LastReplanReason ?? "none",
ship.LastAccessFailureReason ?? "none",
ship.CommanderId is not null && world.Commanders.FirstOrDefault(candidate => candidate.Id == ship.CommanderId)?.Assignment is { } assignment
? $"{assignment.ObjectiveId}:{assignment.Kind}:{assignment.BehaviorKind}:{assignment.Status}:{assignment.CampaignId}:{assignment.TheaterId}:{assignment.TargetSystemId}:{assignment.TargetEntityId}:{assignment.ItemId}:{assignment.Priority:0.###}:{assignment.UpdatedAtUtc.UtcTicks}"
: "no-assignment",
ship.ActivePlan?.Kind ?? "none",
ship.ActivePlan?.Status.ToContractValue() ?? "none",
ship.ActivePlan?.CurrentStepIndex.ToString(CultureInfo.InvariantCulture) ?? "-1",
string.Join(",",
ToActiveSubTaskSnapshots(ship).Select(subTask =>
$"{subTask.Id}:{subTask.Kind}:{subTask.Status}:{subTask.Progress:0.###}:{subTask.ElapsedSeconds:0.###}:{subTask.BlockingReason ?? "none"}")),
ship.SpatialState.CurrentCelestialId ?? "none",
ship.DockedStationId ?? "none",
ship.CommanderId ?? "none",
ship.PolicySetId ?? "none",
ship.SpatialState.SpaceLayer.ToContractValue(),
ship.SpatialState.CurrentCelestialId ?? "none",
ship.SpatialState.MovementRegime.ToContractValue(),
ship.SpatialState.DestinationNodeId ?? "none",
ship.SpatialState.Transit?.Regime.ToContractValue() ?? "none",
ship.SpatialState.Transit?.OriginNodeId ?? "none",
ship.SpatialState.Transit?.DestinationNodeId ?? "none",
ship.SpatialState.Transit?.Progress.ToString("0.###") ?? "0",
GetShipCargoAmount(ship).ToString("0.###"),
ship.Skills.Navigation.ToString(CultureInfo.InvariantCulture),
ship.Skills.Trade.ToString(CultureInfo.InvariantCulture),
ship.Skills.Mining.ToString(CultureInfo.InvariantCulture),
ship.Skills.Combat.ToString(CultureInfo.InvariantCulture),
ship.Skills.Construction.ToString(CultureInfo.InvariantCulture),
ship.Health.ToString("0.###"),
GetCurrentShipStep(ship)?.Id ?? "none");
private static string BuildInventorySignature(IReadOnlyDictionary<string, float> inventory) =>
string.Join(",",
inventory
.Where(entry => entry.Value > 0.001f)
.OrderBy(entry => entry.Key, StringComparer.Ordinal)
.Select(entry => $"{entry.Key}:{entry.Value:0.###}"));
private static string BuildFactionSignature(FactionRuntime faction, CommanderRuntime? commander)
{
var assignmentSig = commander?.Assignment is null
? string.Empty
: $"{commander.Assignment.ObjectiveId}:{commander.Assignment.Kind}:{commander.Assignment.BehaviorKind}:{commander.Assignment.Status}:{commander.Assignment.TargetSystemId}:{commander.Assignment.TargetEntityId}:{commander.Assignment.ItemId}";
var state = faction.StrategicState;
var strategicSig = string.Join(";",
state.Status,
state.PlanCycle.ToString(CultureInfo.InvariantCulture),
state.EconomicAssessment.PrimaryExpansionSiteId ?? "none",
state.EconomicAssessment.PrimaryExpansionSystemId ?? "none",
state.ThreatAssessment.PrimaryThreatFactionId ?? "none",
state.ThreatAssessment.PrimaryThreatSystemId ?? "none",
state.Theaters.Count.ToString(CultureInfo.InvariantCulture),
state.Campaigns.Count.ToString(CultureInfo.InvariantCulture),
state.Objectives.Count.ToString(CultureInfo.InvariantCulture),
state.Reservations.Count.ToString(CultureInfo.InvariantCulture),
state.ProductionPrograms.Count.ToString(CultureInfo.InvariantCulture),
state.EconomicAssessment.CommoditySignals.Count.ToString(CultureInfo.InvariantCulture),
state.ThreatAssessment.ThreatSignals.Count.ToString(CultureInfo.InvariantCulture));
var doctrineSig = $"{faction.Doctrine.StrategicPosture}:{faction.Doctrine.ExpansionPosture}:{faction.Doctrine.MilitaryPosture}:{faction.Doctrine.EconomicPosture}";
var decisionSig = string.Join(",", faction.DecisionLog.Select(entry => entry.Id));
var theaterSig = string.Join(";",
state.Theaters.OrderBy(theater => theater.Id, StringComparer.Ordinal)
.Select(theater => $"{theater.Id}:{theater.Kind}:{theater.SystemId}:{theater.Status}:{theater.Priority:0.###}:{theater.SupplyRisk:0.###}:{theater.TargetFactionId}:{theater.AnchorEntityId}:{theater.UpdatedAtUtc.UtcTicks}:{string.Join(",", theater.CampaignIds.OrderBy(id => id, StringComparer.Ordinal))}"));
var campaignSig = string.Join(";",
state.Campaigns.OrderBy(campaign => campaign.Id, StringComparer.Ordinal)
.Select(campaign => $"{campaign.Id}:{campaign.Kind}:{campaign.Status}:{campaign.Priority:0.###}:{campaign.TheaterId}:{campaign.TargetFactionId}:{campaign.TargetSystemId}:{campaign.TargetEntityId}:{campaign.CurrentStepIndex}:{campaign.PauseReason}:{campaign.ContinuationScore:0.###}:{campaign.SupplyAdequacy:0.###}:{campaign.ReplacementPressure:0.###}:{campaign.RequiresReinforcement}:{campaign.UpdatedAtUtc.UtcTicks}"));
var objectiveSig = string.Join(";",
state.Objectives.OrderBy(objective => objective.Id, StringComparer.Ordinal)
.Select(objective => $"{objective.Id}:{objective.CampaignId}:{objective.TheaterId}:{objective.Kind}:{objective.DelegationKind}:{objective.BehaviorKind}:{objective.Status}:{objective.Priority:0.###}:{objective.CommanderId}:{objective.TargetSystemId}:{objective.TargetEntityId}:{objective.ItemId}:{objective.CurrentStepIndex}:{objective.UseOrders}:{objective.StagingOrderKind}:{objective.ReinforcementLevel}:{objective.UpdatedAtUtc.UtcTicks}:{string.Join(",", objective.ReservedAssetIds.OrderBy(id => id, StringComparer.Ordinal))}"));
var reservationSig = string.Join(";",
state.Reservations.OrderBy(reservation => reservation.Id, StringComparer.Ordinal)
.Select(reservation => $"{reservation.Id}:{reservation.ObjectiveId}:{reservation.CampaignId}:{reservation.AssetKind}:{reservation.AssetId}:{reservation.Priority:0.###}:{reservation.UpdatedAtUtc.UtcTicks}"));
var productionSig = string.Join(";",
state.ProductionPrograms.OrderBy(program => program.Id, StringComparer.Ordinal)
.Select(program => $"{program.Id}:{program.Kind}:{program.Status}:{program.Priority:0.###}:{program.CampaignId}:{program.CommodityId}:{program.ModuleId}:{program.ShipKind}:{program.TargetSystemId}:{program.TargetCount}:{program.CurrentCount}:{program.Notes}"));
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(";",
state.Diplomacy.Relations.OrderBy(relation => relation.Id, StringComparer.Ordinal)
.Select(relation => $"{relation.Id}:{relation.Posture}:{relation.TensionScore:0.###}:{relation.GrievanceScore:0.###}:{relation.TradeAccessPolicy}:{relation.MilitaryAccessPolicy}:{relation.WarStateId}:{relation.UpdatedAtUtc.UtcTicks}"));
var territorySig = string.Join(";",
state.Territory.ControlStates.OrderBy(control => control.SystemId, StringComparer.Ordinal)
.Select(control => $"{control.SystemId}:{control.ControllerFactionId}:{control.PrimaryClaimantFactionId}:{control.ControlKind}:{control.IsContested}:{control.ControlScore:0.###}:{control.StrategicValue:0.###}:{control.UpdatedAtUtc.UtcTicks}"));
var economySig = string.Join(";",
state.EconomyRegions.Regions.OrderBy(region => region.Id, StringComparer.Ordinal)
.Select(region => $"{region.Id}:{region.FactionId}:{region.Kind}:{region.Status}:{region.CoreSystemId}:{string.Join(",", region.SystemIds.OrderBy(id => id, StringComparer.Ordinal))}:{region.UpdatedAtUtc.UtcTicks}"));
var tensionSig = string.Join(";",
state.Diplomacy.BorderTensions.OrderBy(tension => tension.Id, StringComparer.Ordinal)
.Select(tension => $"{tension.Id}:{tension.RelationId}:{tension.BorderEdgeId}:{tension.Status}:{tension.TensionScore:0.###}:{tension.IncidentScore:0.###}:{tension.MilitaryPressure:0.###}:{tension.AccessFriction:0.###}:{string.Join(",", tension.SystemIds.OrderBy(id => id, StringComparer.Ordinal))}:{tension.UpdatedAtUtc.UtcTicks}"));
var frontSig = string.Join(";",
state.Territory.FrontLines.OrderBy(front => front.Id, StringComparer.Ordinal)
.Select(front => $"{front.Id}:{front.Kind}:{front.Status}:{front.AnchorSystemId}:{front.PressureScore:0.###}:{front.SupplyRisk:0.###}:{string.Join(",", front.FactionIds.OrderBy(id => id, StringComparer.Ordinal))}:{string.Join(",", front.SystemIds.OrderBy(id => id, StringComparer.Ordinal))}:{front.UpdatedAtUtc.UtcTicks}"));
var corridorSig = string.Join(";",
state.EconomyRegions.Corridors.OrderBy(corridor => corridor.Id, StringComparer.Ordinal)
.Select(corridor => $"{corridor.Id}:{corridor.FactionId}:{corridor.Kind}:{corridor.Status}:{corridor.RiskScore:0.###}:{corridor.ThroughputScore:0.###}:{corridor.AccessState}:{string.Join(",", corridor.SystemPathIds.OrderBy(id => id, StringComparer.Ordinal))}:{corridor.UpdatedAtUtc.UtcTicks}"));
var bottleneckSig = string.Join(";",
state.EconomyRegions.Bottlenecks.OrderBy(bottleneck => bottleneck.Id, StringComparer.Ordinal)
.Select(bottleneck => $"{bottleneck.Id}:{bottleneck.RegionId}:{bottleneck.ItemId}:{bottleneck.Cause}:{bottleneck.Status}:{bottleneck.Severity:0.###}:{bottleneck.UpdatedAtUtc.UtcTicks}"));
var assessmentSig = string.Join(";",
state.EconomyRegions.SecurityAssessments.OrderBy(assessment => assessment.RegionId, StringComparer.Ordinal)
.Select(assessment => $"security:{assessment.RegionId}:{assessment.SupplyRisk:0.###}:{assessment.BorderPressure:0.###}:{assessment.ActiveWarCount}:{assessment.HostileRelationCount}:{assessment.AccessFriction:0.###}:{assessment.UpdatedAtUtc.UtcTicks}")
.Concat(state.EconomyRegions.EconomicAssessments.OrderBy(assessment => assessment.RegionId, StringComparer.Ordinal)
.Select(assessment => $"economic:{assessment.RegionId}:{assessment.SustainmentScore:0.###}:{assessment.ProductionDepth:0.###}:{assessment.ConstructionPressure:0.###}:{assessment.CorridorDependency:0.###}:{assessment.UpdatedAtUtc.UtcTicks}")));
return $"{state.Cycle}|{state.UpdatedAtUtc.UtcTicks}|{state.Routes.Count}|{state.Diplomacy.Relations.Count}|{state.Diplomacy.Incidents.Count}|{state.Diplomacy.Wars.Count}|{state.Territory.ControlStates.Count}|{state.Territory.BorderEdges.Count}|{state.Territory.FrontLines.Count}|{state.EconomyRegions.Regions.Count}|{state.EconomyRegions.Corridors.Count}|{diplomacySig}|{territorySig}|{economySig}|{tensionSig}|{frontSig}|{corridorSig}|{bottleneckSig}|{assessmentSig}";
}
private static ResourceNodeDelta ToNodeDelta(ResourceNodeRuntime node) => new(
node.Id,
node.SystemId,
ToDto(node.Position),
node.CelestialId,
node.SourceKind,
node.OreRemaining,
node.MaxOre,
node.ItemId);
private static CelestialDelta ToCelestialDelta(CelestialRuntime celestial) => new(
celestial.Id,
celestial.SystemId,
celestial.Kind.ToContractValue(),
ToDto(celestial.Position),
celestial.LocalSpaceRadius,
celestial.ParentNodeId,
celestial.OccupyingStructureId,
celestial.OrbitReferenceId);
private static StationDelta ToStationDelta(SimulationWorld world, StationRuntime station) => new(
station.Id,
station.Label,
station.Category,
station.Objective,
station.SystemId,
ToDto(station.Position),
station.CelestialId,
station.Color,
station.DockedShipIds.Count,
station.DockedShipIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
GetDockingPadCount(station),
ToStationActionProgressSnapshots(world, station),
ToInventoryEntries(station.Inventory),
station.FactionId,
station.CommanderId,
station.PolicySetId,
station.Population,
station.PopulationCapacity,
station.WorkforceRequired,
station.WorkforceEffectiveRatio,
ToStationStorageUsageSnapshots(world, station),
station.InstalledModules.OrderBy(moduleId => moduleId, StringComparer.Ordinal).ToList(),
station.MarketOrderIds.OrderBy(orderId => orderId, StringComparer.Ordinal).ToList());
private static IReadOnlyList<StationActionProgressSnapshot> ToStationActionProgressSnapshots(SimulationWorld world, StationRuntime station) =>
GetStationProductionLanes(world, station)
.Select(laneKey =>
{
var recipe = SelectProductionRecipe(world, station, laneKey);
var timer = GetStationProductionTimer(station, laneKey);
var duration = MathF.Max(recipe?.Duration ?? 0.1f, 0.1f);
var progress = Math.Clamp(timer / duration, 0f, 1f);
return recipe is null || timer <= 0.01f
? null
: new StationActionProgressSnapshot(
laneKey,
recipe.Label,
progress,
duration * (1f - progress),
duration,
recipe.Inputs.Select(i => new RecipeEntrySnapshot(i.ItemId, i.Amount)).ToList(),
recipe.Outputs.Select(o => new RecipeEntrySnapshot(o.ItemId, o.Amount)).ToList());
})
.Where(snapshot => snapshot is not null)
.Cast<StationActionProgressSnapshot>()
.ToList();
private static IReadOnlyList<StationStorageUsageSnapshot> ToStationStorageUsageSnapshots(SimulationWorld world, StationRuntime station)
{
string[] storageClasses = ["solid", "liquid", "container", "manufactured"];
return storageClasses
.Select(storageClass => new StationStorageUsageSnapshot(
storageClass,
station.Inventory
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoKind == storageClass)
.Sum(entry => entry.Value),
GetStationStorageCapacity(station, storageClass)))
.Where(snapshot => snapshot.Capacity > 0.01f)
.ToList();
}
private static ClaimDelta ToClaimDelta(ClaimRuntime claim) => new(
claim.Id,
claim.FactionId,
claim.SystemId,
claim.CelestialId,
claim.State,
claim.Health,
claim.PlacedAtUtc,
claim.ActivatesAtUtc);
private static ConstructionSiteDelta ToConstructionSiteDelta(SimulationWorld world, ConstructionSiteRuntime site) => new(
site.Id,
site.FactionId,
site.SystemId,
site.CelestialId,
site.TargetKind,
site.TargetDefinitionId,
site.BlueprintId,
site.ClaimId,
site.StationId,
site.State,
GetConstructionSiteProgress(world, site),
ToInventoryEntries(site.Inventory),
ToInventoryEntries(site.RequiredItems),
ToInventoryEntries(site.DeliveredItems),
site.AssignedConstructorShipIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
site.MarketOrderIds.OrderBy(id => id, StringComparer.Ordinal).ToList());
private static float GetConstructionSiteProgress(SimulationWorld world, ConstructionSiteRuntime site)
{
if (site.BlueprintId is not null
&& world.ModuleRecipes.TryGetValue(site.BlueprintId, out var recipe)
&& recipe.Duration > 0.01f)
{
return Math.Clamp(site.Progress / recipe.Duration, 0f, 1f);
}
return Math.Clamp(site.Progress, 0f, 1f);
}
private static MarketOrderDelta ToMarketOrderDelta(MarketOrderRuntime order) => new(
order.Id,
order.FactionId,
order.StationId,
order.ConstructionSiteId,
order.Kind,
order.ItemId,
order.Amount,
order.RemainingAmount,
order.Valuation,
order.ReserveThreshold,
order.PolicySetId,
order.State);
private static PolicySetDelta ToPolicySetDelta(PolicySetRuntime policy) => new(
policy.Id,
policy.OwnerKind,
policy.OwnerId,
policy.TradeAccessPolicy,
policy.DockingAccessPolicy,
policy.ConstructionAccessPolicy,
policy.OperationalRangePolicy,
policy.CombatEngagementPolicy,
policy.AvoidHostileSystems,
policy.FleeHullRatio,
policy.BlacklistedSystemIds.OrderBy(id => id, StringComparer.Ordinal).ToList());
private ShipDelta ToShipDelta(SimulationWorld world, ShipRuntime ship)
{
var commander = ship.CommanderId is null ? null
: world.Commanders.FirstOrDefault(c => c.Id == ship.CommanderId && c.Kind == CommanderKind.Ship);
return new ShipDelta(
ship.Id,
ship.Definition.Label,
ship.Definition.Kind,
ship.Definition.Class,
ship.SystemId,
ToDto(ship.Position),
ToDto(ship.Velocity),
ToDto(ship.TargetPosition),
ship.State.ToContractValue(),
ToShipOrderSnapshots(ship),
ToDefaultBehaviorSnapshot(ship.DefaultBehavior),
ToShipAssignmentSnapshot(commander),
new ShipSkillProfileSnapshot(ship.Skills.Navigation, ship.Skills.Trade, ship.Skills.Mining, ship.Skills.Combat, ship.Skills.Construction),
ToShipPlanSnapshot(ship.ActivePlan),
GetCurrentShipStep(ship)?.Id,
ToActiveSubTaskSnapshots(ship),
ship.ControlSourceKind,
ship.ControlSourceId,
ship.ControlReason,
ship.LastReplanReason,
ship.LastAccessFailureReason,
ship.SpatialState.CurrentCelestialId,
ship.DockedStationId,
ship.CommanderId,
ship.PolicySetId,
ship.Definition.CargoCapacity,
ToShipTravelSpeed(ship).Speed,
ToShipTravelSpeed(ship).Unit,
ToInventoryEntries(ship.Inventory),
ship.FactionId,
ship.Health,
ship.History.ToList(),
ToShipSpatialStateSnapshot(ship.SpatialState));
}
private static (float Speed, string Unit) ToShipTravelSpeed(ShipRuntime ship)
{
return ship.SpatialState.MovementRegime switch
{
MovementRegimeKind.FtlTransit => (ship.State == ShipState.Ftl ? ship.Definition.FtlSpeed : 0f, "ly/s"),
MovementRegimeKind.Warp => (ship.State == ShipState.Warping ? ship.Definition.WarpSpeed : 0f, "AU/s"),
_ => (MathF.Sqrt(MathF.Max(0f, ship.Velocity.LengthSquared())) * SimulationUnits.MetersPerKilometer, "m/s"),
};
}
private static IReadOnlyList<InventoryEntry> ToInventoryEntries(IReadOnlyDictionary<string, float> inventory) =>
inventory
.Where(entry => entry.Value > 0.001f)
.OrderBy(entry => entry.Key, StringComparer.Ordinal)
.Select(entry => new InventoryEntry(entry.Key, entry.Value))
.ToList();
private static IReadOnlyList<ShipOrderSnapshot> ToShipOrderSnapshots(ShipRuntime ship) =>
ship.OrderQueue
.OrderByDescending(order => order.Priority)
.ThenBy(order => order.CreatedAtUtc)
.Select(order => new ShipOrderSnapshot(
order.Id,
order.Kind,
order.Status.ToContractValue(),
order.Priority,
order.InterruptCurrentPlan,
order.CreatedAtUtc,
order.Label,
order.TargetEntityId,
order.TargetSystemId,
order.TargetPosition is null ? null : ToDto(order.TargetPosition.Value),
order.SourceStationId,
order.DestinationStationId,
order.ItemId,
order.NodeId,
order.ConstructionSiteId,
order.ModuleId,
order.WaitSeconds,
order.Radius,
order.MaxSystemRange,
order.KnownStationsOnly,
order.FailureReason))
.ToList();
private static DefaultBehaviorSnapshot ToDefaultBehaviorSnapshot(DefaultBehaviorRuntime behavior) =>
new(
behavior.Kind,
behavior.HomeSystemId,
behavior.HomeStationId,
behavior.AreaSystemId,
behavior.TargetEntityId,
behavior.PreferredItemId,
behavior.PreferredNodeId,
behavior.PreferredConstructionSiteId,
behavior.PreferredModuleId,
behavior.TargetPosition is null ? null : ToDto(behavior.TargetPosition.Value),
behavior.WaitSeconds,
behavior.Radius,
behavior.MaxSystemRange,
behavior.KnownStationsOnly,
behavior.PatrolPoints.Select(ToDto).ToList(),
behavior.PatrolIndex,
behavior.RepeatOrders.Select(ToShipOrderTemplateSnapshot).ToList(),
behavior.RepeatIndex);
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 ShipAssignmentSnapshot? ToShipAssignmentSnapshot(CommanderRuntime? commander)
{
if (commander?.Assignment is not { } assignment)
{
return null;
}
return new ShipAssignmentSnapshot(
commander.Id,
commander.ParentCommanderId,
assignment.Kind,
assignment.BehaviorKind,
assignment.Status,
assignment.ObjectiveId,
assignment.CampaignId,
assignment.TheaterId,
assignment.Priority,
assignment.HomeSystemId,
assignment.HomeStationId,
assignment.TargetSystemId,
assignment.TargetEntityId,
assignment.TargetPosition is null ? null : ToDto(assignment.TargetPosition.Value),
assignment.ItemId,
assignment.Notes,
assignment.UpdatedAtUtc);
}
private static ShipPlanSnapshot? ToShipPlanSnapshot(ShipPlanRuntime? plan)
{
if (plan is null)
{
return null;
}
return new ShipPlanSnapshot(
plan.Id,
plan.SourceKind.ToContractValue(),
plan.SourceId,
plan.Kind,
plan.Status.ToContractValue(),
plan.Summary,
plan.CurrentStepIndex,
plan.CreatedAtUtc,
plan.UpdatedAtUtc,
plan.InterruptReason,
plan.FailureReason,
plan.Steps.Select(ToShipPlanStepSnapshot).ToList());
}
private static ShipPlanStepSnapshot ToShipPlanStepSnapshot(ShipPlanStepRuntime step) =>
new(
step.Id,
step.Kind,
step.Status.ToContractValue(),
step.Summary,
step.BlockingReason,
step.CurrentSubTaskIndex,
step.SubTasks.Select(ToShipSubTaskSnapshot).ToList());
private static ShipSubTaskSnapshot ToShipSubTaskSnapshot(ShipSubTaskRuntime subTask) =>
new(
subTask.Id,
subTask.Kind,
subTask.Status.ToContractValue(),
subTask.Summary,
subTask.TargetEntityId,
subTask.TargetSystemId,
subTask.TargetNodeId,
subTask.TargetPosition is null ? null : ToDto(subTask.TargetPosition.Value),
subTask.ItemId,
subTask.ModuleId,
subTask.Threshold,
subTask.Amount,
subTask.Progress,
subTask.ElapsedSeconds,
subTask.TotalSeconds,
subTask.BlockingReason);
private static IReadOnlyList<ShipSubTaskSnapshot> ToActiveSubTaskSnapshots(ShipRuntime ship)
{
var step = GetCurrentShipStep(ship);
if (step is null)
{
return [];
}
return step.SubTasks
.Where(subTask => subTask.Status is WorkStatus.Pending or WorkStatus.Active or WorkStatus.Blocked)
.Select(ToShipSubTaskSnapshot)
.ToList();
}
private static ShipPlanStepRuntime? GetCurrentShipStep(ShipRuntime ship) =>
ship.ActivePlan is null || ship.ActivePlan.CurrentStepIndex >= ship.ActivePlan.Steps.Count
? null
: ship.ActivePlan.Steps[ship.ActivePlan.CurrentStepIndex];
private static CommanderAssignmentSnapshot ToCommanderAssignmentSnapshot(CommanderRuntime commander)
{
var assignment = commander.Assignment;
return new CommanderAssignmentSnapshot(
commander.Id,
assignment?.Kind ?? "unassigned",
assignment?.BehaviorKind ?? "none",
assignment?.Status ?? "idle",
assignment?.ObjectiveId,
assignment?.CampaignId,
assignment?.TheaterId,
commander.ParentCommanderId,
commander.ControlledEntityId,
assignment?.Priority ?? 0f,
assignment?.HomeSystemId,
assignment?.HomeStationId,
assignment?.TargetSystemId,
assignment?.TargetEntityId,
assignment?.TargetPosition is null ? null : ToDto(assignment.TargetPosition.Value),
assignment?.ItemId,
assignment?.Notes,
assignment?.UpdatedAtUtc,
commander.ActiveObjectiveIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
commander.SubordinateCommanderIds.OrderBy(id => id, StringComparer.Ordinal).ToList());
}
private static FactionDelta ToFactionDelta(SimulationWorld world, FactionRuntime faction, CommanderRuntime? commander)
{
var commanders = world.Commanders
.Where(candidate => candidate.FactionId == faction.Id)
.OrderBy(candidate => candidate.Kind, StringComparer.Ordinal)
.ThenBy(candidate => candidate.Id, StringComparer.Ordinal)
.Select(ToCommanderAssignmentSnapshot)
.ToList();
return new FactionDelta(
faction.Id,
faction.Label,
faction.Color,
faction.Credits,
faction.PopulationTotal,
faction.OreMined,
faction.GoodsProduced,
faction.ShipsBuilt,
faction.ShipsLost,
faction.DefaultPolicySetId,
ToFactionDoctrineSnapshot(faction.Doctrine),
ToFactionMemorySnapshot(faction.Memory),
ToFactionStrategicStateSnapshot(faction.StrategicState),
ToFactionDecisionLogSnapshots(faction.DecisionLog),
commanders);
}
private static FactionDoctrineSnapshot ToFactionDoctrineSnapshot(FactionDoctrineRuntime doctrine) => new(
doctrine.StrategicPosture,
doctrine.ExpansionPosture,
doctrine.MilitaryPosture,
doctrine.EconomicPosture,
doctrine.DesiredControlledSystems,
doctrine.DesiredMilitaryPerFront,
doctrine.DesiredMinersPerSystem,
doctrine.DesiredTransportsPerSystem,
doctrine.DesiredConstructors,
doctrine.ReserveCreditsRatio,
doctrine.ExpansionBudgetRatio,
doctrine.WarBudgetRatio,
doctrine.ReserveMilitaryRatio,
doctrine.OffensiveReadinessThreshold,
doctrine.SupplySecurityBias,
doctrine.FailureAversion,
doctrine.ReinforcementLeadPerFront);
private static FactionMemorySnapshot ToFactionMemorySnapshot(FactionMemoryRuntime memory) => new(
memory.LastPlanCycle,
memory.UpdatedAtUtc,
memory.LastObservedShipsBuilt,
memory.LastObservedShipsLost,
memory.LastObservedCredits,
memory.KnownSystemIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
memory.KnownEnemyFactionIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
memory.SystemMemories
.OrderBy(entry => entry.SystemId, StringComparer.Ordinal)
.Select(entry => new FactionSystemMemorySnapshot(
entry.SystemId,
entry.LastSeenAtUtc,
entry.LastEnemyShipCount,
entry.LastEnemyStationCount,
entry.ControlledByFaction,
entry.LastRole,
NormalizeFiniteFloat(entry.FrontierPressure),
NormalizeFiniteFloat(entry.RouteRisk),
NormalizeFiniteFloat(entry.HistoricalShortagePressure),
entry.OffensiveFailures,
entry.DefensiveFailures,
entry.OffensiveSuccesses,
entry.DefensiveSuccesses,
entry.LastContestedAtUtc,
entry.LastShortageAtUtc))
.ToList(),
memory.CommodityMemories
.OrderBy(entry => entry.ItemId, StringComparer.Ordinal)
.Select(entry => new FactionCommodityMemorySnapshot(
entry.ItemId,
NormalizeFiniteFloat(entry.HistoricalShortageScore),
NormalizeFiniteFloat(entry.HistoricalSurplusScore),
NormalizeFiniteFloat(entry.LastObservedBacklog),
entry.UpdatedAtUtc,
entry.LastCriticalAtUtc))
.ToList(),
memory.RecentOutcomes
.OrderBy(entry => entry.OccurredAtUtc)
.ThenBy(entry => entry.Id, StringComparer.Ordinal)
.Select(entry => new FactionOutcomeRecordSnapshot(
entry.Id,
entry.Kind,
entry.Summary,
entry.RelatedCampaignId,
entry.RelatedObjectiveId,
entry.OccurredAtUtc))
.ToList());
private static FactionStrategicStateSnapshot ToFactionStrategicStateSnapshot(FactionStrategicStateRuntime state) => new(
state.PlanCycle,
state.UpdatedAtUtc,
state.Status,
new FactionBudgetSnapshot(
state.Budget.ReservedCredits,
state.Budget.ExpansionCredits,
state.Budget.WarCredits,
state.Budget.ReservedMilitaryAssets,
state.Budget.ReservedLogisticsAssets,
state.Budget.ReservedConstructionAssets),
new FactionEconomicAssessmentSnapshot(
state.EconomicAssessment.PlanCycle,
state.EconomicAssessment.UpdatedAtUtc,
state.EconomicAssessment.MilitaryShipCount,
state.EconomicAssessment.MinerShipCount,
state.EconomicAssessment.TransportShipCount,
state.EconomicAssessment.ConstructorShipCount,
state.EconomicAssessment.ControlledSystemCount,
state.EconomicAssessment.TargetMilitaryShipCount,
state.EconomicAssessment.TargetMinerShipCount,
state.EconomicAssessment.TargetTransportShipCount,
state.EconomicAssessment.TargetConstructorShipCount,
state.EconomicAssessment.HasShipyard,
state.EconomicAssessment.HasWarIndustrySupplyChain,
state.EconomicAssessment.PrimaryExpansionSiteId,
state.EconomicAssessment.PrimaryExpansionSystemId,
NormalizeFiniteFloat(state.EconomicAssessment.ReplacementPressure),
NormalizeFiniteFloat(state.EconomicAssessment.SustainmentScore),
NormalizeFiniteFloat(state.EconomicAssessment.LogisticsSecurityScore),
state.EconomicAssessment.CriticalShortageCount,
state.EconomicAssessment.IndustrialBottleneckItemId,
state.EconomicAssessment.CommoditySignals.Select(signal => new FactionCommoditySignalSnapshot(
signal.ItemId,
NormalizeFiniteFloat(signal.AvailableStock),
NormalizeFiniteFloat(signal.OnHand),
NormalizeFiniteFloat(signal.ProductionRatePerSecond),
NormalizeFiniteFloat(signal.CommittedProductionRatePerSecond),
NormalizeFiniteFloat(signal.UsageRatePerSecond),
NormalizeFiniteFloat(signal.NetRatePerSecond),
NormalizeFiniteFloat(signal.ProjectedNetRatePerSecond),
NormalizeFiniteFloat(signal.LevelSeconds),
signal.Level,
NormalizeFiniteFloat(signal.ProjectedProductionRatePerSecond),
NormalizeFiniteFloat(signal.BuyBacklog),
NormalizeFiniteFloat(signal.ReservedForConstruction))).ToList()),
new FactionThreatAssessmentSnapshot(
state.ThreatAssessment.PlanCycle,
state.ThreatAssessment.UpdatedAtUtc,
state.ThreatAssessment.EnemyFactionCount,
state.ThreatAssessment.EnemyShipCount,
state.ThreatAssessment.EnemyStationCount,
state.ThreatAssessment.PrimaryThreatFactionId,
state.ThreatAssessment.PrimaryThreatSystemId,
state.ThreatAssessment.ThreatSignals.Select(signal => new FactionThreatSignalSnapshot(
signal.ScopeId,
signal.ScopeKind,
signal.EnemyShipCount,
signal.EnemyStationCount,
signal.EnemyFactionId)).ToList()),
state.Theaters.Select(theater => new FactionTheaterSnapshot(
theater.Id,
theater.Kind,
theater.SystemId,
theater.Status,
theater.Priority,
NormalizeFiniteFloat(theater.SupplyRisk),
NormalizeFiniteFloat(theater.FriendlyAssetValue),
theater.TargetFactionId,
theater.AnchorEntityId,
theater.AnchorPosition is null ? null : ToDto(theater.AnchorPosition.Value),
theater.UpdatedAtUtc,
theater.CampaignIds.OrderBy(id => id, StringComparer.Ordinal).ToList())).ToList(),
state.Campaigns.Select(campaign => new FactionCampaignSnapshot(
campaign.Id,
campaign.Kind,
campaign.Status,
campaign.Priority,
campaign.TheaterId,
campaign.TargetFactionId,
campaign.TargetSystemId,
campaign.TargetEntityId,
campaign.CommodityId,
campaign.SupportStationId,
campaign.CurrentStepIndex,
campaign.CreatedAtUtc,
campaign.UpdatedAtUtc,
campaign.Summary,
campaign.PauseReason,
NormalizeFiniteFloat(campaign.ContinuationScore),
NormalizeFiniteFloat(campaign.SupplyAdequacy),
NormalizeFiniteFloat(campaign.ReplacementPressure),
campaign.FailureCount,
campaign.SuccessCount,
campaign.FleetCommanderId,
campaign.RequiresReinforcement,
campaign.Steps.Select(ToFactionPlanStepSnapshot).ToList(),
campaign.ObjectiveIds.OrderBy(id => id, StringComparer.Ordinal).ToList())).ToList(),
state.Objectives.Select(objective => new FactionObjectiveSnapshot(
objective.Id,
objective.CampaignId,
objective.TheaterId,
objective.Kind,
objective.DelegationKind,
objective.BehaviorKind,
objective.Status,
objective.Priority,
objective.CommanderId,
objective.HomeSystemId,
objective.HomeStationId,
objective.TargetSystemId,
objective.TargetEntityId,
objective.TargetPosition is null ? null : ToDto(objective.TargetPosition.Value),
objective.ItemId,
objective.Notes,
objective.CurrentStepIndex,
objective.CreatedAtUtc,
objective.UpdatedAtUtc,
objective.UseOrders,
objective.StagingOrderKind,
objective.ReinforcementLevel,
objective.Steps.Select(ToFactionPlanStepSnapshot).ToList(),
objective.ReservedAssetIds.OrderBy(id => id, StringComparer.Ordinal).ToList())).ToList(),
state.Reservations.Select(reservation => new FactionReservationSnapshot(
reservation.Id,
reservation.ObjectiveId,
reservation.CampaignId,
reservation.AssetKind,
reservation.AssetId,
reservation.Priority,
reservation.CreatedAtUtc,
reservation.UpdatedAtUtc)).ToList(),
state.ProductionPrograms.Select(program => new FactionProductionProgramSnapshot(
program.Id,
program.Kind,
program.Status,
program.Priority,
program.CampaignId,
program.CommodityId,
program.ModuleId,
program.ShipKind,
program.TargetSystemId,
program.TargetCount,
program.CurrentCount,
program.Notes)).ToList());
private static FactionPlanStepSnapshot ToFactionPlanStepSnapshot(FactionPlanStepRuntime step) => new(
step.Id,
step.Kind,
step.Status,
step.Summary,
step.BlockingReason);
private static IReadOnlyList<FactionDecisionLogEntrySnapshot> ToFactionDecisionLogSnapshots(IReadOnlyCollection<FactionDecisionLogEntryRuntime> entries) =>
entries
.OrderBy(entry => entry.OccurredAtUtc)
.ThenBy(entry => entry.Id, StringComparer.Ordinal)
.Select(entry => new FactionDecisionLogEntrySnapshot(
entry.Id,
entry.Kind,
entry.Summary,
entry.RelatedEntityId,
entry.PlanCycle,
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)
{
return null;
}
return new GeopoliticalStateSnapshot(
state.Cycle,
state.UpdatedAtUtc,
state.Routes.Select(route => new SystemRouteLinkSnapshot(
route.Id,
route.SourceSystemId,
route.DestinationSystemId,
route.Distance,
route.IsPrimaryLane)).ToList(),
new DiplomaticStateSnapshot(
state.Diplomacy.Relations.Select(relation => new DiplomaticRelationSnapshot(
relation.Id,
relation.FactionAId,
relation.FactionBId,
relation.Status,
relation.Posture,
relation.TrustScore,
relation.TensionScore,
relation.GrievanceScore,
relation.TradeAccessPolicy,
relation.MilitaryAccessPolicy,
relation.WarStateId,
relation.CeasefireUntilUtc,
relation.UpdatedAtUtc,
relation.ActiveTreatyIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
relation.ActiveIncidentIds.OrderBy(id => id, StringComparer.Ordinal).ToList())).ToList(),
state.Diplomacy.Treaties.Select(treaty => new TreatySnapshot(
treaty.Id,
treaty.Kind,
treaty.Status,
treaty.TradeAccessPolicy,
treaty.MilitaryAccessPolicy,
treaty.Summary,
treaty.CreatedAtUtc,
treaty.UpdatedAtUtc,
treaty.FactionIds.OrderBy(id => id, StringComparer.Ordinal).ToList())).ToList(),
state.Diplomacy.Incidents.Select(incident => new DiplomaticIncidentSnapshot(
incident.Id,
incident.Kind,
incident.Status,
incident.SourceFactionId,
incident.TargetFactionId,
incident.SystemId,
incident.BorderEdgeId,
incident.Summary,
incident.Severity,
incident.EscalationScore,
incident.CreatedAtUtc,
incident.LastObservedAtUtc)).ToList(),
state.Diplomacy.BorderTensions.Select(tension => new BorderTensionSnapshot(
tension.Id,
tension.RelationId,
tension.BorderEdgeId,
tension.FactionAId,
tension.FactionBId,
tension.Status,
tension.TensionScore,
tension.IncidentScore,
tension.MilitaryPressure,
tension.AccessFriction,
tension.UpdatedAtUtc,
tension.SystemIds.OrderBy(id => id, StringComparer.Ordinal).ToList())).ToList(),
state.Diplomacy.Wars.Select(war => new WarStateSnapshot(
war.Id,
war.RelationId,
war.FactionAId,
war.FactionBId,
war.Status,
war.WarGoal,
war.EscalationScore,
war.StartedAtUtc,
war.CeasefireUntilUtc,
war.UpdatedAtUtc,
war.ActiveFrontLineIds.OrderBy(id => id, StringComparer.Ordinal).ToList())).ToList()),
new TerritoryStateSnapshot(
state.Territory.Claims.Select(claim => new TerritoryClaimSnapshot(
claim.Id,
claim.SourceClaimId,
claim.FactionId,
claim.SystemId,
claim.CelestialId,
claim.Status,
claim.ClaimKind,
claim.ClaimStrength,
claim.UpdatedAtUtc)).ToList(),
state.Territory.Influences.Select(influence => new TerritoryInfluenceSnapshot(
influence.Id,
influence.SystemId,
influence.FactionId,
influence.ClaimStrength,
influence.AssetStrength,
influence.LogisticsStrength,
influence.TotalInfluence,
influence.IsContesting,
influence.UpdatedAtUtc)).ToList(),
state.Territory.ControlStates.Select(control => new TerritoryControlStateSnapshot(
control.SystemId,
control.ControllerFactionId,
control.PrimaryClaimantFactionId,
control.ControlKind,
control.IsContested,
control.ControlScore,
control.StrategicValue,
control.ClaimantFactionIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
control.InfluencingFactionIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
control.UpdatedAtUtc)).ToList(),
state.Territory.StrategicProfiles.Select(profile => new SectorStrategicProfileSnapshot(
profile.SystemId,
profile.ControllerFactionId,
profile.ZoneKind,
profile.IsContested,
profile.StrategicValue,
profile.SecurityRating,
profile.TerritorialPressure,
profile.LogisticsValue,
profile.EconomicRegionId,
profile.FrontLineId,
profile.UpdatedAtUtc)).ToList(),
state.Territory.BorderEdges.Select(edge => new BorderEdgeSnapshot(
edge.Id,
edge.SourceSystemId,
edge.DestinationSystemId,
edge.SourceFactionId,
edge.DestinationFactionId,
edge.IsContested,
edge.RelationId,
edge.TensionScore,
edge.CorridorImportance,
edge.UpdatedAtUtc)).ToList(),
state.Territory.FrontLines.Select(front => new FrontLineSnapshot(
front.Id,
front.Kind,
front.Status,
front.AnchorSystemId,
front.PressureScore,
front.SupplyRisk,
front.UpdatedAtUtc,
front.FactionIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
front.SystemIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
front.BorderEdgeIds.OrderBy(id => id, StringComparer.Ordinal).ToList())).ToList(),
state.Territory.Zones.Select(zone => new TerritoryZoneSnapshot(
zone.Id,
zone.SystemId,
zone.FactionId,
zone.Kind,
zone.Status,
zone.Reason,
zone.UpdatedAtUtc)).ToList(),
state.Territory.Pressures.Select(pressure => new TerritoryPressureSnapshot(
pressure.Id,
pressure.SystemId,
pressure.FactionId,
pressure.Kind,
pressure.PressureScore,
pressure.SecurityScore,
pressure.HostileInfluence,
pressure.CorridorRisk,
pressure.UpdatedAtUtc)).ToList()),
new EconomyRegionStateSnapshot(
state.EconomyRegions.Regions.Select(region => new EconomicRegionSnapshot(
region.Id,
region.FactionId,
region.Label,
region.Kind,
region.Status,
region.CoreSystemId,
region.UpdatedAtUtc,
region.SystemIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
region.StationIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
region.FrontLineIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
region.CorridorIds.OrderBy(id => id, StringComparer.Ordinal).ToList())).ToList(),
state.EconomyRegions.SupplyNetworks.Select(network => new SupplyNetworkSnapshot(
network.Id,
network.RegionId,
network.ThroughputScore,
network.RiskScore,
network.UpdatedAtUtc,
network.StationIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
network.ProducerItemIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
network.ConsumerItemIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
network.ConstructionItemIds.OrderBy(id => id, StringComparer.Ordinal).ToList())).ToList(),
state.EconomyRegions.Corridors.Select(corridor => new LogisticsCorridorSnapshot(
corridor.Id,
corridor.FactionId,
corridor.Kind,
corridor.Status,
corridor.RiskScore,
corridor.ThroughputScore,
corridor.AccessState,
corridor.UpdatedAtUtc,
corridor.SystemPathIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
corridor.RegionIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
corridor.BorderEdgeIds.OrderBy(id => id, StringComparer.Ordinal).ToList())).ToList(),
state.EconomyRegions.ProductionProfiles.Select(profile => new RegionalProductionProfileSnapshot(
profile.RegionId,
profile.PrimaryIndustry,
profile.ShipyardCount,
profile.StationCount,
profile.UpdatedAtUtc,
profile.ProducedItemIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
profile.ScarceItemIds.OrderBy(id => id, StringComparer.Ordinal).ToList())).ToList(),
state.EconomyRegions.TradeBalances.Select(balance => new RegionalTradeBalanceSnapshot(
balance.RegionId,
balance.ImportsRequiredCount,
balance.ExportsSurplusCount,
balance.CriticalShortageCount,
balance.NetTradeScore,
balance.UpdatedAtUtc)).ToList(),
state.EconomyRegions.Bottlenecks.Select(bottleneck => new RegionalBottleneckSnapshot(
bottleneck.Id,
bottleneck.RegionId,
bottleneck.ItemId,
bottleneck.Cause,
bottleneck.Status,
bottleneck.Severity,
bottleneck.UpdatedAtUtc)).ToList(),
state.EconomyRegions.SecurityAssessments.Select(assessment => new RegionalSecurityAssessmentSnapshot(
assessment.RegionId,
assessment.SupplyRisk,
assessment.BorderPressure,
assessment.ActiveWarCount,
assessment.HostileRelationCount,
assessment.AccessFriction,
assessment.UpdatedAtUtc)).ToList(),
state.EconomyRegions.EconomicAssessments.Select(assessment => new RegionalEconomicAssessmentSnapshot(
assessment.RegionId,
assessment.SustainmentScore,
assessment.ProductionDepth,
assessment.ConstructionPressure,
assessment.CorridorDependency,
assessment.UpdatedAtUtc)).ToList()));
}
private static ShipSpatialStateSnapshot ToShipSpatialStateSnapshot(ShipSpatialStateRuntime state) => new(
state.SpaceLayer.ToContractValue(),
state.CurrentSystemId,
state.CurrentCelestialId,
state.LocalPosition is null ? null : ToDto(state.LocalPosition.Value),
state.SystemPosition is null ? null : ToDto(state.SystemPosition.Value),
state.MovementRegime.ToContractValue(),
state.DestinationNodeId,
state.Transit is null ? null : new ShipTransitSnapshot(
state.Transit.Regime.ToContractValue(),
state.Transit.OriginNodeId,
state.Transit.DestinationNodeId,
state.Transit.StartedAtUtc,
state.Transit.ArrivalDueAtUtc,
state.Transit.Progress));
private static Vector3Dto ToDto(Vector3 value) => new(value.X, value.Y, value.Z);
private static float NormalizeFiniteFloat(float value) =>
float.IsFinite(value) ? value : -1f;
}