using SpaceGame.Simulation.Api.Contracts; namespace SpaceGame.Simulation.Api.Simulation; public sealed partial class SimulationEngine { 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.StarKind, system.Definition.StarCount, system.Definition.StarColor, system.Definition.StarSize, system.Definition.Planets.Select(planet => new PlanetSnapshot( planet.Label, planet.PlanetType, planet.Shape, planet.MoonCount, planet.OrbitRadius, planet.OrbitSpeed, planet.OrbitEccentricity, planet.OrbitInclination, planet.OrbitLongitudeOfAscendingNode, planet.OrbitArgumentOfPeriapsis, planet.OrbitPhaseAtEpoch, planet.Size, planet.Color, planet.HasRing)).ToList())).ToList(), world.SpatialNodes.Select(ToSpatialNodeDelta).Select(node => new SpatialNodeSnapshot( node.Id, node.SystemId, node.Kind, node.LocalPosition, node.BubbleId, node.ParentNodeId, node.OccupyingStructureId, node.OrbitReferenceId)).ToList(), world.LocalBubbles.Select(ToLocalBubbleDelta).Select(bubble => new LocalBubbleSnapshot( bubble.Id, bubble.NodeId, bubble.SystemId, bubble.Radius, bubble.OccupantShipIds, bubble.OccupantStationIds, bubble.OccupantClaimIds, bubble.OccupantConstructionSiteIds)).ToList(), world.Nodes.Select(ToNodeDelta).Select(node => new ResourceNodeSnapshot( node.Id, node.SystemId, node.LocalPosition, node.AnchorNodeId, 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.SystemId, station.LocalPosition, station.NodeId, station.BubbleId, station.AnchorNodeId, station.Color, station.DockedShips, station.DockedShipIds, station.DockingPads, station.FuelStored, station.FuelCapacity, station.EnergyStored, station.EnergyCapacity, station.CurrentProcesses, station.Inventory, station.FactionId, station.CommanderId, station.PolicySetId, station.Population, station.PopulationCapacity, station.WorkforceRequired, station.WorkforceEffectiveRatio, station.InstalledModules, station.MarketOrderIds)).ToList(), world.Claims.Select(ToClaimDelta).Select(claim => new ClaimSnapshot( claim.Id, claim.FactionId, claim.SystemId, claim.NodeId, claim.BubbleId, claim.State, claim.Health, claim.PlacedAtUtc, claim.ActivatesAtUtc)).ToList(), world.ConstructionSites.Select(ToConstructionSiteDelta).Select(site => new ConstructionSiteSnapshot( site.Id, site.FactionId, site.SystemId, site.NodeId, site.BubbleId, 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.Role, ship.ShipClass, ship.SystemId, ship.LocalPosition, ship.LocalVelocity, ship.TargetLocalPosition, ship.State, ship.OrderKind, ship.DefaultBehaviorKind, ship.ControllerTaskKind, ship.NodeId, ship.BubbleId, ship.DockedStationId, ship.CommanderId, ship.PolicySetId, ship.CargoCapacity, ship.CargoItemId, ship.WorkerPopulation, ship.EnergyStored, ship.Inventory, ship.FactionId, ship.Health, ship.History, ship.CurrentAction, ship.SpatialState)).ToList(), world.Factions.Select(ToFactionDelta).Select(faction => new FactionSnapshot( faction.Id, faction.Label, faction.Color, faction.Credits, faction.PopulationTotal, faction.OreMined, faction.GoodsProduced, faction.ShipsBuilt, faction.ShipsLost, faction.DefaultPolicySetId)).ToList()); } public void PrimeDeltaBaseline(SimulationWorld world) { foreach (var node in world.Nodes) { node.LastDeltaSignature = BuildNodeSignature(node); } foreach (var node in world.SpatialNodes) { node.LastDeltaSignature = BuildSpatialNodeSignature(node); } foreach (var bubble in world.LocalBubbles) { bubble.LastDeltaSignature = BuildLocalBubbleSignature(bubble); } 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); } } private static IReadOnlyList BuildNodeDeltas(SimulationWorld world) { var deltas = new List(); 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 BuildSpatialNodeDeltas(SimulationWorld world) { var deltas = new List(); foreach (var node in world.SpatialNodes) { var signature = BuildSpatialNodeSignature(node); if (signature == node.LastDeltaSignature) { continue; } node.LastDeltaSignature = signature; deltas.Add(ToSpatialNodeDelta(node)); } return deltas; } private static IReadOnlyList BuildLocalBubbleDeltas(SimulationWorld world) { var deltas = new List(); foreach (var bubble in world.LocalBubbles) { var signature = BuildLocalBubbleSignature(bubble); if (signature == bubble.LastDeltaSignature) { continue; } bubble.LastDeltaSignature = signature; deltas.Add(ToLocalBubbleDelta(bubble)); } return deltas; } private static IReadOnlyList BuildStationDeltas(SimulationWorld world) { var deltas = new List(); 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 BuildClaimDeltas(SimulationWorld world) { var deltas = new List(); 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 BuildConstructionSiteDeltas(SimulationWorld world) { var deltas = new List(); foreach (var site in world.ConstructionSites) { var signature = BuildConstructionSiteSignature(site); if (signature == site.LastDeltaSignature) { continue; } site.LastDeltaSignature = signature; deltas.Add(ToConstructionSiteDelta(site)); } return deltas; } private static IReadOnlyList BuildMarketOrderDeltas(SimulationWorld world) { var deltas = new List(); 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 BuildPolicyDeltas(SimulationWorld world) { var deltas = new List(); 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 BuildShipDeltas(SimulationWorld world) { var deltas = new List(); 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 BuildFactionDeltas(SimulationWorld world) { var deltas = new List(); foreach (var faction in world.Factions) { var signature = BuildFactionSignature(faction); if (signature == faction.LastDeltaSignature) { continue; } faction.LastDeltaSignature = signature; deltas.Add(ToFactionDelta(faction)); } return deltas; } private static string BuildNodeSignature(ResourceNodeRuntime node) => $"{node.SystemId}|{node.Position.X:0.###}|{node.Position.Y:0.###}|{node.Position.Z:0.###}|{node.AnchorNodeId}|{node.OreRemaining:0.###}"; private static string BuildSpatialNodeSignature(NodeRuntime node) => $"{node.SystemId}|{node.Kind.ToContractValue()}|{node.Position.X:0.###}|{node.Position.Y:0.###}|{node.Position.Z:0.###}|{node.BubbleId}|{node.ParentNodeId}|{node.OccupyingStructureId}|{node.OrbitReferenceId}"; private static string BuildLocalBubbleSignature(LocalBubbleRuntime bubble) => $"{bubble.SystemId}|{bubble.NodeId}|{bubble.Radius:0.###}|{string.Join(",", bubble.OccupantShipIds.OrderBy(id => id, StringComparer.Ordinal))}|{string.Join(",", bubble.OccupantStationIds.OrderBy(id => id, StringComparer.Ordinal))}|{string.Join(",", bubble.OccupantClaimIds.OrderBy(id => id, StringComparer.Ordinal))}|{string.Join(",", bubble.OccupantConstructionSiteIds.OrderBy(id => id, StringComparer.Ordinal))}"; private static string BuildStationSignature(SimulationWorld world, StationRuntime station) { var processes = ToStationActionProgressSnapshots(world, station); return string.Join("|", station.SystemId, station.NodeId ?? "none", station.BubbleId ?? "none", station.AnchorNodeId ?? "none", station.CommanderId ?? "none", station.PolicySetId ?? "none", BuildInventorySignature(station.Inventory), GetInventoryAmount(station.Inventory, "fuel").ToString("0.###"), GetStationFuelCapacity(station).ToString("0.###"), station.EnergyStored.ToString("0.###"), GetStationEnergyCapacity(station).ToString("0.###"), 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.NodeId}|{claim.BubbleId}|{claim.State}|{claim.Health:0.###}|{claim.ActivatesAtUtc:O}"; private static string BuildConstructionSiteSignature(ConstructionSiteRuntime site) => $"{site.FactionId}|{site.SystemId}|{site.NodeId}|{site.BubbleId}|{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.ControllerTask.Kind.ToContractValue(), ship.SpatialState.CurrentNodeId ?? "none", ship.SpatialState.CurrentBubbleId ?? "none", ship.DockedStationId ?? "none", ship.CommanderId ?? "none", ship.PolicySetId ?? "none", ship.WorkerPopulation.ToString("0.###"), ship.SpatialState.SpaceLayer, ship.SpatialState.CurrentNodeId ?? "none", ship.SpatialState.CurrentBubbleId ?? "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.###"), GetInventoryAmount(ship.Inventory, "fuel").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 ? GetRemainingConstructionDelivery(world, site).ToString("0.###") : "0", ship.EnergyStored.ToString("0.###"), ship.Health.ToString("0.###"), ship.ActionTimer.ToString("0.###")); private static string BuildInventorySignature(IReadOnlyDictionary 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) => $"{faction.Credits:0.###}|{faction.PopulationTotal:0.###}|{faction.OreMined:0.###}|{faction.GoodsProduced:0.###}|{faction.ShipsBuilt}|{faction.ShipsLost}|{faction.DefaultPolicySetId}"; private static ResourceNodeDelta ToNodeDelta(ResourceNodeRuntime node) => new( node.Id, node.SystemId, ToDto(node.Position), node.AnchorNodeId, node.SourceKind, node.OreRemaining, node.MaxOre, node.ItemId); private static SpatialNodeDelta ToSpatialNodeDelta(NodeRuntime node) => new( node.Id, node.SystemId, node.Kind.ToContractValue(), ToDto(node.Position), node.BubbleId, node.ParentNodeId, node.OccupyingStructureId, node.OrbitReferenceId); private static LocalBubbleDelta ToLocalBubbleDelta(LocalBubbleRuntime bubble) => new( bubble.Id, bubble.NodeId, bubble.SystemId, bubble.Radius, bubble.OccupantShipIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), bubble.OccupantStationIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), bubble.OccupantClaimIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), bubble.OccupantConstructionSiteIds.OrderBy(id => id, StringComparer.Ordinal).ToList()); private static StationDelta ToStationDelta(SimulationWorld world, StationRuntime station) => new( station.Id, station.Definition.Label, station.Definition.Category, station.SystemId, ToDto(station.Position), station.NodeId, station.BubbleId, station.AnchorNodeId, station.Definition.Color, station.DockedShipIds.Count, station.DockedShipIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), GetDockingPadCount(station), GetInventoryAmount(station.Inventory, "fuel"), GetStationFuelCapacity(station), station.EnergyStored, GetStationEnergyCapacity(station), ToStationActionProgressSnapshots(world, station), ToInventoryEntries(station.Inventory), station.FactionId, station.CommanderId, station.PolicySetId, station.Population, station.PopulationCapacity, station.WorkforceRequired, station.WorkforceEffectiveRatio, station.InstalledModules.OrderBy(moduleId => moduleId, StringComparer.Ordinal).ToList(), station.MarketOrderIds.OrderBy(orderId => orderId, StringComparer.Ordinal).ToList()); private static IReadOnlyList ToStationActionProgressSnapshots(SimulationWorld world, StationRuntime station) => GetStationProductionLanes(station) .Select(laneKey => { var recipe = SelectProductionRecipe(world, station, laneKey); var timer = GetStationProductionTimer(station, laneKey); return recipe is null || station.EnergyStored <= 0.01f || timer <= 0.01f ? null : new StationActionProgressSnapshot( laneKey, recipe.Label, Math.Clamp(timer / MathF.Max(recipe.Duration, 0.1f), 0f, 1f)); }) .Where(snapshot => snapshot is not null) .Cast() .ToList(); private static ClaimDelta ToClaimDelta(ClaimRuntime claim) => new( claim.Id, claim.FactionId, claim.SystemId, claim.NodeId, claim.BubbleId, claim.State, claim.Health, claim.PlacedAtUtc, claim.ActivatesAtUtc); private static ConstructionSiteDelta ToConstructionSiteDelta(ConstructionSiteRuntime site) => new( site.Id, site.FactionId, site.SystemId, site.NodeId, site.BubbleId, site.TargetKind, site.TargetDefinitionId, site.BlueprintId, site.ClaimId, site.StationId, site.State, site.Progress, 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 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) => new( ship.Id, ship.Definition.Label, ship.Definition.Role, ship.Definition.ShipClass, ship.SystemId, ToDto(ship.Position), ToDto(ship.Velocity), ToDto(ship.TargetPosition), ship.State.ToContractValue(), ship.Order?.Kind, ship.DefaultBehavior.Kind, ship.ControllerTask.Kind.ToContractValue(), ship.SpatialState.CurrentNodeId, ship.SpatialState.CurrentBubbleId, ship.DockedStationId, ship.CommanderId, ship.PolicySetId, ship.Definition.CargoCapacity, ship.Definition.CargoItemId, ship.WorkerPopulation, ship.EnergyStored, 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.Refueling => CreateShipRemainingActionProgress( "Refuel", ship.TrackedActionTotal, MathF.Max(0f, GetShipRefuelTarget(ship, world) - GetInventoryAmount(ship.Inventory, "fuel"))), ShipState.Loading => CreateShipRemainingActionProgress( "Load workers", ship.TrackedActionTotal, MathF.Max(0f, ship.TrackedActionTotal - ship.WorkerPopulation)), ShipState.Unloading => CreateShipRemainingActionProgress( "Unload workers", ship.TrackedActionTotal, ship.WorkerPopulation), 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, GetRemainingConstructionDelivery(world, site)), _ => null, }; return progress; } 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 ToInventoryEntries(IReadOnlyDictionary 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) => new( faction.Id, faction.Label, faction.Color, faction.Credits, faction.PopulationTotal, faction.OreMined, faction.GoodsProduced, faction.ShipsBuilt, faction.ShipsLost, faction.DefaultPolicySetId); private static ShipSpatialStateSnapshot ToShipSpatialStateSnapshot(ShipSpatialStateRuntime state) => new( state.SpaceLayer, state.CurrentSystemId, state.CurrentNodeId, state.CurrentBubbleId, 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 void EmitShipStateEvents( ShipRuntime ship, ShipState previousState, string previousBehavior, ControllerTaskKind previousTask, string controllerEvent, ICollection events) { var occurredAtUtc = DateTimeOffset.UtcNow; if (previousState != ship.State) { events.Add(new SimulationEventRecord("ship", ship.Id, "state-changed", $"{ship.Definition.Label} {previousState.ToContractValue()} -> {ship.State.ToContractValue()}", occurredAtUtc)); } if (previousBehavior != ship.DefaultBehavior.Kind) { events.Add(new SimulationEventRecord("ship", ship.Id, "behavior-changed", $"{ship.Definition.Label} behavior {previousBehavior} -> {ship.DefaultBehavior.Kind}", occurredAtUtc)); } if (previousTask != ship.ControllerTask.Kind) { events.Add(new SimulationEventRecord("ship", ship.Id, "task-changed", $"{ship.Definition.Label} task {previousTask.ToContractValue()} -> {ship.ControllerTask.Kind.ToContractValue()}", occurredAtUtc)); } if (controllerEvent != "none") { events.Add(new SimulationEventRecord("ship", ship.Id, controllerEvent, $"{ship.Definition.Label} {controllerEvent}", occurredAtUtc)); } } private static Vector3Dto ToDto(Vector3 value) => new(value.X, value.Y, value.Z); }