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

974 lines
37 KiB
C#

using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService;
using static SpaceGame.Api.Stations.Simulation.StationSimulationService;
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
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));
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)).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.OrderKind,
ship.DefaultBehaviorKind,
ship.BehaviorPhase,
ship.ControllerTaskKind,
ship.CommanderObjective,
ship.CelestialId,
ship.DockedStationId,
ship.CommanderId,
ship.PolicySetId,
ship.CargoCapacity,
ship.TravelSpeed,
ship.TravelSpeedUnit,
ship.Inventory,
ship.FactionId,
ship.Health,
ship.History,
ship.CurrentAction,
ship.SpatialState)).ToList(),
world.Factions.Select(faction => ToFactionDelta(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.StrategicAssessment,
faction.StrategicPriorities,
faction.Blackboard,
faction.Objectives,
faction.IssuedTasks)).ToList());
}
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));
}
}
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(faction, commander));
}
return deltas;
}
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}";
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(),
ship.Order?.Kind ?? "none",
ship.DefaultBehavior.Kind,
ship.DefaultBehavior.Phase ?? "none",
ship.ControllerTask.Kind.ToContractValue(),
ship.SpatialState.CurrentCelestialId ?? "none",
ship.DockedStationId ?? "none",
ship.CommanderId ?? "none",
ship.PolicySetId ?? "none",
ship.SpatialState.SpaceLayer,
ship.SpatialState.CurrentCelestialId ?? "none",
ship.SpatialState.MovementRegime,
ship.SpatialState.DestinationNodeId ?? "none",
ship.SpatialState.Transit?.Regime ?? "none",
ship.SpatialState.Transit?.OriginNodeId ?? "none",
ship.SpatialState.Transit?.DestinationNodeId ?? "none",
ship.SpatialState.Transit?.Progress.ToString("0.###") ?? "0",
GetShipCargoAmount(ship).ToString("0.###"),
ship.TrackedActionKey ?? "none",
ship.TrackedActionTotal.ToString("0.###"),
ship.ControllerTask.TargetEntityId is not null && world.ConstructionSites.FirstOrDefault(site => site.Id == ship.ControllerTask.TargetEntityId) is { } site
? ShipTaskExecutionService.GetRemainingConstructionDelivery(world, site).ToString("0.###")
: "0",
ship.Health.ToString("0.###"),
ship.ActionTimer.ToString("0.###"));
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 prioritySig = commander?.LastStrategicPriorities is { } prios
? string.Join(",", prios.Select(p => $"{p.Name}:{p.Priority:0.##}"))
: string.Empty;
var objectiveSig = commander?.Objectives is { Count: > 0 } objectives
? string.Join(",", objectives.Select(objective =>
$"{objective.Kind}:{objective.State}:{objective.Priority:0.##}:{objective.BlockingReason}:{objective.InvalidationReason}"))
: string.Empty;
var taskSig = commander?.IssuedTasks is { Count: > 0 } tasks
? string.Join(",", tasks.Select(task =>
$"{task.Kind}:{task.State}:{task.Priority:0.##}:{task.ShipRole}:{task.CommodityId}:{task.TargetFactionId}:{task.TargetSiteId}"))
: string.Empty;
return $"{faction.Credits:0.###}|{faction.PopulationTotal:0.###}|{faction.OreMined:0.###}|{faction.GoodsProduced:0.###}|{faction.ShipsBuilt}|{faction.ShipsLost}|{faction.DefaultPolicySetId}|{prioritySig}|{objectiveSig}|{taskSig}";
}
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);
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(),
ship.Order?.Kind,
ship.DefaultBehavior.Kind,
ship.DefaultBehavior.Phase,
ship.ControllerTask.Kind.ToContractValue(),
commander?.ActiveActionName,
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(),
ToShipActionProgressSnapshot(world, ship),
ToShipSpatialStateSnapshot(ship.SpatialState));
}
private static ShipActionProgressSnapshot? ToShipActionProgressSnapshot(SimulationWorld world, ShipRuntime ship)
{
var progress = ship.State switch
{
ShipState.SpoolingFtl => CreateShipActionProgress("FTL spool", ship.ActionTimer, MathF.Max(ship.Definition.SpoolTime, 0.1f)),
ShipState.Ftl => ship.SpatialState.Transit is null ? null : new ShipActionProgressSnapshot("FTL", Math.Clamp(ship.SpatialState.Transit.Progress, 0f, 1f)),
ShipState.SpoolingWarp => CreateShipActionProgress("Warp spool", ship.ActionTimer, MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f)),
ShipState.Warping => ship.SpatialState.Transit is null ? null : new ShipActionProgressSnapshot("Warp", Math.Clamp(ship.SpatialState.Transit.Progress, 0f, 1f)),
ShipState.Mining => CreateShipActionProgress("Mining", ship.ActionTimer, MathF.Max(world.Balance.MiningCycleSeconds, 0.1f)),
ShipState.Docking => CreateShipActionProgress("Docking", ship.ActionTimer, MathF.Max(world.Balance.DockingDuration, 0.1f)),
ShipState.Undocking => CreateShipActionProgress("Undocking", ship.ActionTimer, MathF.Max(world.Balance.UndockingDuration, 0.1f)),
ShipState.Transferring => CreateShipRemainingActionProgress("Transfer", ship.TrackedActionTotal, GetShipCargoAmount(ship)),
ShipState.Loading or ShipState.Unloading => null,
ShipState.DeliveringConstruction => ship.ControllerTask.TargetEntityId is null
? null
: world.ConstructionSites.FirstOrDefault(site => site.Id == ship.ControllerTask.TargetEntityId) is not { } site
? null
: CreateShipRemainingActionProgress("Deliver materials", ship.TrackedActionTotal, ShipTaskExecutionService.GetRemainingConstructionDelivery(world, site)),
_ => null,
};
return progress;
}
private static (float Speed, string Unit) ToShipTravelSpeed(ShipRuntime ship)
{
return ship.SpatialState.MovementRegime switch
{
MovementRegimeKinds.FtlTransit => (ship.State == ShipState.Ftl ? ship.Definition.FtlSpeed : 0f, "ly/s"),
MovementRegimeKinds.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 ShipActionProgressSnapshot CreateShipActionProgress(string label, float elapsedSeconds, float requiredSeconds) =>
new(label, Math.Clamp(elapsedSeconds / requiredSeconds, 0f, 1f));
private static ShipActionProgressSnapshot? CreateShipRemainingActionProgress(string label, float totalAmount, float remainingAmount)
{
if (totalAmount <= 0.01f)
{
return null;
}
var progress = 1f - Math.Clamp(remainingAmount / totalAmount, 0f, 1f);
return new ShipActionProgressSnapshot(label, progress);
}
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 FactionDelta ToFactionDelta(FactionRuntime faction, CommanderRuntime? commander)
{
FactionPlanningStateSnapshot? strategicAssessment = null;
IReadOnlyList<FactionStrategicPrioritySnapshot>? strategicPriorities = null;
FactionBlackboardSnapshot? blackboard = null;
IReadOnlyList<FactionObjectiveSnapshot>? objectives = null;
IReadOnlyList<FactionIssuedTaskSnapshot>? issuedTasks = null;
if (commander?.LastStrategicAssessment is { } ps)
{
strategicAssessment = new FactionPlanningStateSnapshot(
ps.MilitaryShipCount,
ps.MinerShipCount,
ps.TransportShipCount,
ps.ConstructorShipCount,
ps.ControlledSystemCount,
ps.TargetSystemCount,
ps.HasShipFactory,
NormalizeFiniteFloat(ps.OreStockpile),
NormalizeFiniteFloat(ps.RefinedMetalsAvailableStock),
NormalizeFiniteFloat(ps.RefinedMetalsUsageRate),
NormalizeFiniteFloat(ps.RefinedMetalsProjectedProductionRate),
NormalizeFiniteFloat(ps.RefinedMetalsProjectedNetRate),
NormalizeFiniteFloat(ps.RefinedMetalsLevelSeconds),
ps.RefinedMetalsLevel,
NormalizeFiniteFloat(ps.HullpartsAvailableStock),
NormalizeFiniteFloat(ps.HullpartsUsageRate),
NormalizeFiniteFloat(ps.HullpartsProjectedProductionRate),
NormalizeFiniteFloat(ps.HullpartsProjectedNetRate),
NormalizeFiniteFloat(ps.HullpartsLevelSeconds),
ps.HullpartsLevel,
NormalizeFiniteFloat(ps.ClaytronicsAvailableStock),
NormalizeFiniteFloat(ps.ClaytronicsUsageRate),
NormalizeFiniteFloat(ps.ClaytronicsProjectedProductionRate),
NormalizeFiniteFloat(ps.ClaytronicsProjectedNetRate),
NormalizeFiniteFloat(ps.ClaytronicsLevelSeconds),
ps.ClaytronicsLevel,
NormalizeFiniteFloat(ps.WaterAvailableStock),
NormalizeFiniteFloat(ps.WaterUsageRate),
NormalizeFiniteFloat(ps.WaterProjectedProductionRate),
NormalizeFiniteFloat(ps.WaterProjectedNetRate),
NormalizeFiniteFloat(ps.WaterLevelSeconds),
ps.WaterLevel);
}
if (commander?.LastStrategicPriorities is { } prios)
{
strategicPriorities = prios.Select(p => new FactionStrategicPrioritySnapshot(p.Name, p.Priority)).ToList();
}
if (commander?.FactionBlackboard is { } bb)
{
blackboard = new FactionBlackboardSnapshot(
bb.PlanCycle,
bb.UpdatedAtUtc,
bb.TargetWarshipCount,
bb.HasWarIndustrySupplyChain,
bb.HasShipyard,
bb.HasActiveExpansionProject,
bb.ActiveExpansionCommodityId,
bb.ActiveExpansionModuleId,
bb.ActiveExpansionSiteId,
bb.ActiveExpansionSystemId,
bb.EnemyFactionCount,
bb.EnemyShipCount,
bb.EnemyStationCount,
bb.MilitaryShipCount,
bb.MinerShipCount,
bb.TransportShipCount,
bb.ConstructorShipCount,
bb.ControlledSystemCount,
bb.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(),
bb.ThreatSignals.Select(signal => new FactionThreatSignalSnapshot(
signal.ScopeId,
signal.ScopeKind,
signal.EnemyShipCount,
signal.EnemyStationCount)).ToList());
}
if (commander?.Objectives is { Count: > 0 } runtimeObjectives)
{
objectives = runtimeObjectives
.OrderByDescending(objective => objective.Priority)
.Select(objective => new FactionObjectiveSnapshot(
objective.Id,
objective.Kind.ToString(),
objective.State.ToString(),
objective.Priority,
objective.ParentObjectiveId,
objective.TargetFactionId,
objective.TargetSystemId,
objective.TargetSiteId,
objective.TargetRegionId,
objective.CommodityId,
objective.ModuleId,
objective.BudgetWeight,
objective.SlotCost,
objective.CreatedAtCycle,
objective.UpdatedAtCycle,
objective.InvalidationReason,
objective.BlockingReason,
objective.PrerequisiteObjectiveIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
objective.AssignedAssetIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
objective.Steps
.OrderByDescending(step => step.Priority)
.Select(step => new FactionPlanStepSnapshot(
step.Id,
step.Kind.ToString(),
step.Status.ToString(),
step.Priority,
step.CommodityId,
step.ModuleId,
step.TargetFactionId,
step.TargetSiteId,
step.BlockingReason,
step.Notes,
step.LastEvaluatedCycle,
step.DependencyStepIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
step.RequiredFacts.OrderBy(fact => fact, StringComparer.Ordinal).ToList(),
step.ProducedFacts.OrderBy(fact => fact, StringComparer.Ordinal).ToList(),
step.AssignedAssetIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
step.IssuedTaskIds.OrderBy(id => id, StringComparer.Ordinal).ToList()))
.ToList()))
.ToList();
}
if (commander?.IssuedTasks is { Count: > 0 } runtimeTasks)
{
issuedTasks = runtimeTasks
.OrderByDescending(task => task.Priority)
.Select(task => new FactionIssuedTaskSnapshot(
task.Id,
task.Kind.ToString(),
task.State.ToString(),
task.ObjectiveId,
task.StepId,
task.Priority,
task.ShipRole,
task.CommodityId,
task.ModuleId,
task.TargetFactionId,
task.TargetSystemId,
task.TargetSiteId,
task.CreatedAtCycle,
task.UpdatedAtCycle,
task.BlockingReason,
task.Notes,
task.AssignedAssetIds.OrderBy(id => id, StringComparer.Ordinal).ToList()))
.ToList();
}
return new FactionDelta(
faction.Id,
faction.Label,
faction.Color,
faction.Credits,
faction.PopulationTotal,
faction.OreMined,
faction.GoodsProduced,
faction.ShipsBuilt,
faction.ShipsLost,
faction.DefaultPolicySetId,
strategicAssessment,
strategicPriorities,
blackboard,
objectives,
issuedTasks);
}
private static ShipSpatialStateSnapshot ToShipSpatialStateSnapshot(ShipSpatialStateRuntime state) => new(
state.SpaceLayer,
state.CurrentSystemId,
state.CurrentCelestialId,
state.LocalPosition is null ? null : ToDto(state.LocalPosition.Value),
state.SystemPosition is null ? null : ToDto(state.SystemPosition.Value),
state.MovementRegime,
state.DestinationNodeId,
state.Transit is null ? null : new ShipTransitSnapshot(
state.Transit.Regime,
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;
}