Refactor station modules into typed runtime models

This commit is contained in:
2026-03-27 14:59:15 -04:00
parent f961ac62b6
commit e8fb033a01
13 changed files with 408 additions and 169 deletions

View File

@@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using SpaceGame.Api.Shared.Runtime; using SpaceGame.Api.Shared.Runtime;
@@ -115,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;
@@ -190,8 +193,41 @@ 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;
@@ -226,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; }
@@ -277,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

@@ -115,6 +115,14 @@ public enum ModuleType
Storage, Storage,
} }
public enum StorageKind
{
Condensate,
Container,
Liquid,
Solid,
}
public static class CommanderKind public static class CommanderKind
{ {
public const string Faction = "faction"; public const string Faction = "faction";
@@ -209,7 +217,14 @@ public static class SimulationEnumMappings
_ => throw new ArgumentOutOfRangeException(nameof(moduleType), moduleType, null), _ => throw new ArgumentOutOfRangeException(nameof(moduleType), moduleType, null),
}; };
public static ModuleType ToModuleType(this string value) => value.Trim() switch 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, "buildmodule" => ModuleType.BuildModule,
"connectionmodule" => ModuleType.ConnectionModule, "connectionmodule" => ModuleType.ConnectionModule,
@@ -222,6 +237,33 @@ public static class SimulationEnumMappings
"storage" => ModuleType.Storage, "storage" => ModuleType.Storage,
_ => throw new ArgumentOutOfRangeException(nameof(value), value, "Unsupported module type."), _ => 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
{ {

View File

@@ -6,13 +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 int CountStationModules(SimulationWorld world, StationRuntime station, ModuleType moduleType) => internal static float GetStationStorageCapacity(SimulationWorld world, StationRuntime station, StorageKind storageKind)
station.Modules.Count(module => {
world.ModuleDefinitions.TryGetValue(module.ModuleId, out var definition) SyncStorageModuleLevels(world, station, storageKind);
&& definition.ModuleType == moduleType); 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)
{ {
@@ -21,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);
} }
@@ -39,41 +75,9 @@ 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));
internal static int CountModules(
IEnumerable<string> modules,
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
ModuleType moduleType) =>
modules.Count(moduleId =>
moduleDefinitions.TryGetValue(moduleId, out var definition)
&& definition.ModuleType == moduleType);
internal static float GetInventoryAmount(IReadOnlyDictionary<string, float> inventory, string itemId) => internal static float GetInventoryAmount(IReadOnlyDictionary<string, float> inventory, string itemId) =>
inventory.TryGetValue(itemId, out var amount) ? amount : 0f; inventory.TryGetValue(itemId, out var amount) ? amount : 0f;
@@ -110,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);
@@ -126,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)
{ {
@@ -141,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

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

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

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

@@ -39,7 +39,7 @@ internal sealed class StationLifecycleService
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, world.ModuleDefinitions, ModuleType.Habitation); var habitatModules = CountStationModules(station, ModuleType.Habitation);
station.PopulationCapacity = 40f + (habitatModules * 220f); station.PopulationCapacity = 40f + (habitatModules * 220f);
if (waterSatisfied) if (waterSatisfied)

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

@@ -16,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);
@@ -263,15 +263,57 @@ 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 try
{ {
module.ModuleType = module.Type.ToModuleType(); module.ModuleType = module.Type.ToModuleType();
@@ -299,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

@@ -101,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);
} }
@@ -126,14 +120,6 @@ internal static class LoaderSupport
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));
internal static int CountModules(
IEnumerable<string> modules,
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
ModuleType moduleType) =>
modules.Count(moduleId =>
moduleDefinitions.TryGetValue(moduleId, out var definition)
&& definition.ModuleType == moduleType);
internal static float ComputeWorkforceRatio(float population, float workforceRequired) internal static float ComputeWorkforceRatio(float population, float workforceRequired)
{ {
if (workforceRequired <= 0.01f) if (workforceRequired <= 0.01f)

View File

@@ -38,7 +38,7 @@ internal sealed class WorldBuilder(
catalog.ModuleDefinitions, catalog.ModuleDefinitions,
catalog.ItemDefinitions); catalog.ItemDefinitions);
seedingService.InitializeStationStockpiles(stations, catalog.ModuleDefinitions); seedingService.InitializeStationStockpiles(stations);
var refinery = seedingService.SelectRefineryStation(stations, scenario); var refinery = seedingService.SelectRefineryStation(stations, scenario);
var patrolRoutes = BuildPatrolRoutes(scenario, systemsById); var patrolRoutes = BuildPatrolRoutes(scenario, systemsById);
var ships = CreateShips(scenario, systemsById, spatialLayout.Celestials, catalog.Balance, catalog.ShipDefinitions, patrolRoutes, stations, refinery); var ships = CreateShips(scenario, systemsById, spatialLayout.Celestials, catalog.Balance, catalog.ShipDefinitions, patrolRoutes, stations, refinery);
@@ -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

@@ -1,4 +1,3 @@
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;
@@ -61,13 +60,11 @@ internal sealed class WorldSeedingService
} }
} }
internal void InitializeStationStockpiles( internal void InitializeStationStockpiles(IReadOnlyCollection<StationRuntime> stations)
IReadOnlyCollection<StationRuntime> stations,
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions)
{ {
foreach (var station in stations) foreach (var station in stations)
{ {
InitializeStationPopulation(station, moduleDefinitions); InitializeStationPopulation(station);
} }
} }
@@ -244,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",
};
} }
} }
@@ -554,11 +549,9 @@ internal sealed class WorldSeedingService
}; };
} }
private static void InitializeStationPopulation( private static void InitializeStationPopulation(StationRuntime station)
StationRuntime station,
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions)
{ {
var habitatModules = CountModules(station.InstalledModules, moduleDefinitions, ModuleType.Habitation); 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