974 lines
37 KiB
C#
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;
|
|
}
|