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.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.SourceKind, node.OreRemaining, node.MaxOre, node.ItemId)).ToList(), world.Stations.Select(ToStationDelta).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.DockingPads, station.EnergyStored, 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(ToShipDelta).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.WorkerPopulation, ship.EnergyStored, ship.Inventory, ship.FactionId, ship.Health, ship.History, 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(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(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(station); if (signature == station.LastDeltaSignature) { continue; } station.LastDeltaSignature = signature; deltas.Add(ToStationDelta(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 static IReadOnlyList BuildShipDeltas(SimulationWorld world) { var deltas = new List(); foreach (var ship in world.Ships) { var signature = BuildShipSignature(ship); if (signature == ship.LastDeltaSignature) { continue; } ship.LastDeltaSignature = signature; deltas.Add(ToShipDelta(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.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(StationRuntime station) => $"{station.SystemId}|{station.NodeId}|{station.BubbleId}|{station.AnchorNodeId}|{station.CommanderId}|{station.PolicySetId}|{BuildInventorySignature(station.Inventory)}|{station.EnergyStored:0.###}|{station.DockedShipIds.Count}|{station.DockingPadAssignments.Count}|{station.Population:0.###}|{station.PopulationCapacity:0.###}|{station.WorkforceRequired:0.###}|{station.WorkforceEffectiveRatio: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"}"; 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(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, ship.Order?.Kind ?? "none", ship.DefaultBehavior.Kind, ship.ControllerTask.Kind, 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.EnergyStored.ToString("0.###"), ship.Health.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.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(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, GetDockingPadCount(station), station.EnergyStored, 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 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 static ShipDelta ToShipDelta(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, ship.Order?.Kind, ship.DefaultBehavior.Kind, ship.ControllerTask.Kind, ship.SpatialState.CurrentNodeId, ship.SpatialState.CurrentBubbleId, ship.DockedStationId, ship.CommanderId, ship.PolicySetId, ship.Definition.CargoCapacity, ship.WorkerPopulation, ship.EnergyStored, ToInventoryEntries(ship.Inventory), ship.FactionId, ship.Health, ship.History.ToList(), ToShipSpatialStateSnapshot(ship.SpatialState)); 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, string previousState, string previousBehavior, string 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} -> {ship.State}", 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} -> {ship.ControllerTask.Kind}", 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); }