refactor(backend): modularize simulation engine
This commit is contained in:
84
apps/backend/Simulation/Engine/SimulationEngine.cs
Normal file
84
apps/backend/Simulation/Engine/SimulationEngine.cs
Normal 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);
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
@@ -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")),
|
||||||
@@ -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);
|
||||||
@@ -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)
|
||||||
{
|
{
|
||||||
@@ -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,
|
||||||
@@ -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)));
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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))
|
||||||
{
|
{
|
||||||
@@ -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));
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user