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.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<string> modules, string moduleId) =>
internal static int CountModules(IEnumerable<string> modules, string moduleId) =>
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;
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)
{
@@ -76,7 +76,7 @@ public sealed partial class SimulationEngine
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 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();
}

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 ShipCommanderReplanInterval = 5f;
@@ -28,7 +32,7 @@ public sealed partial class SimulationEngine
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.
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")),

View File

@@ -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<SimulationEventRecord> events)
internal void UpdateClaims(SimulationWorld world, ICollection<SimulationEventRecord> events)
{
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)
{
@@ -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);

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)
{
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)
{

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

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;
@@ -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

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)
{
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<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);
}

View File

@@ -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<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);
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<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))
{

View File

@@ -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<SimulationEventRecord> events)
internal void RunStationProduction(SimulationWorld world, StationRuntime station, float deltaSeconds, ICollection<SimulationEventRecord> 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<string> GetStationProductionLanes(SimulationWorld world, StationRuntime station)
internal static IEnumerable<string> 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));
}