From 8d2a810f6bbfea5055e55e30bc1103a65accca31 Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Thu, 19 Mar 2026 17:28:07 -0400 Subject: [PATCH] refactor(backend): modularize simulation engine --- .../Simulation/Engine/SimulationEngine.cs | 84 +++++++++++++++ apps/backend/Simulation/SimulationEngine.cs | 101 ------------------ .../SimulationRuntimeSupport.cs} | 49 +++++---- .../CommanderPlanningService.cs} | 28 ++--- .../InfrastructureSimulationService.cs} | 40 +++---- .../OrbitalStateUpdater.cs} | 19 +++- .../ShipControlService.cs} | 58 ++++++++-- .../ShipTaskExecutionService.Actions.cs} | 13 ++- .../ShipTaskExecutionService.cs} | 10 +- .../SimulationProjectionService.cs} | 71 ++++++------ .../StationLifecycleService.cs} | 29 +++-- .../StationSimulationService.cs} | 29 ++--- 12 files changed, 297 insertions(+), 234 deletions(-) create mode 100644 apps/backend/Simulation/Engine/SimulationEngine.cs delete mode 100644 apps/backend/Simulation/SimulationEngine.cs rename apps/backend/Simulation/{SimulationEngine.PowerAndInventorySystems.cs => Support/SimulationRuntimeSupport.cs} (69%) rename apps/backend/Simulation/{SimulationEngine.CommanderSystem.cs => Systems/CommanderPlanningService.cs} (83%) rename apps/backend/Simulation/{SimulationEngine.ResourceAndInfrastructureSystems.cs => Systems/InfrastructureSimulationService.cs} (85%) rename apps/backend/Simulation/{SimulationEngine.OrbitalSystem.cs => Systems/OrbitalStateUpdater.cs} (95%) rename apps/backend/Simulation/{SimulationEngine.ShipControl.cs => Systems/ShipControlService.cs} (88%) rename apps/backend/Simulation/{SimulationEngine.ShipActionSystem.cs => Systems/ShipTaskExecutionService.Actions.cs} (96%) rename apps/backend/Simulation/{SimulationEngine.MovementSystem.cs => Systems/ShipTaskExecutionService.cs} (96%) rename apps/backend/Simulation/{SimulationEngine.Replication.cs => Systems/SimulationProjectionService.cs} (95%) rename apps/backend/Simulation/{SimulationEngine.StationSystems.cs => Systems/StationLifecycleService.cs} (79%) rename apps/backend/Simulation/{SimulationEngine.StationController.cs => Systems/StationSimulationService.cs} (91%) diff --git a/apps/backend/Simulation/Engine/SimulationEngine.cs b/apps/backend/Simulation/Engine/SimulationEngine.cs new file mode 100644 index 0000000..0ef05a0 --- /dev/null +++ b/apps/backend/Simulation/Engine/SimulationEngine.cs @@ -0,0 +1,84 @@ +using SpaceGame.Api.Contracts; +using SpaceGame.Api.Simulation.Model; +using SpaceGame.Api.Simulation.Support; +using SpaceGame.Api.Simulation.Systems; + +namespace SpaceGame.Api.Simulation.Engine; + +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) + { + 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); + 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 static float GetShipCargoAmount(ShipRuntime ship) => + SimulationRuntimeSupport.GetShipCargoAmount(ship); +} diff --git a/apps/backend/Simulation/SimulationEngine.cs b/apps/backend/Simulation/SimulationEngine.cs deleted file mode 100644 index 67dfb9f..0000000 --- a/apps/backend/Simulation/SimulationEngine.cs +++ /dev/null @@ -1,101 +0,0 @@ -using SpaceGame.Simulation.Api.Contracts; - -namespace SpaceGame.Simulation.Api.Simulation; - -public sealed partial class SimulationEngine -{ - private readonly OrbitalSimulationOptions _orbitalSimulation; - private const float WaterConsumptionPerWorkerPerSecond = 0.004f; - private const float PopulationGrowthPerSecond = 0.012f; - private const float PopulationAttritionPerSecond = 0.018f; - private static readonly ShipBehaviorStateMachine _shipBehaviorStateMachine = ShipBehaviorStateMachine.CreateDefault(); - private static readonly IReadOnlyList _worldUpdatePipeline = - [ - new((engine, world, deltaSeconds, nowUtc, events) => engine.UpdateOrbitalState(world)), - new((engine, world, deltaSeconds, nowUtc, events) => UpdateClaims(world, events)), - new((engine, world, deltaSeconds, nowUtc, events) => UpdateConstructionSites(world, events)), - new((engine, world, deltaSeconds, nowUtc, events) => engine.UpdateCommanders(world, deltaSeconds, events)), - new((engine, world, deltaSeconds, nowUtc, events) => engine.UpdateStations(world, deltaSeconds, events)), - ]; - private static readonly IReadOnlyList _shipUpdatePipeline = - [ - new((engine, ship, world, deltaSeconds, events) => engine.RefreshControlLayers(ship, world)), - new((engine, ship, world, deltaSeconds, events) => engine.PlanControllerTask(ship, world)), - ]; - - public SimulationEngine(OrbitalSimulationOptions? orbitalSimulation = null) - { - _orbitalSimulation = orbitalSimulation ?? new OrbitalSimulationOptions(); - } - - public WorldDelta Tick(SimulationWorld world, float deltaSeconds, long sequence) - { - var events = new List(); - var nowUtc = DateTimeOffset.UtcNow; - world.OrbitalTimeSeconds += deltaSeconds * _orbitalSimulation.SimulatedSecondsPerRealSecond; - - foreach (var step in _worldUpdatePipeline) - { - step.Execute(this, world, deltaSeconds, nowUtc, events); - } - - foreach (var ship in world.Ships) - { - var previousPosition = ship.Position; - var previousState = ship.State; - var previousBehavior = ship.DefaultBehavior.Kind; - var previousTask = ship.ControllerTask.Kind; - - foreach (var step in _shipUpdatePipeline) - { - step.Execute(this, ship, world, deltaSeconds, events); - } - - var controllerEvent = UpdateControllerTask(ship, world, deltaSeconds); - AdvanceControlState(ship, world, controllerEvent); - ship.Velocity = ship.Position.Subtract(previousPosition).Divide(deltaSeconds); - TrackHistory(ship, controllerEvent); - - EmitShipStateEvents(ship, previousState, previousBehavior, previousTask, controllerEvent, events); - } - - SyncSpatialState(world); - world.GeneratedAtUtc = nowUtc; - - return new WorldDelta( - 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)); - } - - private delegate void WorldUpdateStepAction( - SimulationEngine engine, - SimulationWorld world, - float deltaSeconds, - DateTimeOffset nowUtc, - List events); - - private delegate void ShipUpdateStepAction( - SimulationEngine engine, - ShipRuntime ship, - SimulationWorld world, - float deltaSeconds, - List events); - - private sealed record WorldUpdateStep(WorldUpdateStepAction Execute); - - private sealed record ShipUpdateStep(ShipUpdateStepAction Execute); -} diff --git a/apps/backend/Simulation/SimulationEngine.PowerAndInventorySystems.cs b/apps/backend/Simulation/Support/SimulationRuntimeSupport.cs similarity index 69% rename from apps/backend/Simulation/SimulationEngine.PowerAndInventorySystems.cs rename to apps/backend/Simulation/Support/SimulationRuntimeSupport.cs index de23a71..90019d7 100644 --- a/apps/backend/Simulation/SimulationEngine.PowerAndInventorySystems.cs +++ b/apps/backend/Simulation/Support/SimulationRuntimeSupport.cs @@ -1,17 +1,17 @@ -using SpaceGame.Simulation.Api.Contracts; -using SpaceGame.Simulation.Api.Data; +using SpaceGame.Api.Data; +using SpaceGame.Api.Simulation.Model; -namespace SpaceGame.Simulation.Api.Simulation; +namespace SpaceGame.Api.Simulation.Support; -public sealed partial class SimulationEngine +internal static class SimulationRuntimeSupport { - private static bool HasShipCapabilities(ShipDefinition definition, params string[] capabilities) => + internal static bool HasShipCapabilities(ShipDefinition definition, params string[] capabilities) => capabilities.All(cap => definition.Capabilities.Contains(cap, StringComparer.Ordinal)); - private static int CountStationModules(StationRuntime station, string moduleId) => + internal static int CountStationModules(StationRuntime station, string moduleId) => station.Modules.Count(module => string.Equals(module.ModuleId, moduleId, StringComparison.Ordinal)); - private static void AddStationModule(SimulationWorld world, StationRuntime station, string moduleId) + internal static void AddStationModule(SimulationWorld world, StationRuntime station, string moduleId) { if (!world.ModuleDefinitions.TryGetValue(moduleId, out var definition)) { @@ -28,7 +28,7 @@ public sealed partial class SimulationEngine station.Radius = GetStationRadius(world, station); } - private static float GetStationRadius(SimulationWorld world, StationRuntime station) + internal static float GetStationRadius(SimulationWorld world, StationRuntime station) { var totalArea = station.Modules .Select(module => world.ModuleDefinitions.TryGetValue(module.ModuleId, out var definition) ? definition.Radius * definition.Radius : 0f) @@ -36,7 +36,7 @@ public sealed partial class SimulationEngine return MathF.Max(24f, MathF.Sqrt(MathF.Max(totalArea, 1f))); } - private static float GetStationStorageCapacity(StationRuntime station, string storageClass) + internal static float GetStationStorageCapacity(StationRuntime station, string storageClass) { var baseCapacity = storageClass switch { @@ -60,13 +60,13 @@ public sealed partial class SimulationEngine return baseCapacity + moduleCapacity; } - private static int CountModules(IEnumerable modules, string moduleId) => + internal static int CountModules(IEnumerable modules, string moduleId) => modules.Count(candidate => string.Equals(candidate, moduleId, StringComparison.Ordinal)); - private static float GetInventoryAmount(IReadOnlyDictionary inventory, string itemId) => + internal static float GetInventoryAmount(IReadOnlyDictionary inventory, string itemId) => inventory.TryGetValue(itemId, out var amount) ? amount : 0f; - private static void AddInventory(IDictionary inventory, string itemId, float amount) + internal static void AddInventory(IDictionary inventory, string itemId, float amount) { if (amount <= 0f) { @@ -76,7 +76,7 @@ public sealed partial class SimulationEngine inventory[itemId] = GetInventoryAmount((IReadOnlyDictionary)inventory, itemId) + amount; } - private static float RemoveInventory(IDictionary inventory, string itemId, float amount) + internal static float RemoveInventory(IDictionary inventory, string itemId, float amount) { var current = GetInventoryAmount((IReadOnlyDictionary)inventory, itemId); var removed = MathF.Min(current, amount); @@ -93,18 +93,18 @@ public sealed partial class SimulationEngine return removed; } - private static bool HasStationModules(StationRuntime station, params string[] modules) => + internal static bool HasStationModules(StationRuntime station, params string[] modules) => modules.All(moduleId => station.Modules.Any(candidate => string.Equals(candidate.ModuleId, moduleId, StringComparison.Ordinal))); - private static bool CanExtractNode(ShipRuntime ship, ResourceNodeRuntime node, SimulationWorld world) => + internal static bool CanExtractNode(ShipRuntime ship, ResourceNodeRuntime node, SimulationWorld world) => HasShipCapabilities(ship.Definition, "mining") && world.ItemDefinitions.TryGetValue(node.ItemId, out var item) && string.Equals(item.CargoKind, ship.Definition.CargoKind, StringComparison.Ordinal); - private static bool CanBuildClaimBeacon(ShipRuntime ship) => + internal static bool CanBuildClaimBeacon(ShipRuntime ship) => string.Equals(ship.Definition.Kind, "military", StringComparison.Ordinal); - private static float ComputeWorkforceRatio(float population, float workforceRequired) + internal static float ComputeWorkforceRatio(float population, float workforceRequired) { if (workforceRequired <= 0.01f) { @@ -115,7 +115,7 @@ public sealed partial class SimulationEngine return 0.1f + (0.9f * staffedRatio); } - private static string? GetStorageRequirement(string storageClass) => + internal static string? GetStorageRequirement(string storageClass) => storageClass switch { "solid" => "module_arg_stor_solid_m_01", @@ -123,7 +123,7 @@ public sealed partial class SimulationEngine _ => null, }; - private static float TryAddStationInventory(SimulationWorld world, StationRuntime station, string itemId, float amount) + internal static float TryAddStationInventory(SimulationWorld world, StationRuntime station, string itemId, float amount) { if (amount <= 0f || !world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition)) { @@ -156,15 +156,15 @@ public sealed partial class SimulationEngine return accepted; } - private static bool CanStartModuleConstruction(StationRuntime station, ModuleRecipeDefinition recipe) => + internal static bool CanStartModuleConstruction(StationRuntime station, ModuleRecipeDefinition recipe) => recipe.Inputs.All(input => GetInventoryAmount(station.Inventory, input.ItemId) + 0.001f >= input.Amount); - private static ConstructionSiteRuntime? GetConstructionSiteForStation(SimulationWorld world, string stationId) => + internal static ConstructionSiteRuntime? GetConstructionSiteForStation(SimulationWorld world, string stationId) => world.ConstructionSites.FirstOrDefault(site => string.Equals(site.StationId, stationId, StringComparison.Ordinal) && site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed); - private static float GetConstructionDeliveredAmount(SimulationWorld world, ConstructionSiteRuntime site, string itemId) + internal static float GetConstructionDeliveredAmount(SimulationWorld world, ConstructionSiteRuntime site, string itemId) { if (site.StationId is not null && world.Stations.FirstOrDefault(candidate => candidate.Id == site.StationId) is { } station) @@ -175,6 +175,9 @@ public sealed partial class SimulationEngine return GetInventoryAmount(site.DeliveredItems, itemId); } - private static bool IsConstructionSiteReady(SimulationWorld world, ConstructionSiteRuntime site) => + internal static bool IsConstructionSiteReady(SimulationWorld world, ConstructionSiteRuntime site) => site.RequiredItems.All(entry => GetConstructionDeliveredAmount(world, site, entry.Key) + 0.001f >= entry.Value); + + internal static float GetShipCargoAmount(ShipRuntime ship) => + ship.Inventory.Values.Sum(); } diff --git a/apps/backend/Simulation/SimulationEngine.CommanderSystem.cs b/apps/backend/Simulation/Systems/CommanderPlanningService.cs similarity index 83% rename from apps/backend/Simulation/SimulationEngine.CommanderSystem.cs rename to apps/backend/Simulation/Systems/CommanderPlanningService.cs index 6f4b607..6e3b399 100644 --- a/apps/backend/Simulation/SimulationEngine.CommanderSystem.cs +++ b/apps/backend/Simulation/Systems/CommanderPlanningService.cs @@ -1,8 +1,12 @@ -using SpaceGame.Simulation.Api.Contracts; +using SpaceGame.Api.Contracts; +using SpaceGame.Api.Simulation.AI; +using SpaceGame.Api.Simulation.Engine; +using SpaceGame.Api.Simulation.Model; +using static SpaceGame.Api.Simulation.Support.SimulationRuntimeSupport; -namespace SpaceGame.Simulation.Api.Simulation; +namespace SpaceGame.Api.Simulation.Systems; -public sealed partial class SimulationEngine +internal sealed class CommanderPlanningService { private const float FactionCommanderReplanInterval = 10f; private const float ShipCommanderReplanInterval = 5f; @@ -28,7 +32,7 @@ public sealed partial class SimulationEngine private static readonly GoapGoal _shipGoal = new AssignObjectiveGoal(); - private void UpdateCommanders(SimulationWorld world, float deltaSeconds, ICollection events) + internal void UpdateCommanders(SimulationEngine engine, SimulationWorld world, float deltaSeconds, ICollection events) { // Faction commanders run first so their directives are available to ship commanders in the same tick. foreach (var commander in world.Commanders) @@ -39,7 +43,7 @@ public sealed partial class SimulationEngine } TickCommander(commander, deltaSeconds); - UpdateFactionCommander(world, commander); + UpdateFactionCommander(engine, world, commander); } foreach (var commander in world.Commanders) @@ -50,7 +54,7 @@ public sealed partial class SimulationEngine } TickCommander(commander, deltaSeconds); - UpdateShipCommander(world, commander); + UpdateShipCommander(engine, world, commander); } } @@ -62,7 +66,7 @@ public sealed partial class SimulationEngine } } - private void UpdateFactionCommander(SimulationWorld world, CommanderRuntime commander) + private void UpdateFactionCommander(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander) { if (commander.ReplanTimer > 0f && !commander.NeedsReplan) { @@ -91,11 +95,11 @@ public sealed partial class SimulationEngine foreach (var (goal, _) in rankedGoals.Take(3)) { var plan = _factionPlanner.Plan(state, goal, actions); - plan?.CurrentAction?.Execute(this, world, commander); + plan?.CurrentAction?.Execute(engine, world, commander); } } - private void UpdateShipCommander(SimulationWorld world, CommanderRuntime commander) + private void UpdateShipCommander(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander) { if (commander.ReplanTimer > 0f && !commander.NeedsReplan) { @@ -117,7 +121,7 @@ public sealed partial class SimulationEngine { commander.ActiveGoalName = _shipGoal.Name; commander.ActiveActionName = action.Name; - action.Execute(this, world, commander); + action.Execute(engine, world, commander); } } @@ -139,8 +143,8 @@ public sealed partial class SimulationEngine ConstructorShipCount = world.Ships.Count(s => s.FactionId == factionId && string.Equals(s.Definition.Kind, "construction", StringComparison.Ordinal)), - ControlledSystemCount = GetFactionControlledSystemsCount(world, factionId), - TargetSystemCount = Math.Max(1, Math.Min(StrategicControlTargetSystems, world.Systems.Count)), + ControlledSystemCount = StationSimulationService.GetFactionControlledSystemsCount(world, factionId), + TargetSystemCount = Math.Max(1, Math.Min(StationSimulationService.StrategicControlTargetSystems, world.Systems.Count)), HasShipFactory = stations.Any(s => s.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal)), OreStockpile = stations.Sum(s => GetInventoryAmount(s.Inventory, "ore")), RefinedMetalsStockpile = stations.Sum(s => GetInventoryAmount(s.Inventory, "refinedmetals")), diff --git a/apps/backend/Simulation/SimulationEngine.ResourceAndInfrastructureSystems.cs b/apps/backend/Simulation/Systems/InfrastructureSimulationService.cs similarity index 85% rename from apps/backend/Simulation/SimulationEngine.ResourceAndInfrastructureSystems.cs rename to apps/backend/Simulation/Systems/InfrastructureSimulationService.cs index 412ce36..45ec836 100644 --- a/apps/backend/Simulation/SimulationEngine.ResourceAndInfrastructureSystems.cs +++ b/apps/backend/Simulation/Systems/InfrastructureSimulationService.cs @@ -1,11 +1,13 @@ -using SpaceGame.Simulation.Api.Contracts; -using SpaceGame.Simulation.Api.Data; +using SpaceGame.Api.Contracts; +using SpaceGame.Api.Data; +using SpaceGame.Api.Simulation.Model; +using static SpaceGame.Api.Simulation.Support.SimulationRuntimeSupport; -namespace SpaceGame.Simulation.Api.Simulation; +namespace SpaceGame.Api.Simulation.Systems; -public sealed partial class SimulationEngine +internal sealed class InfrastructureSimulationService { - private static void UpdateClaims(SimulationWorld world, ICollection events) + internal void UpdateClaims(SimulationWorld world, ICollection events) { foreach (var claim in world.Claims) { @@ -33,7 +35,7 @@ public sealed partial class SimulationEngine } } - private static void UpdateConstructionSites(SimulationWorld world, ICollection events) + internal void UpdateConstructionSites(SimulationWorld world, ICollection events) { foreach (var site in world.ConstructionSites) { @@ -76,7 +78,7 @@ public sealed partial class SimulationEngine } } - private static bool TryEnsureModuleConstructionStarted(StationRuntime station, ModuleRecipeDefinition recipe, string shipId) + internal static bool TryEnsureModuleConstructionStarted(StationRuntime station, ModuleRecipeDefinition recipe, string shipId) { if (station.ActiveConstruction is not null) { @@ -104,7 +106,7 @@ public sealed partial class SimulationEngine return true; } - private static string? GetNextStationModuleToBuild(StationRuntime station, SimulationWorld world) + internal static string? GetNextStationModuleToBuild(StationRuntime station, SimulationWorld world) { // Expand storage before it becomes a bottleneck const float StorageExpansionThreshold = 0.85f; @@ -133,7 +135,7 @@ public sealed partial class SimulationEngine } } - var priorities = GetFactionExpansionPressure(world, station.FactionId) > 0f + var priorities = StationSimulationService.GetFactionExpansionPressure(world, station.FactionId) > 0f ? new (string ModuleId, int TargetCount)[] { ("module_gen_prod_refinedmetals_01", 1), @@ -169,7 +171,7 @@ public sealed partial class SimulationEngine return null; } - private static void PrepareNextConstructionSiteStep(SimulationWorld world, StationRuntime station, ConstructionSiteRuntime site) + internal static void PrepareNextConstructionSiteStep(SimulationWorld world, StationRuntime station, ConstructionSiteRuntime site) { var nextModuleId = GetNextStationModuleToBuild(station, world); foreach (var orderId in site.MarketOrderIds) @@ -224,10 +226,10 @@ public sealed partial class SimulationEngine } } - private static int GetDockingPadCount(StationRuntime station) => + internal static int GetDockingPadCount(StationRuntime station) => CountModules(station.InstalledModules, "module_arg_dock_m_01_lowtech") * 2; - private static int? ReserveDockingPad(StationRuntime station, string shipId) + internal static int? ReserveDockingPad(StationRuntime station, string shipId) { if (station.DockingPadAssignments.FirstOrDefault(entry => string.Equals(entry.Value, shipId, StringComparison.Ordinal)) is var existing && !string.IsNullOrEmpty(existing.Value)) @@ -250,7 +252,7 @@ public sealed partial class SimulationEngine return null; } - private static void ReleaseDockingPad(StationRuntime station, string shipId) + internal static void ReleaseDockingPad(StationRuntime station, string shipId) { var assignment = station.DockingPadAssignments.FirstOrDefault(entry => string.Equals(entry.Value, shipId, StringComparison.Ordinal)); if (!string.IsNullOrEmpty(assignment.Value)) @@ -259,7 +261,7 @@ public sealed partial class SimulationEngine } } - private static Vector3 GetDockingPadPosition(StationRuntime station, int padIndex) + internal static Vector3 GetDockingPadPosition(StationRuntime station, int padIndex) { var padCount = Math.Max(1, GetDockingPadCount(station)); var angle = ((MathF.PI * 2f) / padCount) * padIndex; @@ -270,7 +272,7 @@ public sealed partial class SimulationEngine station.Position.Z + (MathF.Sin(angle) * radius)); } - private static Vector3 GetDockingHoldPosition(StationRuntime station, string shipId) + internal static Vector3 GetDockingHoldPosition(StationRuntime station, string shipId) { var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal)); var angle = (hash % 360) * (MathF.PI / 180f); @@ -281,7 +283,7 @@ public sealed partial class SimulationEngine station.Position.Z + (MathF.Sin(angle) * radius)); } - private static Vector3 GetUndockTargetPosition(StationRuntime station, int? padIndex, float distance) + internal static Vector3 GetUndockTargetPosition(StationRuntime station, int? padIndex, float distance) { if (padIndex is null) { @@ -304,12 +306,12 @@ public sealed partial class SimulationEngine pad.Z + (dz * scale)); } - private static Vector3 GetShipDockedPosition(ShipRuntime ship, StationRuntime station) => + internal static Vector3 GetShipDockedPosition(ShipRuntime ship, StationRuntime station) => ship.AssignedDockingPadIndex is int padIndex ? GetDockingPadPosition(station, padIndex) : station.Position; - private static Vector3 GetConstructionHoldPosition(StationRuntime station, string shipId) + internal static Vector3 GetConstructionHoldPosition(StationRuntime station, string shipId) { var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal)); var angle = (hash % 360) * (MathF.PI / 180f); @@ -320,7 +322,7 @@ public sealed partial class SimulationEngine station.Position.Z + (MathF.Sin(angle) * radius)); } - private static Vector3 GetResourceHoldPosition(Vector3 nodePosition, string shipId, float radius) + internal static Vector3 GetResourceHoldPosition(Vector3 nodePosition, string shipId, float radius) { var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal)); var angle = (hash % 360) * (MathF.PI / 180f); diff --git a/apps/backend/Simulation/SimulationEngine.OrbitalSystem.cs b/apps/backend/Simulation/Systems/OrbitalStateUpdater.cs similarity index 95% rename from apps/backend/Simulation/SimulationEngine.OrbitalSystem.cs rename to apps/backend/Simulation/Systems/OrbitalStateUpdater.cs index a7eb829..1e2a3a3 100644 --- a/apps/backend/Simulation/SimulationEngine.OrbitalSystem.cs +++ b/apps/backend/Simulation/Systems/OrbitalStateUpdater.cs @@ -1,9 +1,18 @@ -using SpaceGame.Simulation.Api.Data; +using SpaceGame.Api.Data; +using SpaceGame.Api.Simulation.Model; +using static SpaceGame.Api.Simulation.Systems.InfrastructureSimulationService; -namespace SpaceGame.Simulation.Api.Simulation; +namespace SpaceGame.Api.Simulation.Systems; -public sealed partial class SimulationEngine +internal sealed class OrbitalStateUpdater { + private readonly OrbitalSimulationOptions _orbitalSimulation; + + internal OrbitalStateUpdater(OrbitalSimulationOptions orbitalSimulation) + { + _orbitalSimulation = orbitalSimulation; + } + private static Vector3 ComputePlanetPosition(PlanetDefinition planet, float timeSeconds) { var eccentricity = Math.Clamp(planet.OrbitEccentricity, 0f, 0.85f); @@ -153,7 +162,7 @@ public sealed partial class SimulationEngine } } - private void UpdateOrbitalState(SimulationWorld world) + internal void Update(SimulationWorld world) { var worldTimeSeconds = (float)world.OrbitalTimeSeconds; var celestialsById = world.Celestials.ToDictionary(c => c.Id, StringComparer.Ordinal); @@ -248,7 +257,7 @@ public sealed partial class SimulationEngine } } - private static void SyncSpatialState(SimulationWorld world) + internal void SyncSpatialState(SimulationWorld world) { foreach (var ship in world.Ships) { diff --git a/apps/backend/Simulation/SimulationEngine.ShipControl.cs b/apps/backend/Simulation/Systems/ShipControlService.cs similarity index 88% rename from apps/backend/Simulation/SimulationEngine.ShipControl.cs rename to apps/backend/Simulation/Systems/ShipControlService.cs index b00a1ee..da81067 100644 --- a/apps/backend/Simulation/SimulationEngine.ShipControl.cs +++ b/apps/backend/Simulation/Systems/ShipControlService.cs @@ -1,9 +1,16 @@ -using SpaceGame.Simulation.Api.Data; +using SpaceGame.Api.Contracts; +using SpaceGame.Api.Simulation.AI; +using SpaceGame.Api.Simulation.Engine; +using SpaceGame.Api.Simulation.Model; +using static SpaceGame.Api.Simulation.Systems.InfrastructureSimulationService; +using static SpaceGame.Api.Simulation.Support.SimulationRuntimeSupport; -namespace SpaceGame.Simulation.Api.Simulation; +namespace SpaceGame.Api.Simulation.Systems; -public sealed partial class SimulationEngine +internal sealed class ShipControlService { + private static readonly ShipBehaviorStateMachine _shipBehaviorStateMachine = ShipBehaviorStateMachine.CreateDefault(); + private static CommanderRuntime? GetShipCommander(SimulationWorld world, ShipRuntime ship) => ship.CommanderId is null ? null @@ -91,7 +98,7 @@ public sealed partial class SimulationEngine commander.ActiveTask.Threshold = ship.ControllerTask.Threshold; } - private void RefreshControlLayers(ShipRuntime ship, SimulationWorld world) + internal void RefreshControlLayers(ShipRuntime ship, SimulationWorld world) { var commander = GetShipCommander(world, ship); if (commander is not null) @@ -114,7 +121,7 @@ public sealed partial class SimulationEngine } } - private void PlanControllerTask(ShipRuntime ship, SimulationWorld world) + internal void PlanControllerTask(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) { var commander = GetShipCommander(world, ship); if (ship.Order is not null) @@ -133,7 +140,7 @@ public sealed partial class SimulationEngine return; } - _shipBehaviorStateMachine.Plan(this, ship, world); + _shipBehaviorStateMachine.Plan(engine, ship, world); SyncCommanderTask(commander, ship.ControllerTask); } @@ -436,7 +443,7 @@ public sealed partial class SimulationEngine } } - private void AdvanceControlState(ShipRuntime ship, SimulationWorld world, string controllerEvent) + internal void AdvanceControlState(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent) { var commander = GetShipCommander(world, ship); if (ship.Order is not null && controllerEvent == "arrived") @@ -458,7 +465,7 @@ public sealed partial class SimulationEngine return; } - _shipBehaviorStateMachine.ApplyEvent(this, ship, world, controllerEvent); + _shipBehaviorStateMachine.ApplyEvent(engine, ship, world, controllerEvent); if (commander is not null) { SyncShipToCommander(ship, commander); @@ -469,7 +476,7 @@ public sealed partial class SimulationEngine } } - private static void TrackHistory(ShipRuntime ship, string controllerEvent) + internal void TrackHistory(ShipRuntime ship, string controllerEvent) { var signature = $"{ship.State.ToContractValue()}|{ship.DefaultBehavior.Kind}|{ship.ControllerTask.Kind.ToContractValue()}|{ship.ControllerTask.TargetSystemId}|{ship.ControllerTask.TargetEntityId}|{GetShipCargoAmount(ship):0.0}|{controllerEvent}"; if (signature == ship.LastSignature) @@ -489,7 +496,38 @@ public sealed partial class SimulationEngine } } - private static ControllerTaskRuntime CreateIdleTask(float threshold) => + internal 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)); + } + } + + internal static ControllerTaskRuntime CreateIdleTask(float threshold) => new() { Kind = ControllerTaskKind.Idle, diff --git a/apps/backend/Simulation/SimulationEngine.ShipActionSystem.cs b/apps/backend/Simulation/Systems/ShipTaskExecutionService.Actions.cs similarity index 96% rename from apps/backend/Simulation/SimulationEngine.ShipActionSystem.cs rename to apps/backend/Simulation/Systems/ShipTaskExecutionService.Actions.cs index 3c3c024..2611a85 100644 --- a/apps/backend/Simulation/SimulationEngine.ShipActionSystem.cs +++ b/apps/backend/Simulation/Systems/ShipTaskExecutionService.Actions.cs @@ -1,6 +1,11 @@ -namespace SpaceGame.Simulation.Api.Simulation; +using SpaceGame.Api.Simulation.Model; +using SpaceGame.Api.Simulation.Support; +using static SpaceGame.Api.Simulation.Systems.InfrastructureSimulationService; +using static SpaceGame.Api.Simulation.Support.SimulationRuntimeSupport; -public sealed partial class SimulationEngine +namespace SpaceGame.Api.Simulation.Systems; + +internal sealed partial class ShipTaskExecutionService { private static bool AdvanceTimedAction(ShipRuntime ship, float deltaSeconds, float requiredSeconds) { @@ -26,7 +31,7 @@ public sealed partial class SimulationEngine } internal static float GetShipCargoAmount(ShipRuntime ship) => - ship.Inventory.Values.Sum(); + SimulationRuntimeSupport.GetShipCargoAmount(ship); private string UpdateExtract(ShipRuntime ship, SimulationWorld world, float deltaSeconds) { @@ -448,6 +453,6 @@ public sealed partial class SimulationEngine return "undocked"; } - private static float GetRemainingConstructionDelivery(SimulationWorld world, ConstructionSiteRuntime site) => + internal static float GetRemainingConstructionDelivery(SimulationWorld world, ConstructionSiteRuntime site) => site.RequiredItems.Sum(required => MathF.Max(0f, required.Value - GetConstructionDeliveredAmount(world, site, required.Key))); } diff --git a/apps/backend/Simulation/SimulationEngine.MovementSystem.cs b/apps/backend/Simulation/Systems/ShipTaskExecutionService.cs similarity index 96% rename from apps/backend/Simulation/SimulationEngine.MovementSystem.cs rename to apps/backend/Simulation/Systems/ShipTaskExecutionService.cs index 4d5b251..87b8ce4 100644 --- a/apps/backend/Simulation/SimulationEngine.MovementSystem.cs +++ b/apps/backend/Simulation/Systems/ShipTaskExecutionService.cs @@ -1,6 +1,10 @@ -namespace SpaceGame.Simulation.Api.Simulation; +using SpaceGame.Api.Simulation.Model; +using static SpaceGame.Api.Simulation.Systems.InfrastructureSimulationService; +using static SpaceGame.Api.Simulation.Support.SimulationRuntimeSupport; -public sealed partial class SimulationEngine +namespace SpaceGame.Api.Simulation.Systems; + +internal sealed partial class ShipTaskExecutionService { private const float WarpEngageDistanceKilometers = 250_000f; @@ -14,7 +18,7 @@ public sealed partial class SimulationEngine world.Systems.FirstOrDefault(candidate => string.Equals(candidate.Definition.Id, systemId, StringComparison.Ordinal))?.Position ?? Vector3.Zero; - private string UpdateControllerTask(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + internal string UpdateControllerTask(ShipRuntime ship, SimulationWorld world, float deltaSeconds) { var task = ship.ControllerTask; return task.Kind switch diff --git a/apps/backend/Simulation/SimulationEngine.Replication.cs b/apps/backend/Simulation/Systems/SimulationProjectionService.cs similarity index 95% rename from apps/backend/Simulation/SimulationEngine.Replication.cs rename to apps/backend/Simulation/Systems/SimulationProjectionService.cs index 0ed9649..0736c7c 100644 --- a/apps/backend/Simulation/SimulationEngine.Replication.cs +++ b/apps/backend/Simulation/Systems/SimulationProjectionService.cs @@ -1,9 +1,39 @@ -using SpaceGame.Simulation.Api.Contracts; +using SpaceGame.Api.Contracts; +using SpaceGame.Api.Simulation.Model; +using static SpaceGame.Api.Simulation.Systems.InfrastructureSimulationService; +using static SpaceGame.Api.Simulation.Systems.StationSimulationService; +using static SpaceGame.Api.Simulation.Support.SimulationRuntimeSupport; -namespace SpaceGame.Simulation.Api.Simulation; +namespace SpaceGame.Api.Simulation.Systems; -public sealed partial class SimulationEngine +internal sealed class SimulationProjectionService { + private readonly OrbitalSimulationOptions _orbitalSimulation; + + internal SimulationProjectionService(OrbitalSimulationOptions orbitalSimulation) + { + _orbitalSimulation = orbitalSimulation; + } + + internal WorldDelta BuildDelta(SimulationWorld world, long sequence, IReadOnlyList 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); @@ -472,7 +502,7 @@ public sealed partial class SimulationEngine 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.###") + ? ShipTaskExecutionService.GetRemainingConstructionDelivery(world, site).ToString("0.###") : "0", ship.Health.ToString("0.###"), ship.ActionTimer.ToString("0.###")); @@ -689,7 +719,7 @@ public sealed partial class SimulationEngine ? null : world.ConstructionSites.FirstOrDefault(site => site.Id == ship.ControllerTask.TargetEntityId) is not { } site ? null - : CreateShipRemainingActionProgress("Deliver materials", ship.TrackedActionTotal, GetRemainingConstructionDelivery(world, site)), + : CreateShipRemainingActionProgress("Deliver materials", ship.TrackedActionTotal, ShipTaskExecutionService.GetRemainingConstructionDelivery(world, site)), _ => null, }; @@ -782,36 +812,5 @@ public sealed partial class SimulationEngine 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); } diff --git a/apps/backend/Simulation/SimulationEngine.StationSystems.cs b/apps/backend/Simulation/Systems/StationLifecycleService.cs similarity index 79% rename from apps/backend/Simulation/SimulationEngine.StationSystems.cs rename to apps/backend/Simulation/Systems/StationLifecycleService.cs index 014eebf..2d2170b 100644 --- a/apps/backend/Simulation/SimulationEngine.StationSystems.cs +++ b/apps/backend/Simulation/Systems/StationLifecycleService.cs @@ -1,20 +1,31 @@ -using SpaceGame.Simulation.Api.Data; -using SpaceGame.Simulation.Api.Contracts; +using SpaceGame.Api.Data; +using SpaceGame.Api.Contracts; +using SpaceGame.Api.Simulation.Model; +using static SpaceGame.Api.Simulation.Systems.ShipControlService; +using static SpaceGame.Api.Simulation.Support.SimulationRuntimeSupport; -namespace SpaceGame.Simulation.Api.Simulation; +namespace SpaceGame.Api.Simulation.Systems; -public sealed partial class SimulationEngine +internal sealed class StationLifecycleService { - private const int StrategicControlTargetSystems = 5; + private const float WaterConsumptionPerWorkerPerSecond = 0.004f; + private const float PopulationGrowthPerSecond = 0.012f; + private const float PopulationAttritionPerSecond = 0.018f; + private readonly StationSimulationService _stationSimulation; - private void UpdateStations(SimulationWorld world, float deltaSeconds, ICollection events) + internal StationLifecycleService(StationSimulationService stationSimulation) + { + _stationSimulation = stationSimulation; + } + + internal void UpdateStations(SimulationWorld world, float deltaSeconds, ICollection events) { var factionPopulation = new Dictionary(StringComparer.Ordinal); foreach (var station in world.Stations) { UpdateStationPopulation(station, deltaSeconds, events); - ReviewStationMarketOrders(world, station); - RunStationProduction(world, station, deltaSeconds, events); + _stationSimulation.ReviewStationMarketOrders(world, station); + _stationSimulation.RunStationProduction(world, station, deltaSeconds, events); factionPopulation[station.FactionId] = GetInventoryAmount(factionPopulation, station.FactionId) + station.Population; } @@ -54,7 +65,7 @@ public sealed partial class SimulationEngine station.WorkforceEffectiveRatio = ComputeWorkforceRatio(station.Population, station.WorkforceRequired); } - private float CompleteShipRecipe(SimulationWorld world, StationRuntime station, RecipeDefinition recipe, ICollection events) + internal static float CompleteShipRecipe(SimulationWorld world, StationRuntime station, RecipeDefinition recipe, ICollection events) { if (recipe.ShipOutputId is null || !world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var definition)) { diff --git a/apps/backend/Simulation/SimulationEngine.StationController.cs b/apps/backend/Simulation/Systems/StationSimulationService.cs similarity index 91% rename from apps/backend/Simulation/SimulationEngine.StationController.cs rename to apps/backend/Simulation/Systems/StationSimulationService.cs index fa40607..b335e80 100644 --- a/apps/backend/Simulation/SimulationEngine.StationController.cs +++ b/apps/backend/Simulation/Systems/StationSimulationService.cs @@ -1,11 +1,16 @@ -using SpaceGame.Simulation.Api.Data; -using SpaceGame.Simulation.Api.Contracts; +using SpaceGame.Api.Data; +using SpaceGame.Api.Contracts; +using SpaceGame.Api.Simulation.Model; +using static SpaceGame.Api.Simulation.Systems.CommanderPlanningService; +using static SpaceGame.Api.Simulation.Support.SimulationRuntimeSupport; -namespace SpaceGame.Simulation.Api.Simulation; +namespace SpaceGame.Api.Simulation.Systems; -public sealed partial class SimulationEngine +internal sealed class StationSimulationService { - private void ReviewStationMarketOrders(SimulationWorld world, StationRuntime station) + internal const int StrategicControlTargetSystems = 5; + + internal void ReviewStationMarketOrders(SimulationWorld world, StationRuntime station) { if (station.CommanderId is null) { @@ -34,7 +39,7 @@ public sealed partial class SimulationEngine ReconcileStationMarketOrders(world, station, desiredOrders); } - private void RunStationProduction(SimulationWorld world, StationRuntime station, float deltaSeconds, ICollection events) + internal void RunStationProduction(SimulationWorld world, StationRuntime station, float deltaSeconds, ICollection events) { var faction = world.Factions.FirstOrDefault(candidate => candidate.Id == station.FactionId); foreach (var laneKey in GetStationProductionLanes(world, station)) @@ -60,7 +65,7 @@ public sealed partial class SimulationEngine if (recipe.ShipOutputId is not null) { - produced += CompleteShipRecipe(world, station, recipe, events); + produced += StationLifecycleService.CompleteShipRecipe(world, station, recipe, events); continue; } @@ -83,7 +88,7 @@ public sealed partial class SimulationEngine } } - private static IEnumerable GetStationProductionLanes(SimulationWorld world, StationRuntime station) + internal static IEnumerable GetStationProductionLanes(SimulationWorld world, StationRuntime station) { foreach (var moduleId in station.InstalledModules.Distinct(StringComparer.Ordinal)) { @@ -101,10 +106,10 @@ public sealed partial class SimulationEngine } } - private static float GetStationProductionTimer(StationRuntime station, string laneKey) => + internal static float GetStationProductionTimer(StationRuntime station, string laneKey) => station.ProductionLaneTimers.TryGetValue(laneKey, out var timer) ? timer : 0f; - private static RecipeDefinition? SelectProductionRecipe(SimulationWorld world, StationRuntime station, string laneKey) => + internal static RecipeDefinition? SelectProductionRecipe(SimulationWorld world, StationRuntime station, string laneKey) => world.Recipes.Values .Where(recipe => RecipeAppliesToStation(station, recipe) && string.Equals(GetStationProductionLaneKey(world, recipe), laneKey, StringComparison.Ordinal)) .OrderByDescending(recipe => GetStationRecipePriority(world, station, recipe)) @@ -315,7 +320,7 @@ public sealed partial class SimulationEngine } } - private static float GetFactionExpansionPressure(SimulationWorld world, string factionId) + internal static float GetFactionExpansionPressure(SimulationWorld world, string factionId) { var targetSystems = Math.Max(1, Math.Min(StrategicControlTargetSystems, world.Systems.Count)); var controlledSystems = GetFactionControlledSystemsCount(world, factionId); @@ -323,7 +328,7 @@ public sealed partial class SimulationEngine return Math.Clamp(deficit / (float)targetSystems, 0f, 1f); } - private static int GetFactionControlledSystemsCount(SimulationWorld world, string factionId) + internal static int GetFactionControlledSystemsCount(SimulationWorld world, string factionId) { return world.Systems.Count(system => FactionControlsSystem(world, factionId, system.Definition.Id)); }