Compare commits

...

2 Commits

16 changed files with 490 additions and 157 deletions

View File

@@ -1,4 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using SpaceGame.Api.Shared.Runtime;
namespace SpaceGame.Api.Definitions; namespace SpaceGame.Api.Definitions;
@@ -114,6 +116,8 @@ public sealed class ItemDefinition
public string Description { get; set; } = string.Empty; public string Description { get; set; } = string.Empty;
public string Type { get; set; } = "material"; public string Type { get; set; } = "material";
public string CargoKind { get; set; } = string.Empty; public string CargoKind { get; set; } = string.Empty;
[JsonIgnore]
public StorageKind? CargoStorageKind { get; set; }
public float Volume { get; set; } = 1f; public float Volume { get; set; } = 1f;
public int Version { get; set; } public int Version { get; set; }
public string FactoryName { get; set; } = string.Empty; public string FactoryName { get; set; } = string.Empty;
@@ -189,13 +193,48 @@ public sealed class ModuleProductionDefinition
public List<RecipeInputDefinition> Wares { get; set; } = []; public List<RecipeInputDefinition> Wares { get; set; } = [];
} }
public sealed class ModuleDefinition public class ModuleDefinition
{ {
public ModuleDefinition()
{
}
[SetsRequiredMembers]
protected ModuleDefinition(ModuleDefinition source)
{
Id = source.Id;
Name = source.Name;
Description = source.Description;
Type = source.Type;
ModuleType = source.ModuleType;
Product = source.Product;
Products = [.. source.Products];
ProductionMode = source.ProductionMode;
Radius = source.Radius;
Hull = source.Hull;
WorkforceNeeded = source.WorkforceNeeded;
Version = source.Version;
Macro = source.Macro;
MakerRace = source.MakerRace;
ExplosionDamage = source.ExplosionDamage;
Price = source.Price;
Owners = [.. source.Owners];
Cargo = source.Cargo;
WorkForce = source.WorkForce;
Docks = [.. source.Docks];
Shields = [.. source.Shields];
Turrets = [.. source.Turrets];
Production = [.. source.Production];
Construction = source.Construction;
}
public required string Id { get; set; } public required string Id { get; set; }
public required string Name { get; set; } public required string Name { get; set; }
public string Description { get; set; } = string.Empty; public string Description { get; set; } = string.Empty;
public required string Type { get; set; } public required string Type { get; set; }
[JsonIgnore] [JsonIgnore]
public ModuleType ModuleType { get; set; }
[JsonIgnore]
public string? Product { get; set; } public string? Product { get; set; }
public List<string> Products { get; set; } = []; public List<string> Products { get; set; } = [];
public string ProductionMode { get; set; } = "passive"; public string ProductionMode { get; set; } = "passive";
@@ -223,6 +262,32 @@ public sealed class ModuleDefinition
} }
} }
public sealed class ProductionModuleDefinition : ModuleDefinition
{
[SetsRequiredMembers]
internal ProductionModuleDefinition(ModuleDefinition source)
: base(source)
{
ProductItemIds = [.. source.Products];
}
public IReadOnlyList<string> ProductItemIds { get; init; } = [];
}
public sealed class StorageModuleDefinition : ModuleDefinition
{
[SetsRequiredMembers]
internal StorageModuleDefinition(ModuleDefinition source, StorageKind storageKind, float storageCapacity)
: base(source)
{
StorageKind = storageKind;
StorageCapacity = storageCapacity;
}
public StorageKind StorageKind { get; init; }
public float StorageCapacity { get; init; }
}
public sealed class ModuleRecipeDefinition public sealed class ModuleRecipeDefinition
{ {
public required string ModuleId { get; set; } public required string ModuleId { get; set; }
@@ -274,6 +339,8 @@ public sealed class ShipDefinition
public float SpoolTime { get; set; } public float SpoolTime { get; set; }
public float CargoCapacity { get; set; } public float CargoCapacity { get; set; }
public string? CargoKind { get; set; } public string? CargoKind { get; set; }
[JsonIgnore]
public StorageKind? CargoStorageKind { get; set; }
public required string Color { get; set; } public required string Color { get; set; }
public required string HullColor { get; set; } public required string HullColor { get; set; }
public float Size { get; set; } public float Size { get; set; }

View File

@@ -94,12 +94,33 @@ public enum SpaceLayerKind
LocalSpace, LocalSpace,
} }
public static class MovementRegimeKinds public enum MovementRegimeKind
{ {
public const string LocalFlight = "local-flight"; LocalFlight,
public const string Warp = "warp"; Warp,
public const string StargateTransit = "stargate-transit"; StargateTransit,
public const string FtlTransit = "ftl-transit"; FtlTransit,
}
public enum ModuleType
{
BuildModule,
ConnectionModule,
DefenceModule,
DockArea,
Habitation,
Pier,
ProcessingModule,
Production,
Storage,
}
public enum StorageKind
{
Condensate,
Container,
Liquid,
Solid,
} }
public static class CommanderKind public static class CommanderKind
@@ -182,6 +203,68 @@ public static class MarketOrderStateKinds
public static class SimulationEnumMappings public static class SimulationEnumMappings
{ {
public static string ToDataValue(this ModuleType moduleType) => moduleType switch
{
ModuleType.BuildModule => "buildmodule",
ModuleType.ConnectionModule => "connectionmodule",
ModuleType.DefenceModule => "defencemodule",
ModuleType.DockArea => "dockarea",
ModuleType.Habitation => "habitation",
ModuleType.Pier => "pier",
ModuleType.ProcessingModule => "processingmodule",
ModuleType.Production => "production",
ModuleType.Storage => "storage",
_ => throw new ArgumentOutOfRangeException(nameof(moduleType), moduleType, null),
};
public static ModuleType ToModuleType(this string value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentOutOfRangeException(nameof(value), value, "Module type is required.");
}
return value.Trim().ToLowerInvariant() switch
{
"buildmodule" => ModuleType.BuildModule,
"connectionmodule" => ModuleType.ConnectionModule,
"defencemodule" => ModuleType.DefenceModule,
"dockarea" => ModuleType.DockArea,
"habitation" => ModuleType.Habitation,
"pier" => ModuleType.Pier,
"processingmodule" => ModuleType.ProcessingModule,
"production" => ModuleType.Production,
"storage" => ModuleType.Storage,
_ => throw new ArgumentOutOfRangeException(nameof(value), value, "Unsupported module type."),
};
}
public static string ToDataValue(this StorageKind storageKind) => storageKind switch
{
StorageKind.Condensate => "condensate",
StorageKind.Container => "container",
StorageKind.Liquid => "liquid",
StorageKind.Solid => "solid",
_ => throw new ArgumentOutOfRangeException(nameof(storageKind), storageKind, null),
};
public static StorageKind ToStorageKind(this string value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentOutOfRangeException(nameof(value), value, "Storage kind is required.");
}
return value.Trim().ToLowerInvariant() switch
{
"condensate" => StorageKind.Condensate,
"container" => StorageKind.Container,
"liquid" => StorageKind.Liquid,
"solid" => StorageKind.Solid,
_ => throw new ArgumentOutOfRangeException(nameof(value), value, "Unsupported storage kind."),
};
}
public static string ToContractValue(this SpatialNodeKind kind) => kind switch public static string ToContractValue(this SpatialNodeKind kind) => kind switch
{ {
SpatialNodeKind.Star => "star", SpatialNodeKind.Star => "star",
@@ -283,4 +366,13 @@ public static class SimulationEnumMappings
SpaceLayerKind.LocalSpace => "local-space", SpaceLayerKind.LocalSpace => "local-space",
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null), _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null),
}; };
public static string ToContractValue(this MovementRegimeKind kind) => kind switch
{
MovementRegimeKind.LocalFlight => "local-flight",
MovementRegimeKind.Warp => "warp",
MovementRegimeKind.StargateTransit => "stargate-transit",
MovementRegimeKind.FtlTransit => "ftl-transit",
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null),
};
} }

View File

@@ -6,8 +6,55 @@ internal static class SimulationRuntimeSupport
internal 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));
internal static int CountStationModules(StationRuntime station, string moduleId) => internal static int CountStationModules(StationRuntime station, ModuleType moduleType) =>
station.Modules.Count(module => string.Equals(module.ModuleId, moduleId, StringComparison.Ordinal)); station.Modules.Count(module => module.ModuleType == moduleType);
internal static float GetStationStorageCapacity(SimulationWorld world, StationRuntime station, StorageKind storageKind)
{
SyncStorageModuleLevels(world, station, storageKind);
return GetStorageModules(world, station, storageKind)
.Sum(entry => entry.Definition.StorageCapacity);
}
internal static bool HasStorageCapacity(SimulationWorld world, StationRuntime station, StorageKind storageKind)
{
SyncStorageModuleLevels(world, station, storageKind);
return GetStorageModules(world, station, storageKind).Any();
}
private static IEnumerable<(StorageStationModuleRuntime Module, StorageModuleDefinition Definition)> GetStorageModules(
SimulationWorld world,
StationRuntime station,
StorageKind storageKind) =>
station.Modules
.OfType<StorageStationModuleRuntime>()
.Where(module => module.StorageKind == storageKind)
.Select(module => world.ModuleDefinitions.TryGetValue(module.ModuleId, out var definition) && definition is StorageModuleDefinition storageDefinition
? (Module: module, Definition: storageDefinition)
: ((StorageStationModuleRuntime Module, StorageModuleDefinition Definition)?)null)
.Where(entry => entry is not null && entry.Value.Definition.StorageKind == storageKind)
.Select(entry => entry!.Value);
private static void SyncStorageModuleLevels(SimulationWorld world, StationRuntime station, StorageKind storageKind)
{
var storageModules = GetStorageModules(world, station, storageKind)
.OrderBy(entry => entry.Module.Id, StringComparer.Ordinal)
.ToList();
if (storageModules.Count == 0)
{
return;
}
var remaining = station.Inventory
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoStorageKind == storageKind)
.Sum(entry => entry.Value);
foreach (var (module, definition) in storageModules)
{
module.CurrentLevel = MathF.Min(remaining, definition.StorageCapacity);
remaining = MathF.Max(0f, remaining - definition.StorageCapacity);
}
}
internal static void AddStationModule(SimulationWorld world, StationRuntime station, string moduleId) internal static void AddStationModule(SimulationWorld world, StationRuntime station, string moduleId)
{ {
@@ -16,13 +63,7 @@ internal static class SimulationRuntimeSupport
return; return;
} }
station.Modules.Add(new StationModuleRuntime station.Modules.Add(StationModuleRuntime.Create($"{station.Id}-module-{station.Modules.Count + 1}", definition));
{
Id = $"{station.Id}-module-{station.Modules.Count + 1}",
ModuleId = moduleId,
Health = definition.Hull,
MaxHealth = definition.Hull,
});
station.Radius = GetStationRadius(world, station); station.Radius = GetStationRadius(world, station);
} }
@@ -34,30 +75,6 @@ internal static class SimulationRuntimeSupport
return MathF.Max(24f, MathF.Sqrt(MathF.Max(totalArea, 1f))); return MathF.Max(24f, MathF.Sqrt(MathF.Max(totalArea, 1f)));
} }
internal static float GetStationStorageCapacity(StationRuntime station, string storageClass)
{
var baseCapacity = storageClass switch
{
"manufactured" => 400f,
_ => 0f,
};
var bulkBays = CountStationModules(station, "module_arg_stor_solid_m_01");
var liquidTanks = CountStationModules(station, "module_arg_stor_liquid_m_01");
var containerBays = CountStationModules(station, "module_arg_stor_container_m_01");
var moduleCapacity = storageClass switch
{
"solid" => bulkBays * 1000f,
"liquid" => liquidTanks * 500f,
"container" => containerBays * 800f,
"manufactured" => containerBays * 200f,
_ => 0f,
};
return baseCapacity + moduleCapacity;
}
internal 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));
@@ -97,7 +114,8 @@ internal static class SimulationRuntimeSupport
internal 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); && item.CargoStorageKind is not null
&& item.CargoStorageKind == ship.Definition.CargoStorageKind;
internal 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);
@@ -113,13 +131,43 @@ internal static class SimulationRuntimeSupport
return 0.1f + (0.9f * staffedRatio); return 0.1f + (0.9f * staffedRatio);
} }
internal static string? GetStorageRequirement(string storageClass) => internal static string? GetStorageRequirement(
storageClass switch IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
StorageKind? storageKind)
{ {
"solid" => "module_arg_stor_solid_m_01", if (storageKind is not { } requiredStorageKind)
"liquid" => "module_arg_stor_liquid_m_01", {
_ => null, return null;
}; }
return moduleDefinitions.Values
.OfType<StorageModuleDefinition>()
.Where(definition => definition.StorageKind == requiredStorageKind)
.OrderBy(definition => GetPreferredStorageModuleRank(definition.Id))
.ThenBy(definition => definition.Id, StringComparer.Ordinal)
.Select(definition => definition.Id)
.FirstOrDefault();
}
private static int GetPreferredStorageModuleRank(string moduleId)
{
if (moduleId.Contains("_m_", StringComparison.Ordinal))
{
return 0;
}
if (moduleId.Contains("_s_", StringComparison.Ordinal))
{
return 1;
}
if (moduleId.Contains("_l_", StringComparison.Ordinal))
{
return 2;
}
return 3;
}
internal static float TryAddStationInventory(SimulationWorld world, StationRuntime station, string itemId, float amount) internal static float TryAddStationInventory(SimulationWorld world, StationRuntime station, string itemId, float amount)
{ {
@@ -128,21 +176,25 @@ internal static class SimulationRuntimeSupport
return 0f; return 0f;
} }
var storageClass = itemDefinition.CargoKind; var storageKind = itemDefinition.CargoStorageKind;
var requiredModule = GetStorageRequirement(storageClass); if (storageKind is null)
if (requiredModule is not null && !station.InstalledModules.Contains(requiredModule, StringComparer.Ordinal))
{ {
return 0f; return 0f;
} }
var capacity = GetStationStorageCapacity(station, storageClass); if (!HasStorageCapacity(world, station, storageKind.Value))
{
return 0f;
}
var capacity = GetStationStorageCapacity(world, station, storageKind.Value);
if (capacity <= 0.01f) if (capacity <= 0.01f)
{ {
return 0f; return 0f;
} }
var used = station.Inventory var used = station.Inventory
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoKind == storageClass) .Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoStorageKind == storageKind)
.Sum(entry => entry.Value); .Sum(entry => entry.Value);
var accepted = MathF.Min(amount, MathF.Max(0f, capacity - used)); var accepted = MathF.Min(amount, MathF.Max(0f, capacity - used));
if (accepted <= 0.01f) if (accepted <= 0.01f)

View File

@@ -1647,7 +1647,7 @@ internal sealed class ShipAiService
{ {
var distance = ship.Position.DistanceTo(targetPosition); var distance = ship.Position.DistanceTo(targetPosition);
ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace; ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace;
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight; ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight;
ship.SpatialState.Transit = null; ship.SpatialState.Transit = null;
ship.SpatialState.DestinationNodeId = targetCelestial?.Id; ship.SpatialState.DestinationNodeId = targetCelestial?.Id;
subTask.Progress = Math.Clamp(1f - (distance / MathF.Max(distance + GetLocalTravelSpeed(ship), 1f)), 0f, 1f); subTask.Progress = Math.Clamp(1f - (distance / MathF.Max(distance + GetLocalTravelSpeed(ship), 1f)), 0f, 1f);
@@ -1678,11 +1678,11 @@ internal sealed class ShipAiService
bool completeOnArrival) bool completeOnArrival)
{ {
var transit = ship.SpatialState.Transit; var transit = ship.SpatialState.Transit;
if (transit is null || transit.Regime != MovementRegimeKinds.Warp || transit.DestinationNodeId != targetCelestial.Id) if (transit is null || transit.Regime != MovementRegimeKind.Warp || transit.DestinationNodeId != targetCelestial.Id)
{ {
transit = new ShipTransitRuntime transit = new ShipTransitRuntime
{ {
Regime = MovementRegimeKinds.Warp, Regime = MovementRegimeKind.Warp,
OriginNodeId = ship.SpatialState.CurrentCelestialId, OriginNodeId = ship.SpatialState.CurrentCelestialId,
DestinationNodeId = targetCelestial.Id, DestinationNodeId = targetCelestial.Id,
StartedAtUtc = world.GeneratedAtUtc, StartedAtUtc = world.GeneratedAtUtc,
@@ -1692,7 +1692,7 @@ internal sealed class ShipAiService
} }
ship.SpatialState.SpaceLayer = SpaceLayerKind.SystemSpace; ship.SpatialState.SpaceLayer = SpaceLayerKind.SystemSpace;
ship.SpatialState.MovementRegime = MovementRegimeKinds.Warp; ship.SpatialState.MovementRegime = MovementRegimeKind.Warp;
ship.SpatialState.CurrentCelestialId = null; ship.SpatialState.CurrentCelestialId = null;
ship.SpatialState.DestinationNodeId = targetCelestial.Id; ship.SpatialState.DestinationNodeId = targetCelestial.Id;
@@ -1735,11 +1735,11 @@ internal sealed class ShipAiService
{ {
var destinationNodeId = targetCelestial?.Id; var destinationNodeId = targetCelestial?.Id;
var transit = ship.SpatialState.Transit; var transit = ship.SpatialState.Transit;
if (transit is null || transit.Regime != MovementRegimeKinds.FtlTransit || transit.DestinationNodeId != destinationNodeId) if (transit is null || transit.Regime != MovementRegimeKind.FtlTransit || transit.DestinationNodeId != destinationNodeId)
{ {
transit = new ShipTransitRuntime transit = new ShipTransitRuntime
{ {
Regime = MovementRegimeKinds.FtlTransit, Regime = MovementRegimeKind.FtlTransit,
OriginNodeId = ship.SpatialState.CurrentCelestialId, OriginNodeId = ship.SpatialState.CurrentCelestialId,
DestinationNodeId = destinationNodeId, DestinationNodeId = destinationNodeId,
StartedAtUtc = world.GeneratedAtUtc, StartedAtUtc = world.GeneratedAtUtc,
@@ -1749,7 +1749,7 @@ internal sealed class ShipAiService
} }
ship.SpatialState.SpaceLayer = SpaceLayerKind.GalaxySpace; ship.SpatialState.SpaceLayer = SpaceLayerKind.GalaxySpace;
ship.SpatialState.MovementRegime = MovementRegimeKinds.FtlTransit; ship.SpatialState.MovementRegime = MovementRegimeKind.FtlTransit;
ship.SpatialState.CurrentCelestialId = null; ship.SpatialState.CurrentCelestialId = null;
ship.SpatialState.DestinationNodeId = destinationNodeId; ship.SpatialState.DestinationNodeId = destinationNodeId;
@@ -1780,7 +1780,7 @@ internal sealed class ShipAiService
ship.SpatialState.CurrentSystemId = targetSystemId; ship.SpatialState.CurrentSystemId = targetSystemId;
ship.SpatialState.Transit = null; ship.SpatialState.Transit = null;
ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace; ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace;
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight; ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight;
ship.SpatialState.CurrentCelestialId = targetCelestial?.Id; ship.SpatialState.CurrentCelestialId = targetCelestial?.Id;
ship.SpatialState.DestinationNodeId = targetCelestial?.Id; ship.SpatialState.DestinationNodeId = targetCelestial?.Id;
ship.State = ShipState.Arriving; ship.State = ShipState.Arriving;
@@ -1795,7 +1795,7 @@ internal sealed class ShipAiService
ship.SpatialState.CurrentSystemId = targetSystemId; ship.SpatialState.CurrentSystemId = targetSystemId;
ship.SpatialState.Transit = null; ship.SpatialState.Transit = null;
ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace; ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace;
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight; ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight;
ship.SpatialState.CurrentCelestialId = targetCelestial?.Id; ship.SpatialState.CurrentCelestialId = targetCelestial?.Id;
ship.SpatialState.DestinationNodeId = targetCelestial?.Id; ship.SpatialState.DestinationNodeId = targetCelestial?.Id;
ship.State = ShipState.Arriving; ship.State = ShipState.Arriving;
@@ -2619,15 +2619,11 @@ internal sealed class ShipAiService
{ {
if (world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition)) if (world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition))
{ {
var storageModule = GetStorageRequirement(itemDefinition.CargoKind); var storageModule = GetStorageRequirement(world.ModuleDefinitions, itemDefinition.CargoStorageKind);
if (storageModule is not null && !modules.Contains(storageModule, StringComparer.Ordinal)) if (storageModule is not null && !modules.Contains(storageModule, StringComparer.Ordinal))
{ {
modules.Add(storageModule); modules.Add(storageModule);
} }
else if (storageModule is null && !modules.Contains("module_arg_stor_container_m_01", StringComparer.Ordinal))
{
modules.Add("module_arg_stor_container_m_01");
}
} }
} }

View File

@@ -580,9 +580,9 @@ internal sealed class SimulationProjectionService
ship.PolicySetId ?? "none", ship.PolicySetId ?? "none",
ship.SpatialState.SpaceLayer.ToContractValue(), ship.SpatialState.SpaceLayer.ToContractValue(),
ship.SpatialState.CurrentCelestialId ?? "none", ship.SpatialState.CurrentCelestialId ?? "none",
ship.SpatialState.MovementRegime, ship.SpatialState.MovementRegime.ToContractValue(),
ship.SpatialState.DestinationNodeId ?? "none", ship.SpatialState.DestinationNodeId ?? "none",
ship.SpatialState.Transit?.Regime ?? "none", ship.SpatialState.Transit?.Regime.ToContractValue() ?? "none",
ship.SpatialState.Transit?.OriginNodeId ?? "none", ship.SpatialState.Transit?.OriginNodeId ?? "none",
ship.SpatialState.Transit?.DestinationNodeId ?? "none", ship.SpatialState.Transit?.DestinationNodeId ?? "none",
ship.SpatialState.Transit?.Progress.ToString("0.###") ?? "0", ship.SpatialState.Transit?.Progress.ToString("0.###") ?? "0",
@@ -796,14 +796,14 @@ internal sealed class SimulationProjectionService
private static IReadOnlyList<StationStorageUsageSnapshot> ToStationStorageUsageSnapshots(SimulationWorld world, StationRuntime station) private static IReadOnlyList<StationStorageUsageSnapshot> ToStationStorageUsageSnapshots(SimulationWorld world, StationRuntime station)
{ {
string[] storageClasses = ["solid", "liquid", "container", "manufactured"]; StorageKind[] storageKinds = [StorageKind.Solid, StorageKind.Liquid, StorageKind.Container];
return storageClasses return storageKinds
.Select(storageClass => new StationStorageUsageSnapshot( .Select(storageKind => new StationStorageUsageSnapshot(
storageClass, storageKind.ToDataValue(),
station.Inventory station.Inventory
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoKind == storageClass) .Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoStorageKind == storageKind)
.Sum(entry => entry.Value), .Sum(entry => entry.Value),
GetStationStorageCapacity(station, storageClass))) GetStationStorageCapacity(world, station, storageKind)))
.Where(snapshot => snapshot.Capacity > 0.01f) .Where(snapshot => snapshot.Capacity > 0.01f)
.ToList(); .ToList();
} }
@@ -921,8 +921,8 @@ internal sealed class SimulationProjectionService
{ {
return ship.SpatialState.MovementRegime switch return ship.SpatialState.MovementRegime switch
{ {
MovementRegimeKinds.FtlTransit => (ship.State == ShipState.Ftl ? ship.Definition.FtlSpeed : 0f, "ly/s"), MovementRegimeKind.FtlTransit => (ship.State == ShipState.Ftl ? ship.Definition.FtlSpeed : 0f, "ly/s"),
MovementRegimeKinds.Warp => (ship.State == ShipState.Warping ? ship.Definition.WarpSpeed : 0f, "AU/s"), MovementRegimeKind.Warp => (ship.State == ShipState.Warping ? ship.Definition.WarpSpeed : 0f, "AU/s"),
_ => (MathF.Sqrt(MathF.Max(0f, ship.Velocity.LengthSquared())) * SimulationUnits.MetersPerKilometer, "m/s"), _ => (MathF.Sqrt(MathF.Max(0f, ship.Velocity.LengthSquared())) * SimulationUnits.MetersPerKilometer, "m/s"),
}; };
} }
@@ -1877,10 +1877,10 @@ internal sealed class SimulationProjectionService
state.CurrentCelestialId, state.CurrentCelestialId,
state.LocalPosition is null ? null : ToDto(state.LocalPosition.Value), state.LocalPosition is null ? null : ToDto(state.LocalPosition.Value),
state.SystemPosition is null ? null : ToDto(state.SystemPosition.Value), state.SystemPosition is null ? null : ToDto(state.SystemPosition.Value),
state.MovementRegime, state.MovementRegime.ToContractValue(),
state.DestinationNodeId, state.DestinationNodeId,
state.Transit is null ? null : new ShipTransitSnapshot( state.Transit is null ? null : new ShipTransitSnapshot(
state.Transit.Regime, state.Transit.Regime.ToContractValue(),
state.Transit.OriginNodeId, state.Transit.OriginNodeId,
state.Transit.DestinationNodeId, state.Transit.DestinationNodeId,
state.Transit.StartedAtUtc, state.Transit.StartedAtUtc,

View File

@@ -1,3 +1,6 @@
using SpaceGame.Api.Definitions;
using SpaceGame.Api.Shared.Runtime;
namespace SpaceGame.Api.Stations.Runtime; namespace SpaceGame.Api.Stations.Runtime;
public sealed class StationRuntime public sealed class StationRuntime
@@ -33,12 +36,55 @@ public sealed class StationRuntime
public string LastDeltaSignature { get; set; } = string.Empty; public string LastDeltaSignature { get; set; } = string.Empty;
} }
public sealed class StationModuleRuntime public class StationModuleRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string ModuleId { get; init; } public required string ModuleId { get; init; }
public required ModuleType ModuleType { get; init; }
public float Health { get; set; } public float Health { get; set; }
public float MaxHealth { get; set; } public float MaxHealth { get; set; }
public static StationModuleRuntime Create(string id, ModuleDefinition definition) =>
definition switch
{
StorageModuleDefinition storage => new StorageStationModuleRuntime
{
Id = id,
ModuleId = storage.Id,
ModuleType = storage.ModuleType,
StorageKind = storage.StorageKind,
Health = storage.Hull,
MaxHealth = storage.Hull,
},
ProductionModuleDefinition production => new ProductionStationModuleRuntime
{
Id = id,
ModuleId = production.Id,
ModuleType = production.ModuleType,
ProductItemIds = [.. production.ProductItemIds],
Health = production.Hull,
MaxHealth = production.Hull,
},
_ => new StationModuleRuntime
{
Id = id,
ModuleId = definition.Id,
ModuleType = definition.ModuleType,
Health = definition.Hull,
MaxHealth = definition.Hull,
},
};
}
public sealed class ProductionStationModuleRuntime : StationModuleRuntime
{
public IReadOnlyList<string> ProductItemIds { get; init; } = [];
}
public sealed class StorageStationModuleRuntime : StationModuleRuntime
{
public StorageKind StorageKind { get; init; }
public float CurrentLevel { get; set; }
} }
public sealed class ModuleConstructionRuntime public sealed class ModuleConstructionRuntime

View File

@@ -162,21 +162,21 @@ internal sealed class InfrastructureSimulationService
private static IEnumerable<string> GetStoragePressureCandidates(SimulationWorld world, StationRuntime station) private static IEnumerable<string> GetStoragePressureCandidates(SimulationWorld world, StationRuntime station)
{ {
foreach (var (storageClass, moduleId) in new[] foreach (var (storageKind, moduleId) in new[]
{ {
("solid", "module_arg_stor_solid_m_01"), (StorageKind.Solid, "module_arg_stor_solid_m_01"),
("liquid", "module_arg_stor_liquid_m_01"), (StorageKind.Liquid, "module_arg_stor_liquid_m_01"),
("container", "module_arg_stor_container_m_01"), (StorageKind.Container, "module_arg_stor_container_m_01"),
}) })
{ {
var capacity = GetStationStorageCapacity(station, storageClass); var capacity = GetStationStorageCapacity(world, station, storageKind);
if (capacity <= 0.01f) if (capacity <= 0.01f)
{ {
continue; continue;
} }
var used = station.Inventory var used = station.Inventory
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var def) && def.CargoKind == storageClass) .Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var def) && def.CargoStorageKind == storageKind)
.Sum(entry => entry.Value); .Sum(entry => entry.Value);
if (used / capacity >= 0.65f) if (used / capacity >= 0.65f)
{ {
@@ -195,14 +195,10 @@ internal sealed class InfrastructureSimulationService
continue; continue;
} }
if (GetStorageRequirement(itemDefinition.CargoKind) is { } storageModuleId) if (GetStorageRequirement(world.ModuleDefinitions, itemDefinition.CargoStorageKind) is { } storageModuleId)
{ {
yield return storageModuleId; yield return storageModuleId;
} }
else
{
yield return "module_arg_stor_container_m_01";
}
} }
if (world.ModuleDefinitions.TryGetValue(recipe.ModuleId, out var moduleDefinition)) if (world.ModuleDefinitions.TryGetValue(recipe.ModuleId, out var moduleDefinition))
@@ -214,14 +210,10 @@ internal sealed class InfrastructureSimulationService
continue; continue;
} }
if (GetStorageRequirement(itemDefinition.CargoKind) is { } storageModuleId) if (GetStorageRequirement(world.ModuleDefinitions, itemDefinition.CargoStorageKind) is { } storageModuleId)
{ {
yield return storageModuleId; yield return storageModuleId;
} }
else
{
yield return "module_arg_stor_container_m_01";
}
} }
} }
} }
@@ -324,16 +316,16 @@ internal sealed class InfrastructureSimulationService
string? objectiveCommodityId, string? objectiveCommodityId,
bool requiredByObjective) bool requiredByObjective)
{ {
var storageClass = storageModuleId switch var storageKind = storageModuleId switch
{ {
"module_arg_stor_solid_m_01" => "solid", "module_arg_stor_solid_m_01" => StorageKind.Solid,
"module_arg_stor_liquid_m_01" => "liquid", "module_arg_stor_liquid_m_01" => StorageKind.Liquid,
_ => "container", _ => StorageKind.Container,
}; };
var capacity = GetStationStorageCapacity(station, storageClass); var capacity = GetStationStorageCapacity(world, station, storageKind);
var used = station.Inventory var used = station.Inventory
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var def) && def.CargoKind == storageClass) .Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var def) && def.CargoStorageKind == storageKind)
.Sum(entry => entry.Value); .Sum(entry => entry.Value);
var utilization = capacity <= 0.01f ? 0f : used / capacity; var utilization = capacity <= 0.01f ? 0f : used / capacity;
@@ -342,8 +334,8 @@ internal sealed class InfrastructureSimulationService
if (!string.IsNullOrWhiteSpace(objectiveModuleId) && !string.IsNullOrWhiteSpace(objectiveCommodityId)) if (!string.IsNullOrWhiteSpace(objectiveModuleId) && !string.IsNullOrWhiteSpace(objectiveCommodityId))
{ {
var objectiveUsesStorage = ModuleNeedsStorageClass(world, objectiveModuleId, storageClass) var objectiveUsesStorage = ModuleNeedsStorageClass(world, objectiveModuleId, storageKind)
|| CommodityUsesStorageClass(world, objectiveCommodityId, storageClass); || CommodityUsesStorageClass(world, objectiveCommodityId, storageKind);
if (objectiveUsesStorage) if (objectiveUsesStorage)
{ {
score += 35f; score += 35f;
@@ -579,15 +571,15 @@ internal sealed class InfrastructureSimulationService
case "module_arg_stor_container_m_01": case "module_arg_stor_container_m_01":
case "module_arg_stor_solid_m_01": case "module_arg_stor_solid_m_01":
case "module_arg_stor_liquid_m_01": case "module_arg_stor_liquid_m_01":
var storageClass = supportModuleId switch var storageKind = supportModuleId switch
{ {
"module_arg_stor_solid_m_01" => "solid", "module_arg_stor_solid_m_01" => StorageKind.Solid,
"module_arg_stor_liquid_m_01" => "liquid", "module_arg_stor_liquid_m_01" => StorageKind.Liquid,
_ => "container", _ => StorageKind.Container,
}; };
if (analysis.HasMissingOutputStorage if (analysis.HasMissingOutputStorage
&& (ModuleNeedsStorageClass(world, objectiveModuleId, storageClass) && (ModuleNeedsStorageClass(world, objectiveModuleId, storageKind)
|| CommodityUsesStorageClass(world, objectiveCommodityId, storageClass))) || CommodityUsesStorageClass(world, objectiveCommodityId, storageKind)))
{ {
unlockScore += 70f; unlockScore += 70f;
} }
@@ -688,7 +680,7 @@ internal sealed class InfrastructureSimulationService
return demand; return demand;
} }
private static bool ModuleNeedsStorageClass(SimulationWorld world, string moduleId, string storageClass) private static bool ModuleNeedsStorageClass(SimulationWorld world, string moduleId, StorageKind storageKind)
{ {
if (!world.ModuleRecipes.TryGetValue(moduleId, out var recipe)) if (!world.ModuleRecipes.TryGetValue(moduleId, out var recipe))
{ {
@@ -697,12 +689,12 @@ internal sealed class InfrastructureSimulationService
return recipe.Inputs.Any(input => return recipe.Inputs.Any(input =>
world.ItemDefinitions.TryGetValue(input.ItemId, out var itemDefinition) world.ItemDefinitions.TryGetValue(input.ItemId, out var itemDefinition)
&& string.Equals(itemDefinition.CargoKind, storageClass, StringComparison.Ordinal)); && itemDefinition.CargoStorageKind == storageKind);
} }
private static bool CommodityUsesStorageClass(SimulationWorld world, string commodityId, string storageClass) => private static bool CommodityUsesStorageClass(SimulationWorld world, string commodityId, StorageKind storageKind) =>
world.ItemDefinitions.TryGetValue(commodityId, out var itemDefinition) world.ItemDefinitions.TryGetValue(commodityId, out var itemDefinition)
&& string.Equals(itemDefinition.CargoKind, storageClass, StringComparison.Ordinal); && itemDefinition.CargoStorageKind == storageKind;
private static bool CanStationAcceptStationOutputSoon(SimulationWorld world, StationRuntime station, string itemId, float amount) private static bool CanStationAcceptStationOutputSoon(SimulationWorld world, StationRuntime station, string itemId, float amount)
{ {
@@ -711,14 +703,19 @@ internal sealed class InfrastructureSimulationService
return false; return false;
} }
var capacity = GetStationStorageCapacity(station, itemDefinition.CargoKind); if (itemDefinition.CargoStorageKind is not { } storageKind)
{
return false;
}
var capacity = GetStationStorageCapacity(world, station, storageKind);
if (capacity <= 0.01f) if (capacity <= 0.01f)
{ {
return false; return false;
} }
var used = station.Inventory var used = station.Inventory
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && string.Equals(definition.CargoKind, itemDefinition.CargoKind, StringComparison.Ordinal)) .Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoStorageKind == storageKind)
.Sum(entry => entry.Value); .Sum(entry => entry.Value);
return used + amount <= capacity * 0.95f; return used + amount <= capacity * 0.95f;
} }

View File

@@ -1,3 +1,4 @@
using SpaceGame.Api.Shared.Runtime;
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
namespace SpaceGame.Api.Stations.Simulation; namespace SpaceGame.Api.Stations.Simulation;
@@ -19,7 +20,7 @@ internal sealed class StationLifecycleService
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(world, station, deltaSeconds, events);
_stationSimulation.ReviewStationMarketOrders(world, station); _stationSimulation.ReviewStationMarketOrders(world, station);
_stationSimulation.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;
@@ -31,14 +32,14 @@ internal sealed class StationLifecycleService
} }
} }
private void UpdateStationPopulation(StationRuntime station, float deltaSeconds, ICollection<SimulationEventRecord> events) private void UpdateStationPopulation(SimulationWorld world, StationRuntime station, float deltaSeconds, ICollection<SimulationEventRecord> events)
{ {
station.WorkforceRequired = MathF.Max(12f, station.Modules.Count * 14f); station.WorkforceRequired = MathF.Max(12f, station.Modules.Count * 14f);
var requiredWater = station.Population * WaterConsumptionPerWorkerPerSecond * deltaSeconds; var requiredWater = station.Population * WaterConsumptionPerWorkerPerSecond * deltaSeconds;
var consumedWater = RemoveInventory(station.Inventory, "water", requiredWater); var consumedWater = RemoveInventory(station.Inventory, "water", requiredWater);
var waterSatisfied = requiredWater <= 0.01f || consumedWater + 0.001f >= requiredWater; var waterSatisfied = requiredWater <= 0.01f || consumedWater + 0.001f >= requiredWater;
var habitatModules = CountModules(station.InstalledModules, "module_arg_hab_m_01"); var habitatModules = CountStationModules(station, ModuleType.Habitation);
station.PopulationCapacity = 40f + (habitatModules * 220f); station.PopulationCapacity = 40f + (habitatModules * 220f);
if (waterSatisfied) if (waterSatisfied)
@@ -101,7 +102,7 @@ internal sealed class StationLifecycleService
CurrentCelestialId = station.CelestialId, CurrentCelestialId = station.CelestialId,
LocalPosition = position, LocalPosition = position,
SystemPosition = position, SystemPosition = position,
MovementRegime = MovementRegimeKinds.LocalFlight, MovementRegime = MovementRegimeKind.LocalFlight,
}; };
private static DefaultBehaviorRuntime CreateSpawnedShipBehavior(ShipDefinition definition, StationRuntime station) private static DefaultBehaviorRuntime CreateSpawnedShipBehavior(ShipDefinition definition, StationRuntime station)

View File

@@ -1,5 +1,6 @@
using static SpaceGame.Api.Factions.AI.CommanderPlanningService; using static SpaceGame.Api.Factions.AI.CommanderPlanningService;
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
using SpaceGame.Api.Shared.Runtime;
namespace SpaceGame.Api.Stations.Simulation; namespace SpaceGame.Api.Stations.Simulation;
@@ -54,14 +55,14 @@ internal sealed class StationSimulationService
_ => 0f, _ => 0f,
}; };
var oreReserve = role == "refinery" ? 260f : 0f; var oreReserve = role == "refinery" ? 260f : 0f;
var hullpartsReserve = MathF.Max(constructionHullpartsReserve, HasStationModules(station, "module_gen_build_l_01") ? 120f : 0f); var hullpartsReserve = MathF.Max(constructionHullpartsReserve, HasShipyardCapability(station) ? 120f : 0f);
var claytronicsReserve = MathF.Max(constructionClayReserve, HasStationModules(station, "module_gen_build_l_01") ? 120f : 0f); var claytronicsReserve = MathF.Max(constructionClayReserve, HasShipyardCapability(station) ? 120f : 0f);
var grapheneReserve = role == "graphene" ? 120f : 0f; var grapheneReserve = role == "graphene" ? 120f : 0f;
var siliconWafersReserve = role == "siliconwafers" ? 120f : 0f; var siliconWafersReserve = role == "siliconwafers" ? 120f : 0f;
var antimatterCellsReserve = role == "antimattercells" ? 120f : 0f; var antimatterCellsReserve = role == "antimattercells" ? 120f : 0f;
var superfluidCoolantReserve = role == "superfluidcoolant" ? 120f : 0f; var superfluidCoolantReserve = role == "superfluidcoolant" ? 120f : 0f;
var quantumTubesReserve = role == "quantumtubes" ? 120f : 0f; var quantumTubesReserve = role == "quantumtubes" ? 120f : 0f;
var shipPartsReserve = HasStationModules(station, "module_gen_build_l_01") var shipPartsReserve = HasShipyardCapability(station)
&& GetShipProductionPressure(world, station.FactionId, "military") > 0.2f && GetShipProductionPressure(world, station.FactionId, "military") > 0.2f
? 90f ? 90f
: 0f; : 0f;
@@ -116,7 +117,7 @@ internal sealed class StationSimulationService
var constructionHullpartsReserve = GetConstructionDemandForItem(world, site, "hullparts"); var constructionHullpartsReserve = GetConstructionDemandForItem(world, site, "hullparts");
var constructionClayReserve = GetConstructionDemandForItem(world, site, "claytronics"); var constructionClayReserve = GetConstructionDemandForItem(world, site, "claytronics");
var constructionRefinedReserve = GetConstructionDemandForItem(world, site, "refinedmetals"); var constructionRefinedReserve = GetConstructionDemandForItem(world, site, "refinedmetals");
var shipPartsReserve = HasStationModules(station, "module_gen_build_l_01") var shipPartsReserve = HasShipyardCapability(station)
&& GetShipProductionPressure(world, station.FactionId, "military") > 0.2f && GetShipProductionPressure(world, station.FactionId, "military") > 0.2f
? 90f ? 90f
: 0f; : 0f;
@@ -151,8 +152,8 @@ internal sealed class StationSimulationService
"refinery" => 80f, "refinery" => 80f,
_ => 0f, _ => 0f,
}, constructionRefinedReserve), }, constructionRefinedReserve),
"hullparts" => MathF.Max(constructionHullpartsReserve, HasStationModules(station, "module_gen_build_l_01") ? 120f : 0f) + shipPartsReserve, "hullparts" => MathF.Max(constructionHullpartsReserve, HasShipyardCapability(station) ? 120f : 0f) + shipPartsReserve,
"claytronics" => MathF.Max(constructionClayReserve, HasStationModules(station, "module_gen_build_l_01") ? 120f : 0f), "claytronics" => MathF.Max(constructionClayReserve, HasShipyardCapability(station) ? 120f : 0f),
"graphene" => MathF.Max(role == "graphene" ? 120f : 0f, role == "quantumtubes" ? 160f : 0f), "graphene" => MathF.Max(role == "graphene" ? 120f : 0f, role == "quantumtubes" ? 160f : 0f),
"siliconwafers" => role == "siliconwafers" ? 120f : 0f, "siliconwafers" => role == "siliconwafers" ? 120f : 0f,
"antimattercells" => MathF.Max(role == "antimattercells" ? 120f : 0f, role == "claytronics" ? 120f : 0f), "antimattercells" => MathF.Max(role == "antimattercells" ? 120f : 0f, role == "claytronics" ? 120f : 0f),
@@ -292,7 +293,7 @@ internal sealed class StationSimulationService
if (outputItemIds.Contains("hullparts")) if (outputItemIds.Contains("hullparts"))
{ {
return HasStationModules(station, "module_gen_prod_advancedelectronics_01", "module_gen_build_l_01") return HasShipyardCapability(station) && HasStationModules(station, "module_gen_prod_advancedelectronics_01")
? -140f * MathF.Max(expansionPressure, fleetPressure) ? -140f * MathF.Max(expansionPressure, fleetPressure)
: 280f * MathF.Max(expansionPressure, fleetPressure); : 280f * MathF.Max(expansionPressure, fleetPressure);
} }
@@ -407,20 +408,25 @@ internal sealed class StationSimulationService
return false; return false;
} }
var requiredModule = GetStorageRequirement(itemDefinition.CargoKind); var storageKind = itemDefinition.CargoStorageKind;
if (requiredModule is not null && !station.InstalledModules.Contains(requiredModule, StringComparer.Ordinal)) if (storageKind is null)
{ {
return false; return false;
} }
var capacity = GetStationStorageCapacity(station, itemDefinition.CargoKind); if (!HasStorageCapacity(world, station, storageKind.Value))
{
return false;
}
var capacity = GetStationStorageCapacity(world, station, storageKind.Value);
if (capacity <= 0.01f) if (capacity <= 0.01f)
{ {
return false; return false;
} }
var used = station.Inventory var used = station.Inventory
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoKind == itemDefinition.CargoKind) .Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoStorageKind == storageKind)
.Sum(entry => entry.Value); .Sum(entry => entry.Value);
return used + amount <= capacity + 0.001f; return used + amount <= capacity + 0.001f;
} }
@@ -455,7 +461,7 @@ internal sealed class StationSimulationService
return objective; return objective;
} }
if (HasStationModules(station, "module_gen_build_l_01")) if (HasShipyardCapability(station))
{ {
return "shipyard"; return "shipyard";
} }
@@ -513,6 +519,9 @@ internal sealed class StationSimulationService
return "general"; return "general";
} }
private static bool HasShipyardCapability(StationRuntime station) =>
CountStationModules(station, ModuleType.BuildModule) > 0;
private static float GetConstructionDemandForItem(SimulationWorld world, ConstructionSiteRuntime? site, string itemId) private static float GetConstructionDemandForItem(SimulationWorld world, ConstructionSiteRuntime? site, string itemId)
{ {
if (site is null || !site.RequiredItems.TryGetValue(itemId, out var required)) if (site is null || !site.RequiredItems.TryGetValue(itemId, out var required))

View File

@@ -55,14 +55,14 @@ public sealed class ShipSpatialStateRuntime
public string? CurrentCelestialId { get; set; } public string? CurrentCelestialId { get; set; }
public Vector3? LocalPosition { get; set; } public Vector3? LocalPosition { get; set; }
public Vector3? SystemPosition { get; set; } public Vector3? SystemPosition { get; set; }
public string MovementRegime { get; set; } = MovementRegimeKinds.LocalFlight; public MovementRegimeKind MovementRegime { get; set; } = MovementRegimeKind.LocalFlight;
public string? DestinationNodeId { get; set; } public string? DestinationNodeId { get; set; }
public ShipTransitRuntime? Transit { get; set; } public ShipTransitRuntime? Transit { get; set; }
} }
public sealed class ShipTransitRuntime public sealed class ShipTransitRuntime
{ {
public required string Regime { get; init; } public required MovementRegimeKind Regime { get; init; }
public string? OriginNodeId { get; init; } public string? OriginNodeId { get; init; }
public string? DestinationNodeId { get; init; } public string? DestinationNodeId { get; init; }
public DateTimeOffset? StartedAtUtc { get; set; } public DateTimeOffset? StartedAtUtc { get; set; }

View File

@@ -1,4 +1,5 @@
using System.Text.Json; using System.Text.Json;
using SpaceGame.Api.Shared.Runtime;
using static SpaceGame.Api.Universe.Scenario.LoaderSupport; using static SpaceGame.Api.Universe.Scenario.LoaderSupport;
namespace SpaceGame.Api.Universe.Scenario; namespace SpaceGame.Api.Universe.Scenario;
@@ -15,7 +16,7 @@ internal sealed class DataCatalogLoader(string dataRoot)
var authoredSystems = Read<List<SolarSystemDefinition>>("systems.json"); var authoredSystems = Read<List<SolarSystemDefinition>>("systems.json");
var scenario = Read<ScenarioDefinition>("scenario.json"); var scenario = Read<ScenarioDefinition>("scenario.json");
var modules = NormalizeModules(Read<List<ModuleDefinition>>("modules.json")); var modules = NormalizeModules(Read<List<ModuleDefinition>>("modules.json"));
var ships = Read<List<ShipDefinition>>("ships.json"); var ships = NormalizeShips(Read<List<ShipDefinition>>("ships.json"));
var items = NormalizeItems(Read<List<ItemDefinition>>("items.json")); var items = NormalizeItems(Read<List<ItemDefinition>>("items.json"));
var balance = Read<BalanceDefinition>("balance.json"); var balance = Read<BalanceDefinition>("balance.json");
var recipes = BuildRecipes(items, ships, modules); var recipes = BuildRecipes(items, ships, modules);
@@ -262,15 +263,68 @@ internal sealed class DataCatalogLoader(string dataRoot)
{ {
item.Type = string.IsNullOrWhiteSpace(item.Group) ? "material" : item.Group; item.Type = string.IsNullOrWhiteSpace(item.Group) ? "material" : item.Group;
} }
if (string.IsNullOrWhiteSpace(item.CargoKind))
{
item.CargoStorageKind = null;
}
else
{
try
{
item.CargoStorageKind = item.CargoKind.ToStorageKind();
item.CargoKind = item.CargoStorageKind.Value.ToDataValue();
}
catch (ArgumentOutOfRangeException exception)
{
throw new InvalidOperationException($"Item '{item.Id}' has unsupported cargo kind '{item.CargoKind}'.", exception);
}
}
} }
return items; return items;
} }
private static List<ShipDefinition> NormalizeShips(List<ShipDefinition> ships)
{
foreach (var ship in ships)
{
if (string.IsNullOrWhiteSpace(ship.CargoKind))
{
ship.CargoStorageKind = null;
continue;
}
try
{
ship.CargoStorageKind = ship.CargoKind.ToStorageKind();
ship.CargoKind = ship.CargoStorageKind.Value.ToDataValue();
}
catch (ArgumentOutOfRangeException exception)
{
throw new InvalidOperationException($"Ship '{ship.Id}' has unsupported cargo kind '{ship.CargoKind}'.", exception);
}
}
return ships;
}
private static List<ModuleDefinition> NormalizeModules(List<ModuleDefinition> modules) private static List<ModuleDefinition> NormalizeModules(List<ModuleDefinition> modules)
{ {
foreach (var module in modules) for (var index = 0; index < modules.Count; index += 1)
{ {
var module = modules[index];
try
{
module.ModuleType = module.Type.ToModuleType();
}
catch (ArgumentOutOfRangeException exception)
{
throw new InvalidOperationException($"Module '{module.Id}' has unsupported type '{module.Type}'.", exception);
}
module.Type = module.ModuleType.ToDataValue();
if (module.Products.Count == 0 && !string.IsNullOrWhiteSpace(module.Product)) if (module.Products.Count == 0 && !string.IsNullOrWhiteSpace(module.Product))
{ {
module.Products = [module.Product]; module.Products = [module.Product];
@@ -278,7 +332,7 @@ internal sealed class DataCatalogLoader(string dataRoot)
if (string.IsNullOrWhiteSpace(module.ProductionMode)) if (string.IsNullOrWhiteSpace(module.ProductionMode))
{ {
module.ProductionMode = string.Equals(module.Type, "buildmodule", StringComparison.Ordinal) module.ProductionMode = module.ModuleType == ModuleType.BuildModule
? "commanded" ? "commanded"
: "passive"; : "passive";
} }
@@ -287,10 +341,39 @@ internal sealed class DataCatalogLoader(string dataRoot)
{ {
module.WorkforceNeeded = module.WorkForce?.Max ?? 0f; module.WorkforceNeeded = module.WorkForce?.Max ?? 0f;
} }
modules[index] = CreateSpecializedModuleDefinition(module);
} }
return modules; return modules;
} }
private static ModuleDefinition CreateSpecializedModuleDefinition(ModuleDefinition module)
{
if (module.ModuleType == ModuleType.Storage)
{
if (module.Cargo is null)
{
throw new InvalidOperationException($"Storage module '{module.Id}' is missing cargo metadata.");
}
try
{
return new StorageModuleDefinition(module, module.Cargo.Type.ToStorageKind(), module.Cargo.Max);
}
catch (ArgumentOutOfRangeException exception)
{
throw new InvalidOperationException($"Storage module '{module.Id}' has unsupported cargo type '{module.Cargo.Type}'.", exception);
}
}
if (module.Products.Count > 0)
{
return new ProductionModuleDefinition(module);
}
return module;
}
} }
internal sealed record ScenarioCatalog( internal sealed record ScenarioCatalog(

View File

@@ -1,4 +1,6 @@
using SpaceGame.Api.Shared.Runtime;
namespace SpaceGame.Api.Universe.Scenario; namespace SpaceGame.Api.Universe.Scenario;
internal static class LoaderSupport internal static class LoaderSupport
@@ -99,13 +101,7 @@ internal static class LoaderSupport
return; return;
} }
station.Modules.Add(new StationModuleRuntime station.Modules.Add(StationModuleRuntime.Create($"{station.Id}-module-{station.Modules.Count + 1}", definition));
{
Id = $"{station.Id}-module-{station.Modules.Count + 1}",
ModuleId = moduleId,
Health = definition.Hull,
MaxHealth = definition.Hull,
});
station.Radius = GetStationRadius(moduleDefinitions, station); station.Radius = GetStationRadius(moduleDefinitions, station);
} }

View File

@@ -300,7 +300,7 @@ internal sealed class SpatialBuilder
CurrentCelestialId = nearestCelestial?.Id, CurrentCelestialId = nearestCelestial?.Id,
LocalPosition = position, LocalPosition = position,
SystemPosition = position, SystemPosition = position,
MovementRegime = MovementRegimeKinds.LocalFlight, MovementRegime = MovementRegimeKind.LocalFlight,
}; };
} }
} }

View File

@@ -218,16 +218,12 @@ internal sealed class WorldBuilder(
continue; continue;
} }
var storageModuleId = itemDefinition.CargoKind switch if (SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport.GetStorageRequirement(moduleDefinitions, itemDefinition.CargoStorageKind) is { } storageModuleId)
{ {
"solid" => "module_arg_stor_solid_m_01",
"liquid" => "module_arg_stor_liquid_m_01",
_ => "module_arg_stor_container_m_01",
};
yield return storageModuleId; yield return storageModuleId;
} }
} }
}
private static void EnsureStartingModule(List<string> modules, string moduleId) private static void EnsureStartingModule(List<string> modules, string moduleId)
{ {

View File

@@ -241,12 +241,10 @@ internal sealed class WorldSeedingService
continue; continue;
} }
yield return itemDefinition.CargoKind switch if (SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport.GetStorageRequirement(world.ModuleDefinitions, itemDefinition.CargoStorageKind) is { } storageModuleId)
{ {
"solid" => "module_arg_stor_solid_m_01", yield return storageModuleId;
"liquid" => "module_arg_stor_liquid_m_01", }
_ => "module_arg_stor_container_m_01",
};
} }
} }
@@ -553,7 +551,7 @@ internal sealed class WorldSeedingService
private static void InitializeStationPopulation(StationRuntime station) private static void InitializeStationPopulation(StationRuntime station)
{ {
var habitatModules = CountModules(station.InstalledModules, "module_arg_hab_m_01"); var habitatModules = station.Modules.Count(module => module.ModuleType == ModuleType.Habitation);
station.PopulationCapacity = 40f + (habitatModules * 220f); station.PopulationCapacity = 40f + (habitatModules * 220f);
station.WorkforceRequired = MathF.Max(12f, station.Modules.Count * 14f); station.WorkforceRequired = MathF.Max(12f, station.Modules.Count * 14f);
station.Population = habitatModules > 0 station.Population = habitatModules > 0

View File

@@ -269,7 +269,7 @@ internal sealed class OrbitalStateUpdater
} }
ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace; ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace;
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight; ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight;
var nearestCelestial = world.Celestials var nearestCelestial = world.Celestials
.Where(candidate => candidate.SystemId == ship.SystemId) .Where(candidate => candidate.SystemId == ship.SystemId)
.OrderBy(candidate => candidate.Position.DistanceTo(ship.Position)) .OrderBy(candidate => candidate.Position.DistanceTo(ship.Position))