refactor(backend): modularize simulation engine

This commit is contained in:
2026-03-19 17:28:07 -04:00
parent 5c79946d57
commit 8d2a810f6b
12 changed files with 297 additions and 234 deletions

View File

@@ -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<SimulationEventRecord>();
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);
}

View File

@@ -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<WorldUpdateStep> _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<ShipUpdateStep> _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<SimulationEventRecord>();
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<SimulationEventRecord> events);
private delegate void ShipUpdateStepAction(
SimulationEngine engine,
ShipRuntime ship,
SimulationWorld world,
float deltaSeconds,
List<SimulationEventRecord> events);
private sealed record WorldUpdateStep(WorldUpdateStepAction Execute);
private sealed record ShipUpdateStep(ShipUpdateStepAction Execute);
}

View File

@@ -1,17 +1,17 @@
using SpaceGame.Simulation.Api.Contracts; using SpaceGame.Api.Data;
using SpaceGame.Simulation.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)); 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)); 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)) if (!world.ModuleDefinitions.TryGetValue(moduleId, out var definition))
{ {
@@ -28,7 +28,7 @@ public sealed partial class SimulationEngine
station.Radius = GetStationRadius(world, station); 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 var totalArea = station.Modules
.Select(module => world.ModuleDefinitions.TryGetValue(module.ModuleId, out var definition) ? definition.Radius * definition.Radius : 0f) .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))); 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 var baseCapacity = storageClass switch
{ {
@@ -60,13 +60,13 @@ public sealed partial class SimulationEngine
return baseCapacity + moduleCapacity; return baseCapacity + moduleCapacity;
} }
private static int CountModules(IEnumerable<string> modules, string moduleId) => internal static int CountModules(IEnumerable<string> modules, string moduleId) =>
modules.Count(candidate => string.Equals(candidate, moduleId, StringComparison.Ordinal)); modules.Count(candidate => string.Equals(candidate, moduleId, StringComparison.Ordinal));
private static float GetInventoryAmount(IReadOnlyDictionary<string, float> inventory, string itemId) => internal static float GetInventoryAmount(IReadOnlyDictionary<string, float> inventory, string itemId) =>
inventory.TryGetValue(itemId, out var amount) ? amount : 0f; inventory.TryGetValue(itemId, out var amount) ? amount : 0f;
private static void AddInventory(IDictionary<string, float> inventory, string itemId, float amount) internal static void AddInventory(IDictionary<string, float> inventory, string itemId, float amount)
{ {
if (amount <= 0f) if (amount <= 0f)
{ {
@@ -76,7 +76,7 @@ public sealed partial class SimulationEngine
inventory[itemId] = GetInventoryAmount((IReadOnlyDictionary<string, float>)inventory, itemId) + amount; inventory[itemId] = GetInventoryAmount((IReadOnlyDictionary<string, float>)inventory, itemId) + amount;
} }
private static float RemoveInventory(IDictionary<string, float> inventory, string itemId, float amount) internal static float RemoveInventory(IDictionary<string, float> inventory, string itemId, float amount)
{ {
var current = GetInventoryAmount((IReadOnlyDictionary<string, float>)inventory, itemId); var current = GetInventoryAmount((IReadOnlyDictionary<string, float>)inventory, itemId);
var removed = MathF.Min(current, amount); var removed = MathF.Min(current, amount);
@@ -93,18 +93,18 @@ public sealed partial class SimulationEngine
return removed; 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))); 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") HasShipCapabilities(ship.Definition, "mining")
&& world.ItemDefinitions.TryGetValue(node.ItemId, out var item) && world.ItemDefinitions.TryGetValue(node.ItemId, out var item)
&& string.Equals(item.CargoKind, ship.Definition.CargoKind, StringComparison.Ordinal); && 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); 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) if (workforceRequired <= 0.01f)
{ {
@@ -115,7 +115,7 @@ public sealed partial class SimulationEngine
return 0.1f + (0.9f * staffedRatio); return 0.1f + (0.9f * staffedRatio);
} }
private static string? GetStorageRequirement(string storageClass) => internal static string? GetStorageRequirement(string storageClass) =>
storageClass switch storageClass switch
{ {
"solid" => "module_arg_stor_solid_m_01", "solid" => "module_arg_stor_solid_m_01",
@@ -123,7 +123,7 @@ public sealed partial class SimulationEngine
_ => null, _ => 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)) if (amount <= 0f || !world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition))
{ {
@@ -156,15 +156,15 @@ public sealed partial class SimulationEngine
return accepted; 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); 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 => world.ConstructionSites.FirstOrDefault(site =>
string.Equals(site.StationId, stationId, StringComparison.Ordinal) string.Equals(site.StationId, stationId, StringComparison.Ordinal)
&& site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed); && 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 if (site.StationId is not null
&& world.Stations.FirstOrDefault(candidate => candidate.Id == site.StationId) is { } station) && world.Stations.FirstOrDefault(candidate => candidate.Id == site.StationId) is { } station)
@@ -175,6 +175,9 @@ public sealed partial class SimulationEngine
return GetInventoryAmount(site.DeliveredItems, itemId); 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); site.RequiredItems.All(entry => GetConstructionDeliveredAmount(world, site, entry.Key) + 0.001f >= entry.Value);
internal static float GetShipCargoAmount(ShipRuntime ship) =>
ship.Inventory.Values.Sum();
} }

View File

@@ -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 FactionCommanderReplanInterval = 10f;
private const float ShipCommanderReplanInterval = 5f; private const float ShipCommanderReplanInterval = 5f;
@@ -28,7 +32,7 @@ public sealed partial class SimulationEngine
private static readonly GoapGoal<ShipPlanningState> _shipGoal = new AssignObjectiveGoal(); private static readonly GoapGoal<ShipPlanningState> _shipGoal = new AssignObjectiveGoal();
private void UpdateCommanders(SimulationWorld world, float deltaSeconds, ICollection<SimulationEventRecord> events) internal void UpdateCommanders(SimulationEngine engine, SimulationWorld world, float deltaSeconds, ICollection<SimulationEventRecord> events)
{ {
// Faction commanders run first so their directives are available to ship commanders in the same tick. // Faction commanders run first so their directives are available to ship commanders in the same tick.
foreach (var commander in world.Commanders) foreach (var commander in world.Commanders)
@@ -39,7 +43,7 @@ public sealed partial class SimulationEngine
} }
TickCommander(commander, deltaSeconds); TickCommander(commander, deltaSeconds);
UpdateFactionCommander(world, commander); UpdateFactionCommander(engine, world, commander);
} }
foreach (var commander in world.Commanders) foreach (var commander in world.Commanders)
@@ -50,7 +54,7 @@ public sealed partial class SimulationEngine
} }
TickCommander(commander, deltaSeconds); 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) if (commander.ReplanTimer > 0f && !commander.NeedsReplan)
{ {
@@ -91,11 +95,11 @@ public sealed partial class SimulationEngine
foreach (var (goal, _) in rankedGoals.Take(3)) foreach (var (goal, _) in rankedGoals.Take(3))
{ {
var plan = _factionPlanner.Plan(state, goal, actions); 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) if (commander.ReplanTimer > 0f && !commander.NeedsReplan)
{ {
@@ -117,7 +121,7 @@ public sealed partial class SimulationEngine
{ {
commander.ActiveGoalName = _shipGoal.Name; commander.ActiveGoalName = _shipGoal.Name;
commander.ActiveActionName = action.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 => ConstructorShipCount = world.Ships.Count(s =>
s.FactionId == factionId && s.FactionId == factionId &&
string.Equals(s.Definition.Kind, "construction", StringComparison.Ordinal)), string.Equals(s.Definition.Kind, "construction", StringComparison.Ordinal)),
ControlledSystemCount = GetFactionControlledSystemsCount(world, factionId), ControlledSystemCount = StationSimulationService.GetFactionControlledSystemsCount(world, factionId),
TargetSystemCount = Math.Max(1, Math.Min(StrategicControlTargetSystems, world.Systems.Count)), 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)), HasShipFactory = stations.Any(s => s.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal)),
OreStockpile = stations.Sum(s => GetInventoryAmount(s.Inventory, "ore")), OreStockpile = stations.Sum(s => GetInventoryAmount(s.Inventory, "ore")),
RefinedMetalsStockpile = stations.Sum(s => GetInventoryAmount(s.Inventory, "refinedmetals")), RefinedMetalsStockpile = stations.Sum(s => GetInventoryAmount(s.Inventory, "refinedmetals")),

View File

@@ -1,11 +1,13 @@
using SpaceGame.Simulation.Api.Contracts; using SpaceGame.Api.Contracts;
using SpaceGame.Simulation.Api.Data; 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<SimulationEventRecord> events) internal void UpdateClaims(SimulationWorld world, ICollection<SimulationEventRecord> events)
{ {
foreach (var claim in world.Claims) foreach (var claim in world.Claims)
{ {
@@ -33,7 +35,7 @@ public sealed partial class SimulationEngine
} }
} }
private static void UpdateConstructionSites(SimulationWorld world, ICollection<SimulationEventRecord> events) internal void UpdateConstructionSites(SimulationWorld world, ICollection<SimulationEventRecord> events)
{ {
foreach (var site in world.ConstructionSites) 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) if (station.ActiveConstruction is not null)
{ {
@@ -104,7 +106,7 @@ public sealed partial class SimulationEngine
return true; 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 // Expand storage before it becomes a bottleneck
const float StorageExpansionThreshold = 0.85f; 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)[] ? new (string ModuleId, int TargetCount)[]
{ {
("module_gen_prod_refinedmetals_01", 1), ("module_gen_prod_refinedmetals_01", 1),
@@ -169,7 +171,7 @@ public sealed partial class SimulationEngine
return null; 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); var nextModuleId = GetNextStationModuleToBuild(station, world);
foreach (var orderId in site.MarketOrderIds) 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; 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 if (station.DockingPadAssignments.FirstOrDefault(entry => string.Equals(entry.Value, shipId, StringComparison.Ordinal)) is var existing
&& !string.IsNullOrEmpty(existing.Value)) && !string.IsNullOrEmpty(existing.Value))
@@ -250,7 +252,7 @@ public sealed partial class SimulationEngine
return null; 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)); var assignment = station.DockingPadAssignments.FirstOrDefault(entry => string.Equals(entry.Value, shipId, StringComparison.Ordinal));
if (!string.IsNullOrEmpty(assignment.Value)) 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 padCount = Math.Max(1, GetDockingPadCount(station));
var angle = ((MathF.PI * 2f) / padCount) * padIndex; var angle = ((MathF.PI * 2f) / padCount) * padIndex;
@@ -270,7 +272,7 @@ public sealed partial class SimulationEngine
station.Position.Z + (MathF.Sin(angle) * radius)); 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 hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal));
var angle = (hash % 360) * (MathF.PI / 180f); var angle = (hash % 360) * (MathF.PI / 180f);
@@ -281,7 +283,7 @@ public sealed partial class SimulationEngine
station.Position.Z + (MathF.Sin(angle) * radius)); 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) if (padIndex is null)
{ {
@@ -304,12 +306,12 @@ public sealed partial class SimulationEngine
pad.Z + (dz * scale)); 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 ship.AssignedDockingPadIndex is int padIndex
? GetDockingPadPosition(station, padIndex) ? GetDockingPadPosition(station, padIndex)
: station.Position; : 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 hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal));
var angle = (hash % 360) * (MathF.PI / 180f); var angle = (hash % 360) * (MathF.PI / 180f);
@@ -320,7 +322,7 @@ public sealed partial class SimulationEngine
station.Position.Z + (MathF.Sin(angle) * radius)); 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 hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal));
var angle = (hash % 360) * (MathF.PI / 180f); var angle = (hash % 360) * (MathF.PI / 180f);

View File

@@ -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) private static Vector3 ComputePlanetPosition(PlanetDefinition planet, float timeSeconds)
{ {
var eccentricity = Math.Clamp(planet.OrbitEccentricity, 0f, 0.85f); 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 worldTimeSeconds = (float)world.OrbitalTimeSeconds;
var celestialsById = world.Celestials.ToDictionary(c => c.Id, StringComparer.Ordinal); 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) foreach (var ship in world.Ships)
{ {

View File

@@ -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) => private static CommanderRuntime? GetShipCommander(SimulationWorld world, ShipRuntime ship) =>
ship.CommanderId is null ship.CommanderId is null
? null ? null
@@ -91,7 +98,7 @@ public sealed partial class SimulationEngine
commander.ActiveTask.Threshold = ship.ControllerTask.Threshold; 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); var commander = GetShipCommander(world, ship);
if (commander is not null) 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); var commander = GetShipCommander(world, ship);
if (ship.Order is not null) if (ship.Order is not null)
@@ -133,7 +140,7 @@ public sealed partial class SimulationEngine
return; return;
} }
_shipBehaviorStateMachine.Plan(this, ship, world); _shipBehaviorStateMachine.Plan(engine, ship, world);
SyncCommanderTask(commander, ship.ControllerTask); 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); var commander = GetShipCommander(world, ship);
if (ship.Order is not null && controllerEvent == "arrived") if (ship.Order is not null && controllerEvent == "arrived")
@@ -458,7 +465,7 @@ public sealed partial class SimulationEngine
return; return;
} }
_shipBehaviorStateMachine.ApplyEvent(this, ship, world, controllerEvent); _shipBehaviorStateMachine.ApplyEvent(engine, ship, world, controllerEvent);
if (commander is not null) if (commander is not null)
{ {
SyncShipToCommander(ship, commander); 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}"; 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) 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<SimulationEventRecord> 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() new()
{ {
Kind = ControllerTaskKind.Idle, Kind = ControllerTaskKind.Idle,

View File

@@ -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) 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) => internal static float GetShipCargoAmount(ShipRuntime ship) =>
ship.Inventory.Values.Sum(); SimulationRuntimeSupport.GetShipCargoAmount(ship);
private string UpdateExtract(ShipRuntime ship, SimulationWorld world, float deltaSeconds) private string UpdateExtract(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
{ {
@@ -448,6 +453,6 @@ public sealed partial class SimulationEngine
return "undocked"; 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))); site.RequiredItems.Sum(required => MathF.Max(0f, required.Value - GetConstructionDeliveredAmount(world, site, required.Key)));
} }

View File

@@ -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; 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 world.Systems.FirstOrDefault(candidate => string.Equals(candidate.Definition.Id, systemId, StringComparison.Ordinal))?.Position
?? Vector3.Zero; ?? Vector3.Zero;
private string UpdateControllerTask(ShipRuntime ship, SimulationWorld world, float deltaSeconds) internal string UpdateControllerTask(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
{ {
var task = ship.ControllerTask; var task = ship.ControllerTask;
return task.Kind switch return task.Kind switch

View File

@@ -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<SimulationEventRecord> 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) public WorldSnapshot BuildSnapshot(SimulationWorld world, long sequence)
{ {
PrimeDeltaBaseline(world); PrimeDeltaBaseline(world);
@@ -472,7 +502,7 @@ public sealed partial class SimulationEngine
ship.TrackedActionKey ?? "none", ship.TrackedActionKey ?? "none",
ship.TrackedActionTotal.ToString("0.###"), ship.TrackedActionTotal.ToString("0.###"),
ship.ControllerTask.TargetEntityId is not null && world.ConstructionSites.FirstOrDefault(site => site.Id == ship.ControllerTask.TargetEntityId) is { } site 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", : "0",
ship.Health.ToString("0.###"), ship.Health.ToString("0.###"),
ship.ActionTimer.ToString("0.###")); ship.ActionTimer.ToString("0.###"));
@@ -689,7 +719,7 @@ public sealed partial class SimulationEngine
? null ? null
: world.ConstructionSites.FirstOrDefault(site => site.Id == ship.ControllerTask.TargetEntityId) is not { } site : world.ConstructionSites.FirstOrDefault(site => site.Id == ship.ControllerTask.TargetEntityId) is not { } site
? null ? null
: CreateShipRemainingActionProgress("Deliver materials", ship.TrackedActionTotal, GetRemainingConstructionDelivery(world, site)), : CreateShipRemainingActionProgress("Deliver materials", ship.TrackedActionTotal, ShipTaskExecutionService.GetRemainingConstructionDelivery(world, site)),
_ => null, _ => null,
}; };
@@ -782,36 +812,5 @@ public sealed partial class SimulationEngine
state.Transit.ArrivalDueAtUtc, state.Transit.ArrivalDueAtUtc,
state.Transit.Progress)); state.Transit.Progress));
private static void EmitShipStateEvents(
ShipRuntime ship,
ShipState previousState,
string previousBehavior,
ControllerTaskKind previousTask,
string controllerEvent,
ICollection<SimulationEventRecord> 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); private static Vector3Dto ToDto(Vector3 value) => new(value.X, value.Y, value.Z);
} }

View File

@@ -1,20 +1,31 @@
using SpaceGame.Simulation.Api.Data; using SpaceGame.Api.Data;
using SpaceGame.Simulation.Api.Contracts; 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<SimulationEventRecord> events) internal StationLifecycleService(StationSimulationService stationSimulation)
{
_stationSimulation = stationSimulation;
}
internal void UpdateStations(SimulationWorld world, float deltaSeconds, ICollection<SimulationEventRecord> events)
{ {
var factionPopulation = new Dictionary<string, float>(StringComparer.Ordinal); var factionPopulation = new Dictionary<string, float>(StringComparer.Ordinal);
foreach (var station in world.Stations) foreach (var station in world.Stations)
{ {
UpdateStationPopulation(station, deltaSeconds, events); UpdateStationPopulation(station, deltaSeconds, events);
ReviewStationMarketOrders(world, station); _stationSimulation.ReviewStationMarketOrders(world, station);
RunStationProduction(world, station, deltaSeconds, events); _stationSimulation.RunStationProduction(world, station, deltaSeconds, events);
factionPopulation[station.FactionId] = GetInventoryAmount(factionPopulation, station.FactionId) + station.Population; 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); station.WorkforceEffectiveRatio = ComputeWorkforceRatio(station.Population, station.WorkforceRequired);
} }
private float CompleteShipRecipe(SimulationWorld world, StationRuntime station, RecipeDefinition recipe, ICollection<SimulationEventRecord> events) internal static float CompleteShipRecipe(SimulationWorld world, StationRuntime station, RecipeDefinition recipe, ICollection<SimulationEventRecord> events)
{ {
if (recipe.ShipOutputId is null || !world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var definition)) if (recipe.ShipOutputId is null || !world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var definition))
{ {

View File

@@ -1,11 +1,16 @@
using SpaceGame.Simulation.Api.Data; using SpaceGame.Api.Data;
using SpaceGame.Simulation.Api.Contracts; 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) if (station.CommanderId is null)
{ {
@@ -34,7 +39,7 @@ public sealed partial class SimulationEngine
ReconcileStationMarketOrders(world, station, desiredOrders); ReconcileStationMarketOrders(world, station, desiredOrders);
} }
private void RunStationProduction(SimulationWorld world, StationRuntime station, float deltaSeconds, ICollection<SimulationEventRecord> events) internal void RunStationProduction(SimulationWorld world, StationRuntime station, float deltaSeconds, ICollection<SimulationEventRecord> events)
{ {
var faction = world.Factions.FirstOrDefault(candidate => candidate.Id == station.FactionId); var faction = world.Factions.FirstOrDefault(candidate => candidate.Id == station.FactionId);
foreach (var laneKey in GetStationProductionLanes(world, station)) foreach (var laneKey in GetStationProductionLanes(world, station))
@@ -60,7 +65,7 @@ public sealed partial class SimulationEngine
if (recipe.ShipOutputId is not null) if (recipe.ShipOutputId is not null)
{ {
produced += CompleteShipRecipe(world, station, recipe, events); produced += StationLifecycleService.CompleteShipRecipe(world, station, recipe, events);
continue; continue;
} }
@@ -83,7 +88,7 @@ public sealed partial class SimulationEngine
} }
} }
private static IEnumerable<string> GetStationProductionLanes(SimulationWorld world, StationRuntime station) internal static IEnumerable<string> GetStationProductionLanes(SimulationWorld world, StationRuntime station)
{ {
foreach (var moduleId in station.InstalledModules.Distinct(StringComparer.Ordinal)) 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; 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 world.Recipes.Values
.Where(recipe => RecipeAppliesToStation(station, recipe) && string.Equals(GetStationProductionLaneKey(world, recipe), laneKey, StringComparison.Ordinal)) .Where(recipe => RecipeAppliesToStation(station, recipe) && string.Equals(GetStationProductionLaneKey(world, recipe), laneKey, StringComparison.Ordinal))
.OrderByDescending(recipe => GetStationRecipePriority(world, station, recipe)) .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 targetSystems = Math.Max(1, Math.Min(StrategicControlTargetSystems, world.Systems.Count));
var controlledSystems = GetFactionControlledSystemsCount(world, factionId); var controlledSystems = GetFactionControlledSystemsCount(world, factionId);
@@ -323,7 +328,7 @@ public sealed partial class SimulationEngine
return Math.Clamp(deficit / (float)targetSystems, 0f, 1f); 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)); return world.Systems.Count(system => FactionControlsSystem(world, factionId, system.Definition.Id));
} }