Refactor backend into domain-first slices
This commit is contained in:
80
apps/backend/Simulation/Core/SimulationEngine.cs
Normal file
80
apps/backend/Simulation/Core/SimulationEngine.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
|
||||
namespace SpaceGame.Api.Simulation.Core;
|
||||
|
||||
public sealed class SimulationEngine
|
||||
{
|
||||
private readonly OrbitalSimulationOptions _orbitalSimulation;
|
||||
private readonly OrbitalStateUpdater _orbitalStateUpdater;
|
||||
private readonly InfrastructureSimulationService _infrastructureSimulation;
|
||||
private readonly CommanderPlanningService _commanderPlanning;
|
||||
private readonly StationSimulationService _stationSimulation;
|
||||
private readonly StationLifecycleService _stationLifecycle;
|
||||
private readonly ShipControlService _shipControl;
|
||||
private readonly ShipTaskExecutionService _shipTaskExecution;
|
||||
private readonly SimulationProjectionService _projection;
|
||||
|
||||
public SimulationEngine(OrbitalSimulationOptions? orbitalSimulation = null)
|
||||
{
|
||||
_orbitalSimulation = orbitalSimulation ?? new OrbitalSimulationOptions();
|
||||
_orbitalStateUpdater = new OrbitalStateUpdater(_orbitalSimulation);
|
||||
_infrastructureSimulation = new InfrastructureSimulationService();
|
||||
_commanderPlanning = new CommanderPlanningService();
|
||||
_stationSimulation = new StationSimulationService();
|
||||
_stationLifecycle = new StationLifecycleService(_stationSimulation);
|
||||
_shipControl = new ShipControlService();
|
||||
_shipTaskExecution = new ShipTaskExecutionService();
|
||||
_projection = new SimulationProjectionService(_orbitalSimulation);
|
||||
}
|
||||
|
||||
public WorldDelta Tick(SimulationWorld world, float deltaSeconds, long sequence)
|
||||
{
|
||||
var nowUtc = DateTimeOffset.UtcNow;
|
||||
var events = new List<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);
|
||||
}
|
||||
814
apps/backend/Simulation/Core/SimulationProjectionService.cs
Normal file
814
apps/backend/Simulation/Core/SimulationProjectionService.cs
Normal file
@@ -0,0 +1,814 @@
|
||||
using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService;
|
||||
using static SpaceGame.Api.Stations.Simulation.StationSimulationService;
|
||||
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
|
||||
|
||||
namespace SpaceGame.Api.Simulation.Core;
|
||||
|
||||
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);
|
||||
|
||||
return new WorldSnapshot(
|
||||
world.Label,
|
||||
world.Seed,
|
||||
sequence,
|
||||
world.TickIntervalMs,
|
||||
world.OrbitalTimeSeconds,
|
||||
new OrbitalSimulationSnapshot(_orbitalSimulation.SimulatedSecondsPerRealSecond),
|
||||
world.GeneratedAtUtc,
|
||||
world.Systems.Select(system => new SystemSnapshot(
|
||||
system.Definition.Id,
|
||||
system.Definition.Label,
|
||||
ToDto(system.Position),
|
||||
system.Definition.Stars.Select(star => new StarSnapshot(
|
||||
star.Kind,
|
||||
star.Color,
|
||||
star.Glow,
|
||||
star.Size,
|
||||
star.OrbitRadius,
|
||||
star.OrbitSpeed,
|
||||
star.OrbitPhaseAtEpoch)).ToList(),
|
||||
system.Definition.Planets.Select(planet => new PlanetSnapshot(
|
||||
planet.Label,
|
||||
planet.PlanetType,
|
||||
planet.Shape,
|
||||
planet.Moons.Select(moon => new MoonSnapshot(
|
||||
moon.Label,
|
||||
moon.Size,
|
||||
moon.Color,
|
||||
moon.OrbitRadius,
|
||||
moon.OrbitSpeed,
|
||||
moon.OrbitPhaseAtEpoch,
|
||||
moon.OrbitInclination,
|
||||
moon.OrbitLongitudeOfAscendingNode)).ToList(),
|
||||
planet.OrbitRadius,
|
||||
planet.OrbitSpeed,
|
||||
planet.OrbitEccentricity,
|
||||
planet.OrbitInclination,
|
||||
planet.OrbitLongitudeOfAscendingNode,
|
||||
planet.OrbitArgumentOfPeriapsis,
|
||||
planet.OrbitPhaseAtEpoch,
|
||||
planet.Size,
|
||||
planet.Color,
|
||||
planet.HasRing)).ToList())).ToList(),
|
||||
world.Celestials.Select(ToCelestialDelta).Select(c => new CelestialSnapshot(
|
||||
c.Id,
|
||||
c.SystemId,
|
||||
c.Kind,
|
||||
c.OrbitalAnchor,
|
||||
c.LocalSpaceRadius,
|
||||
c.ParentNodeId,
|
||||
c.OccupyingStructureId,
|
||||
c.OrbitReferenceId)).ToList(),
|
||||
world.Nodes.Select(ToNodeDelta).Select(node => new ResourceNodeSnapshot(
|
||||
node.Id,
|
||||
node.SystemId,
|
||||
node.LocalPosition,
|
||||
node.CelestialId,
|
||||
node.SourceKind,
|
||||
node.OreRemaining,
|
||||
node.MaxOre,
|
||||
node.ItemId)).ToList(),
|
||||
world.Stations.Select(station => ToStationDelta(world, station)).Select(station => new StationSnapshot(
|
||||
station.Id,
|
||||
station.Label,
|
||||
station.Category,
|
||||
station.SystemId,
|
||||
station.LocalPosition,
|
||||
station.CelestialId,
|
||||
station.Color,
|
||||
station.DockedShips,
|
||||
station.DockedShipIds,
|
||||
station.DockingPads,
|
||||
station.CurrentProcesses,
|
||||
station.Inventory,
|
||||
station.FactionId,
|
||||
station.CommanderId,
|
||||
station.PolicySetId,
|
||||
station.Population,
|
||||
station.PopulationCapacity,
|
||||
station.WorkforceRequired,
|
||||
station.WorkforceEffectiveRatio,
|
||||
station.StorageUsage,
|
||||
station.InstalledModules,
|
||||
station.MarketOrderIds)).ToList(),
|
||||
world.Claims.Select(ToClaimDelta).Select(claim => new ClaimSnapshot(
|
||||
claim.Id,
|
||||
claim.FactionId,
|
||||
claim.SystemId,
|
||||
claim.CelestialId,
|
||||
claim.State,
|
||||
claim.Health,
|
||||
claim.PlacedAtUtc,
|
||||
claim.ActivatesAtUtc)).ToList(),
|
||||
world.ConstructionSites.Select(site => ToConstructionSiteDelta(world, site)).Select(site => new ConstructionSiteSnapshot(
|
||||
site.Id,
|
||||
site.FactionId,
|
||||
site.SystemId,
|
||||
site.CelestialId,
|
||||
site.TargetKind,
|
||||
site.TargetDefinitionId,
|
||||
site.BlueprintId,
|
||||
site.ClaimId,
|
||||
site.StationId,
|
||||
site.State,
|
||||
site.Progress,
|
||||
site.Inventory,
|
||||
site.RequiredItems,
|
||||
site.DeliveredItems,
|
||||
site.AssignedConstructorShipIds,
|
||||
site.MarketOrderIds)).ToList(),
|
||||
world.MarketOrders.Select(ToMarketOrderDelta).Select(order => new MarketOrderSnapshot(
|
||||
order.Id,
|
||||
order.FactionId,
|
||||
order.StationId,
|
||||
order.ConstructionSiteId,
|
||||
order.Kind,
|
||||
order.ItemId,
|
||||
order.Amount,
|
||||
order.RemainingAmount,
|
||||
order.Valuation,
|
||||
order.ReserveThreshold,
|
||||
order.PolicySetId,
|
||||
order.State)).ToList(),
|
||||
world.Policies.Select(ToPolicySetDelta).Select(policy => new PolicySetSnapshot(
|
||||
policy.Id,
|
||||
policy.OwnerKind,
|
||||
policy.OwnerId,
|
||||
policy.TradeAccessPolicy,
|
||||
policy.DockingAccessPolicy,
|
||||
policy.ConstructionAccessPolicy,
|
||||
policy.OperationalRangePolicy)).ToList(),
|
||||
world.Ships.Select(ship => ToShipDelta(world, ship)).Select(ship => new ShipSnapshot(
|
||||
ship.Id,
|
||||
ship.Label,
|
||||
ship.Kind,
|
||||
ship.Class,
|
||||
ship.SystemId,
|
||||
ship.LocalPosition,
|
||||
ship.LocalVelocity,
|
||||
ship.TargetLocalPosition,
|
||||
ship.State,
|
||||
ship.OrderKind,
|
||||
ship.DefaultBehaviorKind,
|
||||
ship.BehaviorPhase,
|
||||
ship.ControllerTaskKind,
|
||||
ship.CommanderObjective,
|
||||
ship.CelestialId,
|
||||
ship.DockedStationId,
|
||||
ship.CommanderId,
|
||||
ship.PolicySetId,
|
||||
ship.CargoCapacity,
|
||||
ship.TravelSpeed,
|
||||
ship.TravelSpeedUnit,
|
||||
ship.Inventory,
|
||||
ship.FactionId,
|
||||
ship.Health,
|
||||
ship.History,
|
||||
ship.CurrentAction,
|
||||
ship.SpatialState)).ToList(),
|
||||
world.Factions.Select(faction => ToFactionDelta(faction, FindFactionCommander(world, faction.Id))).Select(faction => new FactionSnapshot(
|
||||
faction.Id,
|
||||
faction.Label,
|
||||
faction.Color,
|
||||
faction.Credits,
|
||||
faction.PopulationTotal,
|
||||
faction.OreMined,
|
||||
faction.GoodsProduced,
|
||||
faction.ShipsBuilt,
|
||||
faction.ShipsLost,
|
||||
faction.DefaultPolicySetId,
|
||||
faction.GoapState,
|
||||
faction.GoapPriorities)).ToList());
|
||||
}
|
||||
|
||||
public void PrimeDeltaBaseline(SimulationWorld world)
|
||||
{
|
||||
foreach (var node in world.Nodes)
|
||||
{
|
||||
node.LastDeltaSignature = BuildNodeSignature(node);
|
||||
}
|
||||
|
||||
foreach (var celestial in world.Celestials)
|
||||
{
|
||||
celestial.LastDeltaSignature = BuildCelestialSignature(celestial);
|
||||
}
|
||||
|
||||
foreach (var station in world.Stations)
|
||||
{
|
||||
station.LastDeltaSignature = BuildStationSignature(world, station);
|
||||
}
|
||||
|
||||
foreach (var claim in world.Claims)
|
||||
{
|
||||
claim.LastDeltaSignature = BuildClaimSignature(claim);
|
||||
}
|
||||
|
||||
foreach (var site in world.ConstructionSites)
|
||||
{
|
||||
site.LastDeltaSignature = BuildConstructionSiteSignature(site);
|
||||
}
|
||||
|
||||
foreach (var order in world.MarketOrders)
|
||||
{
|
||||
order.LastDeltaSignature = BuildMarketOrderSignature(order);
|
||||
}
|
||||
|
||||
foreach (var policy in world.Policies)
|
||||
{
|
||||
policy.LastDeltaSignature = BuildPolicySignature(policy);
|
||||
}
|
||||
|
||||
foreach (var ship in world.Ships)
|
||||
{
|
||||
ship.LastDeltaSignature = BuildShipSignature(world, ship);
|
||||
}
|
||||
|
||||
foreach (var faction in world.Factions)
|
||||
{
|
||||
faction.LastDeltaSignature = BuildFactionSignature(faction, FindFactionCommander(world, faction.Id));
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ResourceNodeDelta> BuildNodeDeltas(SimulationWorld world)
|
||||
{
|
||||
var deltas = new List<ResourceNodeDelta>();
|
||||
foreach (var node in world.Nodes)
|
||||
{
|
||||
var signature = BuildNodeSignature(node);
|
||||
if (signature == node.LastDeltaSignature)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
node.LastDeltaSignature = signature;
|
||||
deltas.Add(ToNodeDelta(node));
|
||||
}
|
||||
|
||||
return deltas;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<CelestialDelta> BuildCelestialDeltas(SimulationWorld world)
|
||||
{
|
||||
var deltas = new List<CelestialDelta>();
|
||||
foreach (var celestial in world.Celestials)
|
||||
{
|
||||
var signature = BuildCelestialSignature(celestial);
|
||||
if (signature == celestial.LastDeltaSignature)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
celestial.LastDeltaSignature = signature;
|
||||
deltas.Add(ToCelestialDelta(celestial));
|
||||
}
|
||||
|
||||
return deltas;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<StationDelta> BuildStationDeltas(SimulationWorld world)
|
||||
{
|
||||
var deltas = new List<StationDelta>();
|
||||
foreach (var station in world.Stations)
|
||||
{
|
||||
var signature = BuildStationSignature(world, station);
|
||||
if (signature == station.LastDeltaSignature)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
station.LastDeltaSignature = signature;
|
||||
deltas.Add(ToStationDelta(world, station));
|
||||
}
|
||||
|
||||
return deltas;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ClaimDelta> BuildClaimDeltas(SimulationWorld world)
|
||||
{
|
||||
var deltas = new List<ClaimDelta>();
|
||||
foreach (var claim in world.Claims)
|
||||
{
|
||||
var signature = BuildClaimSignature(claim);
|
||||
if (signature == claim.LastDeltaSignature)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
claim.LastDeltaSignature = signature;
|
||||
deltas.Add(ToClaimDelta(claim));
|
||||
}
|
||||
|
||||
return deltas;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ConstructionSiteDelta> BuildConstructionSiteDeltas(SimulationWorld world)
|
||||
{
|
||||
var deltas = new List<ConstructionSiteDelta>();
|
||||
foreach (var site in world.ConstructionSites)
|
||||
{
|
||||
var signature = BuildConstructionSiteSignature(site);
|
||||
if (signature == site.LastDeltaSignature)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
site.LastDeltaSignature = signature;
|
||||
deltas.Add(ToConstructionSiteDelta(world, site));
|
||||
}
|
||||
|
||||
return deltas;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<MarketOrderDelta> BuildMarketOrderDeltas(SimulationWorld world)
|
||||
{
|
||||
var deltas = new List<MarketOrderDelta>();
|
||||
foreach (var order in world.MarketOrders)
|
||||
{
|
||||
var signature = BuildMarketOrderSignature(order);
|
||||
if (signature == order.LastDeltaSignature)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
order.LastDeltaSignature = signature;
|
||||
deltas.Add(ToMarketOrderDelta(order));
|
||||
}
|
||||
|
||||
return deltas;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<PolicySetDelta> BuildPolicyDeltas(SimulationWorld world)
|
||||
{
|
||||
var deltas = new List<PolicySetDelta>();
|
||||
foreach (var policy in world.Policies)
|
||||
{
|
||||
var signature = BuildPolicySignature(policy);
|
||||
if (signature == policy.LastDeltaSignature)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
policy.LastDeltaSignature = signature;
|
||||
deltas.Add(ToPolicySetDelta(policy));
|
||||
}
|
||||
|
||||
return deltas;
|
||||
}
|
||||
|
||||
private IReadOnlyList<ShipDelta> BuildShipDeltas(SimulationWorld world)
|
||||
{
|
||||
var deltas = new List<ShipDelta>();
|
||||
foreach (var ship in world.Ships)
|
||||
{
|
||||
var signature = BuildShipSignature(world, ship);
|
||||
if (signature == ship.LastDeltaSignature)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ship.LastDeltaSignature = signature;
|
||||
deltas.Add(ToShipDelta(world, ship));
|
||||
}
|
||||
|
||||
return deltas;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<FactionDelta> BuildFactionDeltas(SimulationWorld world)
|
||||
{
|
||||
var deltas = new List<FactionDelta>();
|
||||
foreach (var faction in world.Factions)
|
||||
{
|
||||
var commander = FindFactionCommander(world, faction.Id);
|
||||
var signature = BuildFactionSignature(faction, commander);
|
||||
if (signature == faction.LastDeltaSignature)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
faction.LastDeltaSignature = signature;
|
||||
deltas.Add(ToFactionDelta(faction, commander));
|
||||
}
|
||||
|
||||
return deltas;
|
||||
}
|
||||
|
||||
private static CommanderRuntime? FindFactionCommander(SimulationWorld world, string factionId) =>
|
||||
world.Commanders.FirstOrDefault(c =>
|
||||
c.FactionId == factionId &&
|
||||
string.Equals(c.Kind, CommanderKind.Faction, StringComparison.Ordinal));
|
||||
|
||||
private static string BuildNodeSignature(ResourceNodeRuntime node) =>
|
||||
$"{node.SystemId}|{node.Position.X:0.###}|{node.Position.Y:0.###}|{node.Position.Z:0.###}|{node.CelestialId}|{node.OreRemaining:0.###}";
|
||||
|
||||
private static string BuildCelestialSignature(CelestialRuntime celestial) =>
|
||||
$"{celestial.SystemId}|{celestial.Kind.ToContractValue()}|{celestial.Position.X:0.###}|{celestial.Position.Y:0.###}|{celestial.Position.Z:0.###}|{celestial.LocalSpaceRadius:0.###}|{celestial.ParentNodeId}|{celestial.OccupyingStructureId}|{celestial.OrbitReferenceId}";
|
||||
|
||||
private static string BuildStationSignature(SimulationWorld world, StationRuntime station)
|
||||
{
|
||||
var processes = ToStationActionProgressSnapshots(world, station);
|
||||
return string.Join("|",
|
||||
station.SystemId,
|
||||
station.CelestialId ?? "none",
|
||||
station.CommanderId ?? "none",
|
||||
station.PolicySetId ?? "none",
|
||||
BuildInventorySignature(station.Inventory),
|
||||
string.Join(",", processes.Select(process => $"{process.Lane}:{process.Label}:{process.Progress:0.###}")),
|
||||
string.Join(",", station.DockedShipIds.OrderBy(id => id, StringComparer.Ordinal)),
|
||||
station.DockingPadAssignments.Count.ToString(),
|
||||
station.Population.ToString("0.###"),
|
||||
station.PopulationCapacity.ToString("0.###"),
|
||||
station.WorkforceRequired.ToString("0.###"),
|
||||
station.WorkforceEffectiveRatio.ToString("0.###"),
|
||||
string.Join(",", station.InstalledModules.OrderBy(moduleId => moduleId, StringComparer.Ordinal)),
|
||||
string.Join(",", station.MarketOrderIds.OrderBy(orderId => orderId, StringComparer.Ordinal)),
|
||||
station.ActiveConstruction?.ModuleId ?? "none",
|
||||
station.ActiveConstruction?.ProgressSeconds.ToString("0.###") ?? "0",
|
||||
string.Join(",", station.ProductionLaneTimers.OrderBy(entry => entry.Key, StringComparer.Ordinal).Select(entry => $"{entry.Key}:{entry.Value:0.###}")));
|
||||
}
|
||||
|
||||
private static string BuildClaimSignature(ClaimRuntime claim) =>
|
||||
$"{claim.FactionId}|{claim.SystemId}|{claim.CelestialId}|{claim.State}|{claim.Health:0.###}|{claim.ActivatesAtUtc:O}";
|
||||
|
||||
private static string BuildConstructionSiteSignature(ConstructionSiteRuntime site) =>
|
||||
$"{site.FactionId}|{site.SystemId}|{site.CelestialId}|{site.TargetKind}|{site.TargetDefinitionId}|{site.BlueprintId}|{site.ClaimId}|{site.StationId}|{site.State}|{site.Progress:0.###}|{BuildInventorySignature(site.Inventory)}|{BuildInventorySignature(site.RequiredItems)}|{BuildInventorySignature(site.DeliveredItems)}|{string.Join(",", site.AssignedConstructorShipIds.OrderBy(id => id, StringComparer.Ordinal))}|{string.Join(",", site.MarketOrderIds.OrderBy(id => id, StringComparer.Ordinal))}";
|
||||
|
||||
private static string BuildMarketOrderSignature(MarketOrderRuntime order) =>
|
||||
$"{order.FactionId}|{order.StationId}|{order.ConstructionSiteId}|{order.Kind}|{order.ItemId}|{order.Amount:0.###}|{order.RemainingAmount:0.###}|{order.Valuation:0.###}|{order.ReserveThreshold?.ToString("0.###") ?? "none"}|{order.PolicySetId}|{order.State}";
|
||||
|
||||
private static string BuildPolicySignature(PolicySetRuntime policy) =>
|
||||
$"{policy.OwnerKind}|{policy.OwnerId}|{policy.TradeAccessPolicy}|{policy.DockingAccessPolicy}|{policy.ConstructionAccessPolicy}|{policy.OperationalRangePolicy}";
|
||||
|
||||
private static string BuildShipSignature(SimulationWorld world, ShipRuntime ship) =>
|
||||
string.Join("|",
|
||||
ship.SystemId,
|
||||
ship.Position.X.ToString("0.###"),
|
||||
ship.Position.Y.ToString("0.###"),
|
||||
ship.Position.Z.ToString("0.###"),
|
||||
ship.Velocity.X.ToString("0.###"),
|
||||
ship.Velocity.Y.ToString("0.###"),
|
||||
ship.Velocity.Z.ToString("0.###"),
|
||||
ship.TargetPosition.X.ToString("0.###"),
|
||||
ship.TargetPosition.Y.ToString("0.###"),
|
||||
ship.TargetPosition.Z.ToString("0.###"),
|
||||
ship.State.ToContractValue(),
|
||||
ship.Order?.Kind ?? "none",
|
||||
ship.DefaultBehavior.Kind,
|
||||
ship.DefaultBehavior.Phase ?? "none",
|
||||
ship.ControllerTask.Kind.ToContractValue(),
|
||||
ship.SpatialState.CurrentCelestialId ?? "none",
|
||||
ship.DockedStationId ?? "none",
|
||||
ship.CommanderId ?? "none",
|
||||
ship.PolicySetId ?? "none",
|
||||
ship.SpatialState.SpaceLayer,
|
||||
ship.SpatialState.CurrentCelestialId ?? "none",
|
||||
ship.SpatialState.MovementRegime,
|
||||
ship.SpatialState.DestinationNodeId ?? "none",
|
||||
ship.SpatialState.Transit?.Regime ?? "none",
|
||||
ship.SpatialState.Transit?.OriginNodeId ?? "none",
|
||||
ship.SpatialState.Transit?.DestinationNodeId ?? "none",
|
||||
ship.SpatialState.Transit?.Progress.ToString("0.###") ?? "0",
|
||||
GetShipCargoAmount(ship).ToString("0.###"),
|
||||
ship.TrackedActionKey ?? "none",
|
||||
ship.TrackedActionTotal.ToString("0.###"),
|
||||
ship.ControllerTask.TargetEntityId is not null && world.ConstructionSites.FirstOrDefault(site => site.Id == ship.ControllerTask.TargetEntityId) is { } site
|
||||
? ShipTaskExecutionService.GetRemainingConstructionDelivery(world, site).ToString("0.###")
|
||||
: "0",
|
||||
ship.Health.ToString("0.###"),
|
||||
ship.ActionTimer.ToString("0.###"));
|
||||
|
||||
private static string BuildInventorySignature(IReadOnlyDictionary<string, float> inventory) =>
|
||||
string.Join(",",
|
||||
inventory
|
||||
.Where(entry => entry.Value > 0.001f)
|
||||
.OrderBy(entry => entry.Key, StringComparer.Ordinal)
|
||||
.Select(entry => $"{entry.Key}:{entry.Value:0.###}"));
|
||||
|
||||
private static string BuildFactionSignature(FactionRuntime faction, CommanderRuntime? commander)
|
||||
{
|
||||
var goapSig = commander?.LastGoalPriorities is { } prios
|
||||
? string.Join(",", prios.Select(p => $"{p.Name}:{p.Priority:0.##}"))
|
||||
: string.Empty;
|
||||
return $"{faction.Credits:0.###}|{faction.PopulationTotal:0.###}|{faction.OreMined:0.###}|{faction.GoodsProduced:0.###}|{faction.ShipsBuilt}|{faction.ShipsLost}|{faction.DefaultPolicySetId}|{goapSig}";
|
||||
}
|
||||
|
||||
private static ResourceNodeDelta ToNodeDelta(ResourceNodeRuntime node) => new(
|
||||
node.Id,
|
||||
node.SystemId,
|
||||
ToDto(node.Position),
|
||||
node.CelestialId,
|
||||
node.SourceKind,
|
||||
node.OreRemaining,
|
||||
node.MaxOre,
|
||||
node.ItemId);
|
||||
|
||||
private static CelestialDelta ToCelestialDelta(CelestialRuntime celestial) => new(
|
||||
celestial.Id,
|
||||
celestial.SystemId,
|
||||
celestial.Kind.ToContractValue(),
|
||||
ToDto(celestial.Position),
|
||||
celestial.LocalSpaceRadius,
|
||||
celestial.ParentNodeId,
|
||||
celestial.OccupyingStructureId,
|
||||
celestial.OrbitReferenceId);
|
||||
|
||||
private static StationDelta ToStationDelta(SimulationWorld world, StationRuntime station) => new(
|
||||
station.Id,
|
||||
station.Label,
|
||||
station.Category,
|
||||
station.SystemId,
|
||||
ToDto(station.Position),
|
||||
station.CelestialId,
|
||||
station.Color,
|
||||
station.DockedShipIds.Count,
|
||||
station.DockedShipIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
|
||||
GetDockingPadCount(station),
|
||||
ToStationActionProgressSnapshots(world, station),
|
||||
ToInventoryEntries(station.Inventory),
|
||||
station.FactionId,
|
||||
station.CommanderId,
|
||||
station.PolicySetId,
|
||||
station.Population,
|
||||
station.PopulationCapacity,
|
||||
station.WorkforceRequired,
|
||||
station.WorkforceEffectiveRatio,
|
||||
ToStationStorageUsageSnapshots(world, station),
|
||||
station.InstalledModules.OrderBy(moduleId => moduleId, StringComparer.Ordinal).ToList(),
|
||||
station.MarketOrderIds.OrderBy(orderId => orderId, StringComparer.Ordinal).ToList());
|
||||
|
||||
private static IReadOnlyList<StationActionProgressSnapshot> ToStationActionProgressSnapshots(SimulationWorld world, StationRuntime station) =>
|
||||
GetStationProductionLanes(world, station)
|
||||
.Select(laneKey =>
|
||||
{
|
||||
var recipe = SelectProductionRecipe(world, station, laneKey);
|
||||
var timer = GetStationProductionTimer(station, laneKey);
|
||||
var duration = MathF.Max(recipe?.Duration ?? 0.1f, 0.1f);
|
||||
var progress = Math.Clamp(timer / duration, 0f, 1f);
|
||||
return recipe is null || timer <= 0.01f
|
||||
? null
|
||||
: new StationActionProgressSnapshot(
|
||||
laneKey,
|
||||
recipe.Label,
|
||||
progress,
|
||||
duration * (1f - progress),
|
||||
duration,
|
||||
recipe.Inputs.Select(i => new RecipeEntrySnapshot(i.ItemId, i.Amount)).ToList(),
|
||||
recipe.Outputs.Select(o => new RecipeEntrySnapshot(o.ItemId, o.Amount)).ToList());
|
||||
})
|
||||
.Where(snapshot => snapshot is not null)
|
||||
.Cast<StationActionProgressSnapshot>()
|
||||
.ToList();
|
||||
|
||||
private static IReadOnlyList<StationStorageUsageSnapshot> ToStationStorageUsageSnapshots(SimulationWorld world, StationRuntime station)
|
||||
{
|
||||
string[] storageClasses = ["solid", "liquid", "container", "manufactured"];
|
||||
return storageClasses
|
||||
.Select(storageClass => new StationStorageUsageSnapshot(
|
||||
storageClass,
|
||||
station.Inventory
|
||||
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoKind == storageClass)
|
||||
.Sum(entry => entry.Value),
|
||||
GetStationStorageCapacity(station, storageClass)))
|
||||
.Where(snapshot => snapshot.Capacity > 0.01f)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static ClaimDelta ToClaimDelta(ClaimRuntime claim) => new(
|
||||
claim.Id,
|
||||
claim.FactionId,
|
||||
claim.SystemId,
|
||||
claim.CelestialId,
|
||||
claim.State,
|
||||
claim.Health,
|
||||
claim.PlacedAtUtc,
|
||||
claim.ActivatesAtUtc);
|
||||
|
||||
private static ConstructionSiteDelta ToConstructionSiteDelta(SimulationWorld world, ConstructionSiteRuntime site) => new(
|
||||
site.Id,
|
||||
site.FactionId,
|
||||
site.SystemId,
|
||||
site.CelestialId,
|
||||
site.TargetKind,
|
||||
site.TargetDefinitionId,
|
||||
site.BlueprintId,
|
||||
site.ClaimId,
|
||||
site.StationId,
|
||||
site.State,
|
||||
GetConstructionSiteProgress(world, site),
|
||||
ToInventoryEntries(site.Inventory),
|
||||
ToInventoryEntries(site.RequiredItems),
|
||||
ToInventoryEntries(site.DeliveredItems),
|
||||
site.AssignedConstructorShipIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
|
||||
site.MarketOrderIds.OrderBy(id => id, StringComparer.Ordinal).ToList());
|
||||
|
||||
private static float GetConstructionSiteProgress(SimulationWorld world, ConstructionSiteRuntime site)
|
||||
{
|
||||
if (site.BlueprintId is not null
|
||||
&& world.ModuleRecipes.TryGetValue(site.BlueprintId, out var recipe)
|
||||
&& recipe.Duration > 0.01f)
|
||||
{
|
||||
return Math.Clamp(site.Progress / recipe.Duration, 0f, 1f);
|
||||
}
|
||||
|
||||
return Math.Clamp(site.Progress, 0f, 1f);
|
||||
}
|
||||
|
||||
private static MarketOrderDelta ToMarketOrderDelta(MarketOrderRuntime order) => new(
|
||||
order.Id,
|
||||
order.FactionId,
|
||||
order.StationId,
|
||||
order.ConstructionSiteId,
|
||||
order.Kind,
|
||||
order.ItemId,
|
||||
order.Amount,
|
||||
order.RemainingAmount,
|
||||
order.Valuation,
|
||||
order.ReserveThreshold,
|
||||
order.PolicySetId,
|
||||
order.State);
|
||||
|
||||
private static PolicySetDelta ToPolicySetDelta(PolicySetRuntime policy) => new(
|
||||
policy.Id,
|
||||
policy.OwnerKind,
|
||||
policy.OwnerId,
|
||||
policy.TradeAccessPolicy,
|
||||
policy.DockingAccessPolicy,
|
||||
policy.ConstructionAccessPolicy,
|
||||
policy.OperationalRangePolicy);
|
||||
|
||||
private ShipDelta ToShipDelta(SimulationWorld world, ShipRuntime ship)
|
||||
{
|
||||
var commander = ship.CommanderId is null ? null
|
||||
: world.Commanders.FirstOrDefault(c => c.Id == ship.CommanderId && c.Kind == CommanderKind.Ship);
|
||||
|
||||
return new ShipDelta(
|
||||
ship.Id,
|
||||
ship.Definition.Label,
|
||||
ship.Definition.Kind,
|
||||
ship.Definition.Class,
|
||||
ship.SystemId,
|
||||
ToDto(ship.Position),
|
||||
ToDto(ship.Velocity),
|
||||
ToDto(ship.TargetPosition),
|
||||
ship.State.ToContractValue(),
|
||||
ship.Order?.Kind,
|
||||
ship.DefaultBehavior.Kind,
|
||||
ship.DefaultBehavior.Phase,
|
||||
ship.ControllerTask.Kind.ToContractValue(),
|
||||
commander?.ActiveActionName,
|
||||
ship.SpatialState.CurrentCelestialId,
|
||||
ship.DockedStationId,
|
||||
ship.CommanderId,
|
||||
ship.PolicySetId,
|
||||
ship.Definition.CargoCapacity,
|
||||
|
||||
ToShipTravelSpeed(ship).Speed,
|
||||
ToShipTravelSpeed(ship).Unit,
|
||||
ToInventoryEntries(ship.Inventory),
|
||||
ship.FactionId,
|
||||
ship.Health,
|
||||
ship.History.ToList(),
|
||||
ToShipActionProgressSnapshot(world, ship),
|
||||
ToShipSpatialStateSnapshot(ship.SpatialState));
|
||||
}
|
||||
|
||||
private static ShipActionProgressSnapshot? ToShipActionProgressSnapshot(SimulationWorld world, ShipRuntime ship)
|
||||
{
|
||||
var progress = ship.State switch
|
||||
{
|
||||
ShipState.SpoolingFtl => CreateShipActionProgress("FTL spool", ship.ActionTimer, MathF.Max(ship.Definition.SpoolTime, 0.1f)),
|
||||
ShipState.Ftl => ship.SpatialState.Transit is null ? null : new ShipActionProgressSnapshot("FTL", Math.Clamp(ship.SpatialState.Transit.Progress, 0f, 1f)),
|
||||
ShipState.SpoolingWarp => CreateShipActionProgress("Warp spool", ship.ActionTimer, MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f)),
|
||||
ShipState.Warping => ship.SpatialState.Transit is null ? null : new ShipActionProgressSnapshot("Warp", Math.Clamp(ship.SpatialState.Transit.Progress, 0f, 1f)),
|
||||
ShipState.Mining => CreateShipActionProgress("Mining", ship.ActionTimer, MathF.Max(world.Balance.MiningCycleSeconds, 0.1f)),
|
||||
ShipState.Docking => CreateShipActionProgress("Docking", ship.ActionTimer, MathF.Max(world.Balance.DockingDuration, 0.1f)),
|
||||
ShipState.Undocking => CreateShipActionProgress("Undocking", ship.ActionTimer, MathF.Max(world.Balance.UndockingDuration, 0.1f)),
|
||||
ShipState.Transferring => CreateShipRemainingActionProgress("Transfer", ship.TrackedActionTotal, GetShipCargoAmount(ship)),
|
||||
ShipState.Loading or ShipState.Unloading => null,
|
||||
ShipState.DeliveringConstruction => ship.ControllerTask.TargetEntityId is null
|
||||
? null
|
||||
: world.ConstructionSites.FirstOrDefault(site => site.Id == ship.ControllerTask.TargetEntityId) is not { } site
|
||||
? null
|
||||
: CreateShipRemainingActionProgress("Deliver materials", ship.TrackedActionTotal, ShipTaskExecutionService.GetRemainingConstructionDelivery(world, site)),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
return progress;
|
||||
}
|
||||
|
||||
private static (float Speed, string Unit) ToShipTravelSpeed(ShipRuntime ship)
|
||||
{
|
||||
return ship.SpatialState.MovementRegime switch
|
||||
{
|
||||
MovementRegimeKinds.FtlTransit => (ship.State == ShipState.Ftl ? ship.Definition.FtlSpeed : 0f, "ly/s"),
|
||||
MovementRegimeKinds.Warp => (ship.State == ShipState.Warping ? ship.Definition.WarpSpeed : 0f, "AU/s"),
|
||||
_ => (MathF.Sqrt(MathF.Max(0f, ship.Velocity.LengthSquared())) * SimulationUnits.MetersPerKilometer, "m/s"),
|
||||
};
|
||||
}
|
||||
|
||||
private static ShipActionProgressSnapshot CreateShipActionProgress(string label, float elapsedSeconds, float requiredSeconds) =>
|
||||
new(label, Math.Clamp(elapsedSeconds / requiredSeconds, 0f, 1f));
|
||||
|
||||
private static ShipActionProgressSnapshot? CreateShipRemainingActionProgress(string label, float totalAmount, float remainingAmount)
|
||||
{
|
||||
if (totalAmount <= 0.01f)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var progress = 1f - Math.Clamp(remainingAmount / totalAmount, 0f, 1f);
|
||||
return new ShipActionProgressSnapshot(label, progress);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<InventoryEntry> ToInventoryEntries(IReadOnlyDictionary<string, float> inventory) =>
|
||||
inventory
|
||||
.Where(entry => entry.Value > 0.001f)
|
||||
.OrderBy(entry => entry.Key, StringComparer.Ordinal)
|
||||
.Select(entry => new InventoryEntry(entry.Key, entry.Value))
|
||||
.ToList();
|
||||
|
||||
private static FactionDelta ToFactionDelta(FactionRuntime faction, CommanderRuntime? commander)
|
||||
{
|
||||
FactionGoapStateSnapshot? goapState = null;
|
||||
IReadOnlyList<FactionGoapPrioritySnapshot>? goapPriorities = null;
|
||||
|
||||
if (commander?.LastPlanningState is { } ps)
|
||||
{
|
||||
goapState = new FactionGoapStateSnapshot(
|
||||
ps.MilitaryShipCount,
|
||||
ps.MinerShipCount,
|
||||
ps.TransportShipCount,
|
||||
ps.ConstructorShipCount,
|
||||
ps.ControlledSystemCount,
|
||||
ps.TargetSystemCount,
|
||||
ps.HasShipFactory,
|
||||
ps.OreStockpile,
|
||||
ps.RefinedMetalsStockpile);
|
||||
}
|
||||
|
||||
if (commander?.LastGoalPriorities is { } prios)
|
||||
{
|
||||
goapPriorities = prios.Select(p => new FactionGoapPrioritySnapshot(p.Name, p.Priority)).ToList();
|
||||
}
|
||||
|
||||
return new FactionDelta(
|
||||
faction.Id,
|
||||
faction.Label,
|
||||
faction.Color,
|
||||
faction.Credits,
|
||||
faction.PopulationTotal,
|
||||
faction.OreMined,
|
||||
faction.GoodsProduced,
|
||||
faction.ShipsBuilt,
|
||||
faction.ShipsLost,
|
||||
faction.DefaultPolicySetId,
|
||||
goapState,
|
||||
goapPriorities);
|
||||
}
|
||||
|
||||
private static ShipSpatialStateSnapshot ToShipSpatialStateSnapshot(ShipSpatialStateRuntime state) => new(
|
||||
state.SpaceLayer,
|
||||
state.CurrentSystemId,
|
||||
state.CurrentCelestialId,
|
||||
state.LocalPosition is null ? null : ToDto(state.LocalPosition.Value),
|
||||
state.SystemPosition is null ? null : ToDto(state.SystemPosition.Value),
|
||||
state.MovementRegime,
|
||||
state.DestinationNodeId,
|
||||
state.Transit is null ? null : new ShipTransitSnapshot(
|
||||
state.Transit.Regime,
|
||||
state.Transit.OriginNodeId,
|
||||
state.Transit.DestinationNodeId,
|
||||
state.Transit.StartedAtUtc,
|
||||
state.Transit.ArrivalDueAtUtc,
|
||||
state.Transit.Progress));
|
||||
|
||||
private static Vector3Dto ToDto(Vector3 value) => new(value.X, value.Y, value.Z);
|
||||
}
|
||||
Reference in New Issue
Block a user