332 lines
11 KiB
C#
332 lines
11 KiB
C#
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
|
|
|
|
namespace SpaceGame.Api.Stations.Simulation;
|
|
|
|
internal sealed class InfrastructureSimulationService
|
|
{
|
|
internal void UpdateClaims(SimulationWorld world, ICollection<SimulationEventRecord> events)
|
|
{
|
|
foreach (var claim in world.Claims)
|
|
{
|
|
if (claim.State == ClaimStateKinds.Destroyed || claim.Health <= 0f)
|
|
{
|
|
if (claim.State != ClaimStateKinds.Destroyed)
|
|
{
|
|
claim.State = ClaimStateKinds.Destroyed;
|
|
events.Add(new SimulationEventRecord("claim", claim.Id, "claim-destroyed", $"Claim {claim.Id} was destroyed.", world.GeneratedAtUtc));
|
|
}
|
|
|
|
foreach (var site in world.ConstructionSites.Where(candidate => candidate.ClaimId == claim.Id))
|
|
{
|
|
site.State = ConstructionSiteStateKinds.Destroyed;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (claim.State == ClaimStateKinds.Activating && world.GeneratedAtUtc >= claim.ActivatesAtUtc)
|
|
{
|
|
claim.State = ClaimStateKinds.Active;
|
|
events.Add(new SimulationEventRecord("claim", claim.Id, "claim-activated", $"Claim {claim.Id} is now active.", world.GeneratedAtUtc));
|
|
}
|
|
}
|
|
}
|
|
|
|
internal void UpdateConstructionSites(SimulationWorld world, ICollection<SimulationEventRecord> events)
|
|
{
|
|
foreach (var site in world.ConstructionSites)
|
|
{
|
|
if (site.State == ConstructionSiteStateKinds.Destroyed)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var claim = site.ClaimId is null
|
|
? null
|
|
: world.Claims.FirstOrDefault(candidate => candidate.Id == site.ClaimId);
|
|
if (claim?.State == ClaimStateKinds.Destroyed)
|
|
{
|
|
site.State = ConstructionSiteStateKinds.Destroyed;
|
|
continue;
|
|
}
|
|
|
|
if (claim?.State == ClaimStateKinds.Active && site.State == ConstructionSiteStateKinds.Planned)
|
|
{
|
|
site.State = ConstructionSiteStateKinds.Active;
|
|
events.Add(new SimulationEventRecord("construction-site", site.Id, "site-active", $"Construction site {site.Id} is active.", world.GeneratedAtUtc));
|
|
}
|
|
|
|
foreach (var orderId in site.MarketOrderIds)
|
|
{
|
|
var order = world.MarketOrders.FirstOrDefault(candidate => candidate.Id == orderId);
|
|
if (order is null || !site.RequiredItems.TryGetValue(order.ItemId, out var required))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var remaining = MathF.Max(0f, required - GetConstructionDeliveredAmount(world, site, order.ItemId));
|
|
order.RemainingAmount = remaining;
|
|
order.State = remaining <= 0.01f
|
|
? MarketOrderStateKinds.Filled
|
|
: remaining < order.Amount
|
|
? MarketOrderStateKinds.PartiallyFilled
|
|
: MarketOrderStateKinds.Open;
|
|
}
|
|
}
|
|
}
|
|
|
|
internal static bool TryEnsureModuleConstructionStarted(StationRuntime station, ModuleRecipeDefinition recipe, string shipId)
|
|
{
|
|
if (station.ActiveConstruction is not null)
|
|
{
|
|
return string.Equals(station.ActiveConstruction.ModuleId, recipe.ModuleId, StringComparison.Ordinal)
|
|
&& string.Equals(station.ActiveConstruction.AssignedConstructorShipId, shipId, StringComparison.Ordinal);
|
|
}
|
|
|
|
if (!CanStartModuleConstruction(station, recipe))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
foreach (var input in recipe.Inputs)
|
|
{
|
|
RemoveInventory(station.Inventory, input.ItemId, input.Amount);
|
|
}
|
|
|
|
station.ActiveConstruction = new ModuleConstructionRuntime
|
|
{
|
|
ModuleId = recipe.ModuleId,
|
|
RequiredSeconds = recipe.Duration,
|
|
AssignedConstructorShipId = shipId,
|
|
};
|
|
|
|
return true;
|
|
}
|
|
|
|
internal static string? GetNextStationModuleToBuild(StationRuntime station, SimulationWorld world)
|
|
{
|
|
// Expand storage before it becomes a bottleneck
|
|
const float StorageExpansionThreshold = 0.85f;
|
|
var storageExpansionCandidates = new[]
|
|
{
|
|
("solid", "module_arg_stor_solid_m_01"),
|
|
("liquid", "module_arg_stor_liquid_m_01"),
|
|
("container", "module_arg_stor_container_m_01"),
|
|
};
|
|
|
|
foreach (var (storageClass, moduleId) in storageExpansionCandidates)
|
|
{
|
|
var capacity = GetStationStorageCapacity(station, storageClass);
|
|
if (capacity <= 0.01f)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var used = station.Inventory
|
|
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var def) && def.CargoKind == storageClass)
|
|
.Sum(entry => entry.Value);
|
|
|
|
if (used / capacity >= StorageExpansionThreshold && world.ModuleRecipes.ContainsKey(moduleId))
|
|
{
|
|
return moduleId;
|
|
}
|
|
}
|
|
|
|
var priorities = StationSimulationService.GetFactionExpansionPressure(world, station.FactionId) > 0f
|
|
? new (string ModuleId, int TargetCount)[]
|
|
{
|
|
("module_gen_prod_refinedmetals_01", 1),
|
|
("module_arg_stor_solid_m_01", 1),
|
|
("module_arg_stor_container_m_01", 1),
|
|
("module_gen_prod_hullparts_01", 2),
|
|
("module_gen_prod_advancedelectronics_01", 1),
|
|
("module_gen_build_l_01", 1),
|
|
("module_arg_dock_m_01_lowtech", 2),
|
|
("module_gen_prod_energycells_01", 2),
|
|
}
|
|
: new (string ModuleId, int TargetCount)[]
|
|
{
|
|
("module_gen_prod_refinedmetals_01", 1),
|
|
("module_arg_stor_solid_m_01", 1),
|
|
("module_arg_stor_container_m_01", 1),
|
|
("module_gen_prod_hullparts_01", 2),
|
|
("module_gen_prod_advancedelectronics_01", 1),
|
|
("module_gen_build_l_01", 1),
|
|
("module_gen_prod_energycells_01", 2),
|
|
("module_arg_dock_m_01_lowtech", 2),
|
|
};
|
|
|
|
foreach (var (moduleId, targetCount) in priorities)
|
|
{
|
|
if (CountModules(station.InstalledModules, moduleId) < targetCount
|
|
&& world.ModuleRecipes.ContainsKey(moduleId))
|
|
{
|
|
return moduleId;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
internal static void PrepareNextConstructionSiteStep(SimulationWorld world, StationRuntime station, ConstructionSiteRuntime site)
|
|
{
|
|
var nextModuleId = GetNextStationModuleToBuild(station, world);
|
|
foreach (var orderId in site.MarketOrderIds)
|
|
{
|
|
var order = world.MarketOrders.FirstOrDefault(candidate => candidate.Id == orderId);
|
|
if (order is not null)
|
|
{
|
|
order.State = MarketOrderStateKinds.Cancelled;
|
|
order.RemainingAmount = 0f;
|
|
world.MarketOrders.Remove(order);
|
|
}
|
|
|
|
station.MarketOrderIds.Remove(orderId);
|
|
}
|
|
|
|
site.MarketOrderIds.Clear();
|
|
site.Inventory.Clear();
|
|
site.DeliveredItems.Clear();
|
|
site.RequiredItems.Clear();
|
|
site.AssignedConstructorShipIds.Clear();
|
|
site.Progress = 0f;
|
|
|
|
if (nextModuleId is null || !world.ModuleRecipes.TryGetValue(nextModuleId, out var recipe))
|
|
{
|
|
site.State = ConstructionSiteStateKinds.Completed;
|
|
site.BlueprintId = null;
|
|
return;
|
|
}
|
|
|
|
site.BlueprintId = nextModuleId;
|
|
site.State = ConstructionSiteStateKinds.Active;
|
|
foreach (var input in recipe.Inputs)
|
|
{
|
|
site.RequiredItems[input.ItemId] = input.Amount;
|
|
site.DeliveredItems[input.ItemId] = 0f;
|
|
var orderId = $"market-order-{station.Id}-{nextModuleId}-{input.ItemId}";
|
|
site.MarketOrderIds.Add(orderId);
|
|
station.MarketOrderIds.Add(orderId);
|
|
world.MarketOrders.Add(new MarketOrderRuntime
|
|
{
|
|
Id = orderId,
|
|
FactionId = station.FactionId,
|
|
StationId = station.Id,
|
|
ConstructionSiteId = site.Id,
|
|
Kind = MarketOrderKinds.Buy,
|
|
ItemId = input.ItemId,
|
|
Amount = input.Amount,
|
|
RemainingAmount = input.Amount,
|
|
Valuation = 1f,
|
|
State = MarketOrderStateKinds.Open,
|
|
});
|
|
}
|
|
}
|
|
|
|
internal static int GetDockingPadCount(StationRuntime station) =>
|
|
CountModules(station.InstalledModules, "module_arg_dock_m_01_lowtech") * 2;
|
|
|
|
internal static int? ReserveDockingPad(StationRuntime station, string shipId)
|
|
{
|
|
if (station.DockingPadAssignments.FirstOrDefault(entry => string.Equals(entry.Value, shipId, StringComparison.Ordinal)) is var existing
|
|
&& !string.IsNullOrEmpty(existing.Value))
|
|
{
|
|
return existing.Key;
|
|
}
|
|
|
|
var padCount = GetDockingPadCount(station);
|
|
for (var padIndex = 0; padIndex < padCount; padIndex += 1)
|
|
{
|
|
if (station.DockingPadAssignments.ContainsKey(padIndex))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
station.DockingPadAssignments[padIndex] = shipId;
|
|
return padIndex;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
internal static void ReleaseDockingPad(StationRuntime station, string shipId)
|
|
{
|
|
var assignment = station.DockingPadAssignments.FirstOrDefault(entry => string.Equals(entry.Value, shipId, StringComparison.Ordinal));
|
|
if (!string.IsNullOrEmpty(assignment.Value))
|
|
{
|
|
station.DockingPadAssignments.Remove(assignment.Key);
|
|
}
|
|
}
|
|
|
|
internal static Vector3 GetDockingPadPosition(StationRuntime station, int padIndex)
|
|
{
|
|
var padCount = Math.Max(1, GetDockingPadCount(station));
|
|
var angle = ((MathF.PI * 2f) / padCount) * padIndex;
|
|
var radius = station.Radius + 18f;
|
|
return new Vector3(
|
|
station.Position.X + (MathF.Cos(angle) * radius),
|
|
station.Position.Y,
|
|
station.Position.Z + (MathF.Sin(angle) * radius));
|
|
}
|
|
|
|
internal static Vector3 GetDockingHoldPosition(StationRuntime station, string shipId)
|
|
{
|
|
var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal));
|
|
var angle = (hash % 360) * (MathF.PI / 180f);
|
|
var radius = station.Radius + 24f;
|
|
return new Vector3(
|
|
station.Position.X + (MathF.Cos(angle) * radius),
|
|
station.Position.Y,
|
|
station.Position.Z + (MathF.Sin(angle) * radius));
|
|
}
|
|
|
|
internal static Vector3 GetUndockTargetPosition(StationRuntime station, int? padIndex, float distance)
|
|
{
|
|
if (padIndex is null)
|
|
{
|
|
return new Vector3(station.Position.X + distance, station.Position.Y, station.Position.Z);
|
|
}
|
|
|
|
var pad = GetDockingPadPosition(station, padIndex.Value);
|
|
var dx = pad.X - station.Position.X;
|
|
var dz = pad.Z - station.Position.Z;
|
|
var length = MathF.Sqrt((dx * dx) + (dz * dz));
|
|
if (length <= 0.001f)
|
|
{
|
|
return new Vector3(station.Position.X + distance, station.Position.Y, station.Position.Z);
|
|
}
|
|
|
|
var scale = distance / length;
|
|
return new Vector3(
|
|
pad.X + (dx * scale),
|
|
station.Position.Y,
|
|
pad.Z + (dz * scale));
|
|
}
|
|
|
|
internal static Vector3 GetShipDockedPosition(ShipRuntime ship, StationRuntime station) =>
|
|
ship.AssignedDockingPadIndex is int padIndex
|
|
? GetDockingPadPosition(station, padIndex)
|
|
: station.Position;
|
|
|
|
internal static Vector3 GetConstructionHoldPosition(StationRuntime station, string shipId)
|
|
{
|
|
var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal));
|
|
var angle = (hash % 360) * (MathF.PI / 180f);
|
|
var radius = station.Radius + 78f;
|
|
return new Vector3(
|
|
station.Position.X + (MathF.Cos(angle) * radius),
|
|
station.Position.Y,
|
|
station.Position.Z + (MathF.Sin(angle) * radius));
|
|
}
|
|
|
|
internal static Vector3 GetResourceHoldPosition(Vector3 nodePosition, string shipId, float radius)
|
|
{
|
|
var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal));
|
|
var angle = (hash % 360) * (MathF.PI / 180f);
|
|
return new Vector3(
|
|
nodePosition.X + (MathF.Cos(angle) * radius),
|
|
nodePosition.Y,
|
|
nodePosition.Z + (MathF.Sin(angle) * radius));
|
|
}
|
|
}
|