namespace SpaceGame.Api.Simulation.Core; internal sealed class SimulationEngine { private readonly IBalanceService _balance; private readonly IPlayerStateStore _playerStateStore; private readonly OrbitalSimulationOptions _orbitalSimulation; private readonly OrbitalStateUpdater _orbitalStateUpdater; private readonly InfrastructureSimulationService _infrastructureSimulation; private readonly GeopoliticalSimulationService _geopolitics; private readonly CommanderPlanningService _commanderPlanning; private readonly PlayerFactionService _playerFaction; private readonly StationSimulationService _stationSimulation; private readonly StationLifecycleService _stationLifecycle; private readonly ShipAiService _shipAi; private readonly SimulationProjectionService _projection; internal SimulationEngine(OrbitalSimulationOptions orbitalSimulation, IBalanceService balance, IPlayerStateStore playerStateStore) { _balance = balance; _playerStateStore = playerStateStore; _orbitalSimulation = orbitalSimulation; _orbitalStateUpdater = new OrbitalStateUpdater(_orbitalSimulation); _infrastructureSimulation = new InfrastructureSimulationService(); _geopolitics = new GeopoliticalSimulationService(); _commanderPlanning = new CommanderPlanningService(); _playerFaction = new PlayerFactionService(); _stationSimulation = new StationSimulationService(); _stationLifecycle = new StationLifecycleService(_stationSimulation); _shipAi = new ShipAiService(balance); _projection = new SimulationProjectionService(_orbitalSimulation); } public WorldDelta Tick(SimulationWorld world, float deltaSeconds, long sequence) { var nowUtc = DateTimeOffset.UtcNow; var events = new List(); var simulationDeltaSeconds = deltaSeconds * MathF.Max(_balance.SimulationSpeedMultiplier, 0.01f); world.GeneratedAtUtc = nowUtc; world.OrbitalTimeSeconds += simulationDeltaSeconds * _orbitalSimulation.SimulatedSecondsPerRealSecond; _orbitalStateUpdater.Update(world); _infrastructureSimulation.UpdateClaims(world, events); _infrastructureSimulation.UpdateConstructionSites(world, events); _geopolitics.Update(world, simulationDeltaSeconds, events); _commanderPlanning.UpdateCommanders(world, _playerStateStore, simulationDeltaSeconds, events); _playerFaction.Update(world, _playerStateStore, simulationDeltaSeconds, events); _stationLifecycle.UpdateStations(world, simulationDeltaSeconds, events); foreach (var ship in world.Ships.ToList()) { if (ship.Health <= 0f) { continue; } var previousPosition = ship.Position; _shipAi.UpdateShip(world, ship, simulationDeltaSeconds, events); ship.Velocity = ship.Position.Subtract(previousPosition).Divide(simulationDeltaSeconds); } _orbitalStateUpdater.SyncSpatialState(world); CleanupDestroyedEntities(world, events); return _projection.BuildDelta(world, sequence, events); } public WorldSnapshot BuildSnapshot(SimulationWorld world, long sequence) => _projection.BuildSnapshot(world, sequence); public void PrimeDeltaBaseline(SimulationWorld world) => _projection.PrimeDeltaBaseline(world); internal static float GetShipCargoAmount(ShipRuntime ship) => SimulationRuntimeSupport.GetShipCargoAmount(ship); private static void CleanupDestroyedEntities(SimulationWorld world, ICollection events) { foreach (var ship in world.Ships.Where(candidate => candidate.Health <= 0f).ToList()) { CreateWreck(world, "ship", ship.Id, ship.SystemId, ship.Position, ship.Definition.GetTotalCargoCapacity() + (ship.Definition.Hull * 0.08f)); world.Ships.Remove(ship); if (ship.DockedStationId is not null && world.Stations.FirstOrDefault(station => station.Id == ship.DockedStationId) is { } dockedStation) { dockedStation.DockedShipIds.Remove(ship.Id); dockedStation.DockingPadAssignments.Remove(ship.AssignedDockingPadIndex ?? -1); } if (world.Factions.FirstOrDefault(candidate => candidate.Id == ship.FactionId) is { } faction) { faction.ShipsLost += 1; } if (ship.CommanderId is not null && world.Commanders.FirstOrDefault(candidate => candidate.Id == ship.CommanderId) is { } commander) { commander.IsAlive = false; } events.Add(new SimulationEventRecord("ship", ship.Id, "destroyed", $"{ship.Definition.Name} was destroyed.", DateTimeOffset.UtcNow)); } foreach (var station in world.Stations.Where(candidate => candidate.Health <= 0f).ToList()) { CreateWreck(world, "station", station.Id, station.SystemId, station.Position, station.MaxHealth * 0.12f); world.Stations.Remove(station); if (station.AnchorId is not null && world.Anchors.FirstOrDefault(candidate => candidate.Id == station.AnchorId) is { } anchor) { anchor.OccupyingStructureId = null; } foreach (var claim in world.Claims.Where(candidate => candidate.AnchorId == station.AnchorId)) { claim.Health = 0f; claim.State = ClaimStateKinds.Destroyed; } foreach (var site in world.ConstructionSites.Where(candidate => candidate.StationId == station.Id)) { site.State = ConstructionSiteStateKinds.Destroyed; } events.Add(new SimulationEventRecord("station", station.Id, "destroyed", $"{station.Label} was destroyed.", DateTimeOffset.UtcNow)); } } private static void CreateWreck(SimulationWorld world, string sourceKind, string sourceEntityId, string systemId, Vector3 position, float amount) { var itemId = world.ItemDefinitions.ContainsKey("scrapmetal") ? "scrapmetal" : world.ItemDefinitions.ContainsKey("rawscrap") ? "rawscrap" : world.ItemDefinitions.Keys.OrderBy(id => id, StringComparer.Ordinal).FirstOrDefault(); if (itemId is null || amount <= 0.01f) { return; } world.Wrecks.Add(new WreckRuntime { Id = $"wreck-{sourceKind}-{sourceEntityId}", SourceKind = sourceKind, SourceEntityId = sourceEntityId, SystemId = systemId, Position = position, ItemId = itemId, RemainingAmount = amount, MaxAmount = amount, }); } }