1895 lines
86 KiB
C#
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)
|
|
{
|
|
StorageKind[] storageKinds = [StorageKind.Solid, StorageKind.Liquid, StorageKind.Container];
|
|
return storageKinds
|
|
.Select(storageKind => new StationStorageUsageSnapshot(
|
|
storageKind.ToDataValue(),
|
|
station.Inventory
|
|
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoStorageKind == storageKind)
|
|
.Sum(entry => entry.Value),
|
|
GetStationStorageCapacity(world, station, storageKind)))
|
|
.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;
|
|
}
|