namespace SpaceGame.Api.Simulation.Core; public sealed class SimulationEngine { private readonly OrbitalSimulationOptions _orbitalSimulation; private readonly OrbitalStateUpdater _orbitalStateUpdater; private readonly InfrastructureSimulationService _infrastructureSimulation; private readonly CommanderPlanningService _commanderPlanning; private readonly StationSimulationService _stationSimulation; private readonly StationLifecycleService _stationLifecycle; private readonly ShipControlService _shipControl; private readonly ShipTaskExecutionService _shipTaskExecution; private readonly SimulationProjectionService _projection; public SimulationEngine(OrbitalSimulationOptions? orbitalSimulation = null) { _orbitalSimulation = orbitalSimulation ?? new OrbitalSimulationOptions(); _orbitalStateUpdater = new OrbitalStateUpdater(_orbitalSimulation); _infrastructureSimulation = new InfrastructureSimulationService(); _commanderPlanning = new CommanderPlanningService(); _stationSimulation = new StationSimulationService(); _stationLifecycle = new StationLifecycleService(_stationSimulation); _shipControl = new ShipControlService(); _shipTaskExecution = new ShipTaskExecutionService(); _projection = new SimulationProjectionService(_orbitalSimulation); } public WorldDelta Tick(SimulationWorld world, float deltaSeconds, long sequence) { var nowUtc = DateTimeOffset.UtcNow; var events = new List(); world.OrbitalTimeSeconds += deltaSeconds * _orbitalSimulation.SimulatedSecondsPerRealSecond; _orbitalStateUpdater.Update(world); _infrastructureSimulation.UpdateClaims(world, events); _infrastructureSimulation.UpdateConstructionSites(world, events); _commanderPlanning.UpdateCommanders(this, world, deltaSeconds, events); _stationLifecycle.UpdateStations(world, deltaSeconds, events); foreach (var ship in world.Ships.ToList()) { if (ship.Health <= 0f) { continue; } var previousPosition = ship.Position; var previousState = ship.State; var previousBehavior = ship.DefaultBehavior.Kind; var previousTask = ship.ControllerTask.Kind; _shipControl.RefreshControlLayers(ship, world); _shipControl.PlanControllerTask(this, ship, world); var controllerEvent = _shipTaskExecution.UpdateControllerTask(ship, world, deltaSeconds); _shipControl.AdvanceControlState(this, ship, world, controllerEvent); ship.Velocity = ship.Position.Subtract(previousPosition).Divide(deltaSeconds); _shipControl.TrackHistory(ship, controllerEvent); _shipControl.EmitShipStateEvents(ship, previousState, previousBehavior, previousTask, controllerEvent, events); } _orbitalStateUpdater.SyncSpatialState(world); CleanupDestroyedEntities(world, events); world.GeneratedAtUtc = nowUtc; 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 void PlanResourceHarvest(ShipRuntime ship, SimulationWorld world, string resourceItemId, string requiredModule) => _shipControl.PlanResourceHarvest(ship, world, resourceItemId, requiredModule); internal void PlanStationConstruction(ShipRuntime ship, SimulationWorld world) => _shipControl.PlanStationConstruction(ship, world); internal void PlanAttackTarget(ShipRuntime ship, SimulationWorld world) => _shipControl.PlanAttackTarget(ship, world); internal void PlanTransportHaul(ShipRuntime ship, SimulationWorld world) => _shipControl.PlanTransportHaul(ship, 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()) { 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.Label} was destroyed.", DateTimeOffset.UtcNow)); } foreach (var station in world.Stations.Where(candidate => candidate.Health <= 0f).ToList()) { world.Stations.Remove(station); if (station.CelestialId is not null && world.Celestials.FirstOrDefault(candidate => candidate.Id == station.CelestialId) is { } celestial) { celestial.OccupyingStructureId = null; } foreach (var claim in world.Claims.Where(candidate => candidate.CelestialId == station.CelestialId)) { 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)); } } }