Compare commits

...

2 Commits

13 changed files with 160 additions and 164 deletions

View File

@@ -114,10 +114,8 @@ public sealed class ItemDefinition
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 string Type { get; set; } = "material";
public string CargoKind { get; set; } = string.Empty;
[JsonIgnore] [JsonIgnore]
public StorageKind? CargoStorageKind { get; set; } public StorageKind? CargoKind { 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;
@@ -130,7 +128,8 @@ public sealed class ItemDefinition
[JsonPropertyName("transport")] [JsonPropertyName("transport")]
public string Transport public string Transport
{ {
set => CargoKind = value; get => CargoKind?.ToDataValue() ?? string.Empty;
set => CargoKind = value.ToNullableStorageKind();
} }
} }
@@ -151,12 +150,6 @@ public sealed class RecipeInputDefinition
} }
} }
public sealed class ModuleConstructionDefinition
{
public required List<RecipeInputDefinition> Requirements { get; set; }
public float ProductionTime { get; set; }
}
public sealed class ModuleDockDefinition public sealed class ModuleDockDefinition
{ {
public int Capacity { get; set; } public int Capacity { get; set; }
@@ -169,10 +162,14 @@ public sealed class ModuleCargoDefinition
public required string Type { get; set; } public required string Type { get; set; }
} }
public sealed class ModuleWorkForceDefinition public sealed class ModuleWorkforceDefinition
{ {
public float Capacity { get; set; } [JsonPropertyName("capacity")]
public float Max { get; set; } public float SupportedPopulation { get; set; }
[JsonPropertyName("max")]
public float RequiredWorkforce { get; set; }
public string Race { get; set; } = string.Empty; public string Race { get; set; } = string.Empty;
} }
@@ -184,7 +181,7 @@ public sealed class ModuleMountDefinition
public List<string> Types { get; set; } = []; public List<string> Types { get; set; } = [];
} }
public sealed class ModuleProductionDefinition public sealed class ModuleBuildRecipeDefinition
{ {
public float Time { get; set; } public float Time { get; set; }
public float Amount { get; set; } public float Amount { get; set; }
@@ -207,12 +204,9 @@ public class ModuleDefinition
Description = source.Description; Description = source.Description;
Type = source.Type; Type = source.Type;
ModuleType = source.ModuleType; ModuleType = source.ModuleType;
Product = source.Product; ProductIds = [.. source.ProductIds];
Products = [.. source.Products];
ProductionMode = source.ProductionMode;
Radius = source.Radius; Radius = source.Radius;
Hull = source.Hull; Hull = source.Hull;
WorkforceNeeded = source.WorkforceNeeded;
Version = source.Version; Version = source.Version;
Macro = source.Macro; Macro = source.Macro;
MakerRace = source.MakerRace; MakerRace = source.MakerRace;
@@ -220,12 +214,11 @@ public class ModuleDefinition
Price = source.Price; Price = source.Price;
Owners = [.. source.Owners]; Owners = [.. source.Owners];
Cargo = source.Cargo; Cargo = source.Cargo;
WorkForce = source.WorkForce; SerializedWorkforce = source.SerializedWorkforce;
Docks = [.. source.Docks]; Docks = [.. source.Docks];
Shields = [.. source.Shields]; Shields = [.. source.Shields];
Turrets = [.. source.Turrets]; Turrets = [.. source.Turrets];
Production = [.. source.Production]; BuildRecipes = [.. source.BuildRecipes];
Construction = source.Construction;
} }
public required string Id { get; set; } public required string Id { get; set; }
@@ -234,13 +227,12 @@ public class ModuleDefinition
public required string Type { get; set; } public required string Type { get; set; }
[JsonIgnore] [JsonIgnore]
public ModuleType ModuleType { get; set; } public ModuleType ModuleType { get; set; }
[JsonPropertyName("product")]
public List<string> ProductIds { get; set; } = [];
[JsonIgnore] [JsonIgnore]
public string? Product { get; set; } public virtual IReadOnlyList<string> ProductItemIds => [];
public List<string> Products { get; set; } = [];
public string ProductionMode { get; set; } = "passive";
public float Radius { get; set; } = 12f; public float Radius { get; set; } = 12f;
public float Hull { get; set; } = 100f; public float Hull { get; set; } = 100f;
public float WorkforceNeeded { get; set; }
public int Version { get; set; } public int Version { get; set; }
public string Macro { get; set; } = string.Empty; public string Macro { get; set; } = string.Empty;
public string MakerRace { get; set; } = string.Empty; public string MakerRace { get; set; } = string.Empty;
@@ -248,30 +240,58 @@ public class ModuleDefinition
public ItemPriceDefinition? Price { get; set; } public ItemPriceDefinition? Price { get; set; }
public List<string> Owners { get; set; } = []; public List<string> Owners { get; set; } = [];
public ModuleCargoDefinition? Cargo { get; set; } public ModuleCargoDefinition? Cargo { get; set; }
public ModuleWorkForceDefinition? WorkForce { get; set; } [JsonPropertyName("workForce")]
public ModuleWorkforceDefinition? SerializedWorkforce { get; set; }
public List<ModuleDockDefinition> Docks { get; set; } = []; public List<ModuleDockDefinition> Docks { get; set; } = [];
public List<ModuleMountDefinition> Shields { get; set; } = []; public List<ModuleMountDefinition> Shields { get; set; } = [];
public List<ModuleMountDefinition> Turrets { get; set; } = []; public List<ModuleMountDefinition> Turrets { get; set; } = [];
public List<ModuleProductionDefinition> Production { get; set; } = []; [JsonPropertyName("production")]
public ModuleConstructionDefinition? Construction { get; set; } public List<ModuleBuildRecipeDefinition> BuildRecipes { get; set; } = [];
[JsonPropertyName("product")] }
public List<string> ProductIds
public abstract class ProductionLaneModuleDefinition : ModuleDefinition
{
[SetsRequiredMembers]
protected ProductionLaneModuleDefinition(ModuleDefinition source, float requiredWorkforce)
: base(source)
{
RequiredWorkforce = requiredWorkforce;
}
public float RequiredWorkforce { get; init; }
}
public sealed class ProductionModuleDefinition : ProductionLaneModuleDefinition
{
[SetsRequiredMembers]
internal ProductionModuleDefinition(ModuleDefinition source, float requiredWorkforce)
: base(source, requiredWorkforce)
{
ProductItemIds = [.. source.ProductIds];
}
public override IReadOnlyList<string> ProductItemIds { get; } = [];
}
public sealed class BuildModuleDefinition : ProductionLaneModuleDefinition
{
[SetsRequiredMembers]
internal BuildModuleDefinition(ModuleDefinition source, float requiredWorkforce)
: base(source, requiredWorkforce)
{ {
get => Products;
set => Products = value ?? [];
} }
} }
public sealed class ProductionModuleDefinition : ModuleDefinition public sealed class HabitationModuleDefinition : ModuleDefinition
{ {
[SetsRequiredMembers] [SetsRequiredMembers]
internal ProductionModuleDefinition(ModuleDefinition source) internal HabitationModuleDefinition(ModuleDefinition source, float supportedPopulation)
: base(source) : base(source)
{ {
ProductItemIds = [.. source.Products]; SupportedPopulation = supportedPopulation;
} }
public IReadOnlyList<string> ProductItemIds { get; init; } = []; public float SupportedPopulation { get; init; }
} }
public sealed class StorageModuleDefinition : ModuleDefinition public sealed class StorageModuleDefinition : ModuleDefinition
@@ -338,9 +358,14 @@ public sealed class ShipDefinition
public float FtlSpeed { get; set; } public float FtlSpeed { get; set; }
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; }
[JsonIgnore] [JsonIgnore]
public StorageKind? CargoStorageKind { get; set; } public StorageKind? CargoKind { get; set; }
[JsonPropertyName("cargoKind")]
public string? SerializedCargoKind
{
get => CargoKind?.ToDataValue();
set => CargoKind = value.ToNullableStorageKind();
}
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

@@ -14,7 +14,7 @@ internal static class ProductionGraphBuilder
ItemId = item.Id, ItemId = item.Id,
Name = item.Name, Name = item.Name,
Group = item.Group, Group = item.Group,
CargoKind = item.CargoKind, CargoKind = item.CargoKind?.ToDataValue() ?? string.Empty,
}, },
StringComparer.Ordinal); StringComparer.Ordinal);
@@ -87,7 +87,7 @@ internal static class ProductionGraphBuilder
outputsByModuleId[module.Id] = outputs; outputsByModuleId[module.Id] = outputs;
} }
foreach (var product in module.Products) foreach (var product in module.ProductItemIds)
{ {
outputs.Add(product); outputs.Add(product);
} }

View File

@@ -265,6 +265,9 @@ public static class SimulationEnumMappings
}; };
} }
public static StorageKind? ToNullableStorageKind(this string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value.ToStorageKind();
public static string ToContractValue(this SpatialNodeKind kind) => kind switch public static string ToContractValue(this SpatialNodeKind kind) => kind switch
{ {
SpatialNodeKind.Star => "star", SpatialNodeKind.Star => "star",

View File

@@ -9,6 +9,25 @@ internal static class SimulationRuntimeSupport
internal static int CountStationModules(StationRuntime station, ModuleType moduleType) => internal static int CountStationModules(StationRuntime station, ModuleType moduleType) =>
station.Modules.Count(module => module.ModuleType == moduleType); station.Modules.Count(module => module.ModuleType == moduleType);
internal static float GetStationSupportedPopulation(
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
StationRuntime station) =>
40f + station.Modules
.Select(module => moduleDefinitions.TryGetValue(module.ModuleId, out var definition) && definition is HabitationModuleDefinition habitation
? habitation.SupportedPopulation
: 0f)
.Sum();
internal static float GetStationRequiredWorkforce(
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
StationRuntime station) =>
MathF.Max(12f, station.Modules
.Select(module => moduleDefinitions.TryGetValue(module.ModuleId, out var definition)
&& definition is ProductionLaneModuleDefinition productionLane
? productionLane.RequiredWorkforce
: 0f)
.Sum());
internal static float GetStationStorageCapacity(SimulationWorld world, StationRuntime station, StorageKind storageKind) internal static float GetStationStorageCapacity(SimulationWorld world, StationRuntime station, StorageKind storageKind)
{ {
SyncStorageModuleLevels(world, station, storageKind); SyncStorageModuleLevels(world, station, storageKind);
@@ -46,7 +65,7 @@ internal static class SimulationRuntimeSupport
} }
var remaining = station.Inventory var remaining = station.Inventory
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoStorageKind == storageKind) .Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoKind == storageKind)
.Sum(entry => entry.Value); .Sum(entry => entry.Value);
foreach (var (module, definition) in storageModules) foreach (var (module, definition) in storageModules)
@@ -114,8 +133,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)
&& item.CargoStorageKind is not null && item.CargoKind is not null
&& item.CargoStorageKind == ship.Definition.CargoStorageKind; && item.CargoKind == ship.Definition.CargoKind;
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);
@@ -176,7 +195,7 @@ internal static class SimulationRuntimeSupport
return 0f; return 0f;
} }
var storageKind = itemDefinition.CargoStorageKind; var storageKind = itemDefinition.CargoKind;
if (storageKind is null) if (storageKind is null)
{ {
return 0f; return 0f;
@@ -194,7 +213,7 @@ internal static class SimulationRuntimeSupport
} }
var used = station.Inventory var used = station.Inventory
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoStorageKind == storageKind) .Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoKind == 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,7 +2619,7 @@ internal sealed class ShipAiService
{ {
if (world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition)) if (world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition))
{ {
var storageModule = GetStorageRequirement(world.ModuleDefinitions, itemDefinition.CargoStorageKind); var storageModule = GetStorageRequirement(world.ModuleDefinitions, itemDefinition.CargoKind);
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);

View File

@@ -801,7 +801,7 @@ internal sealed class SimulationProjectionService
.Select(storageKind => new StationStorageUsageSnapshot( .Select(storageKind => new StationStorageUsageSnapshot(
storageKind.ToDataValue(), storageKind.ToDataValue(),
station.Inventory station.Inventory
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoStorageKind == storageKind) .Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoKind == storageKind)
.Sum(entry => entry.Value), .Sum(entry => entry.Value),
GetStationStorageCapacity(world, station, storageKind))) GetStationStorageCapacity(world, station, storageKind)))
.Where(snapshot => snapshot.Capacity > 0.01f) .Where(snapshot => snapshot.Capacity > 0.01f)

View File

@@ -65,6 +65,14 @@ public class StationModuleRuntime
Health = production.Hull, Health = production.Hull,
MaxHealth = production.Hull, MaxHealth = production.Hull,
}, },
BuildModuleDefinition build => new BuildStationModuleRuntime
{
Id = id,
ModuleId = build.Id,
ModuleType = build.ModuleType,
Health = build.Hull,
MaxHealth = build.Hull,
},
_ => new StationModuleRuntime _ => new StationModuleRuntime
{ {
Id = id, Id = id,
@@ -81,6 +89,10 @@ public sealed class ProductionStationModuleRuntime : StationModuleRuntime
public IReadOnlyList<string> ProductItemIds { get; init; } = []; public IReadOnlyList<string> ProductItemIds { get; init; } = [];
} }
public sealed class BuildStationModuleRuntime : StationModuleRuntime
{
}
public sealed class StorageStationModuleRuntime : StationModuleRuntime public sealed class StorageStationModuleRuntime : StationModuleRuntime
{ {
public StorageKind StorageKind { get; init; } public StorageKind StorageKind { get; init; }

View File

@@ -176,7 +176,7 @@ internal sealed class InfrastructureSimulationService
} }
var used = station.Inventory var used = station.Inventory
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var def) && def.CargoStorageKind == storageKind) .Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var def) && def.CargoKind == storageKind)
.Sum(entry => entry.Value); .Sum(entry => entry.Value);
if (used / capacity >= 0.65f) if (used / capacity >= 0.65f)
{ {
@@ -195,7 +195,7 @@ internal sealed class InfrastructureSimulationService
continue; continue;
} }
if (GetStorageRequirement(world.ModuleDefinitions, itemDefinition.CargoStorageKind) is { } storageModuleId) if (GetStorageRequirement(world.ModuleDefinitions, itemDefinition.CargoKind) is { } storageModuleId)
{ {
yield return storageModuleId; yield return storageModuleId;
} }
@@ -203,14 +203,14 @@ internal sealed class InfrastructureSimulationService
if (world.ModuleDefinitions.TryGetValue(recipe.ModuleId, out var moduleDefinition)) if (world.ModuleDefinitions.TryGetValue(recipe.ModuleId, out var moduleDefinition))
{ {
foreach (var productItemId in moduleDefinition.Products) foreach (var productItemId in moduleDefinition.ProductItemIds)
{ {
if (!world.ItemDefinitions.TryGetValue(productItemId, out var itemDefinition)) if (!world.ItemDefinitions.TryGetValue(productItemId, out var itemDefinition))
{ {
continue; continue;
} }
if (GetStorageRequirement(world.ModuleDefinitions, itemDefinition.CargoStorageKind) is { } storageModuleId) if (GetStorageRequirement(world.ModuleDefinitions, itemDefinition.CargoKind) is { } storageModuleId)
{ {
yield return storageModuleId; yield return storageModuleId;
} }
@@ -325,7 +325,7 @@ internal sealed class InfrastructureSimulationService
var capacity = GetStationStorageCapacity(world, station, storageKind); 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.CargoStorageKind == storageKind) .Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var def) && def.CargoKind == 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;
@@ -621,7 +621,7 @@ internal sealed class InfrastructureSimulationService
} }
var score = 0f; var score = 0f;
foreach (var productItemId in moduleDefinition.Products) foreach (var productItemId in moduleDefinition.ProductItemIds)
{ {
if (!constructionDemandByItem.TryGetValue(productItemId, out var outstandingDemand) || outstandingDemand <= 0.01f) if (!constructionDemandByItem.TryGetValue(productItemId, out var outstandingDemand) || outstandingDemand <= 0.01f)
{ {
@@ -689,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)
&& itemDefinition.CargoStorageKind == storageKind); && itemDefinition.CargoKind == storageKind);
} }
private static bool CommodityUsesStorageClass(SimulationWorld world, string commodityId, StorageKind storageKind) => private static bool CommodityUsesStorageClass(SimulationWorld world, string commodityId, StorageKind storageKind) =>
world.ItemDefinitions.TryGetValue(commodityId, out var itemDefinition) world.ItemDefinitions.TryGetValue(commodityId, out var itemDefinition)
&& itemDefinition.CargoStorageKind == storageKind; && itemDefinition.CargoKind == 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)
{ {
@@ -703,7 +703,7 @@ internal sealed class InfrastructureSimulationService
return false; return false;
} }
if (itemDefinition.CargoStorageKind is not { } storageKind) if (itemDefinition.CargoKind is not { } storageKind)
{ {
return false; return false;
} }
@@ -715,7 +715,7 @@ internal sealed class InfrastructureSimulationService
} }
var used = station.Inventory var used = station.Inventory
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoStorageKind == storageKind) .Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoKind == storageKind)
.Sum(entry => entry.Value); .Sum(entry => entry.Value);
return used + amount <= capacity * 0.95f; return used + amount <= capacity * 0.95f;
} }

View File

@@ -34,17 +34,16 @@ internal sealed class StationLifecycleService
private void UpdateStationPopulation(SimulationWorld world, 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 = GetStationRequiredWorkforce(world.ModuleDefinitions, station);
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 = CountStationModules(station, ModuleType.Habitation); station.PopulationCapacity = GetStationSupportedPopulation(world.ModuleDefinitions, station);
station.PopulationCapacity = 40f + (habitatModules * 220f);
if (waterSatisfied) if (waterSatisfied)
{ {
if (habitatModules > 0 && station.Population < station.PopulationCapacity) if (station.PopulationCapacity > 40f && station.Population < station.PopulationCapacity)
{ {
station.Population = MathF.Min(station.PopulationCapacity, station.Population + (PopulationGrowthPerSecond * deltaSeconds)); station.Population = MathF.Min(station.PopulationCapacity, station.Population + (PopulationGrowthPerSecond * deltaSeconds));
} }

View File

@@ -216,12 +216,8 @@ internal sealed class StationSimulationService
{ {
foreach (var moduleId in station.InstalledModules.Distinct(StringComparer.Ordinal)) foreach (var moduleId in station.InstalledModules.Distinct(StringComparer.Ordinal))
{ {
if (!world.ModuleDefinitions.TryGetValue(moduleId, out var def) || string.IsNullOrEmpty(def.ProductionMode)) if (!world.ModuleDefinitions.TryGetValue(moduleId, out var definition)
{ || definition is not ProductionLaneModuleDefinition)
continue;
}
if (string.Equals(def.ProductionMode, "commanded", StringComparison.Ordinal) && station.CommanderId is null)
{ {
continue; continue;
} }
@@ -241,7 +237,7 @@ internal sealed class StationSimulationService
internal static string? GetStationProductionLaneKey(SimulationWorld world, RecipeDefinition recipe) => internal static string? GetStationProductionLaneKey(SimulationWorld world, RecipeDefinition recipe) =>
recipe.RequiredModules.FirstOrDefault(moduleId => recipe.RequiredModules.FirstOrDefault(moduleId =>
world.ModuleDefinitions.TryGetValue(moduleId, out var def) && !string.IsNullOrEmpty(def.ProductionMode)); world.ModuleDefinitions.TryGetValue(moduleId, out var definition) && definition is ProductionLaneModuleDefinition);
internal static float GetStationProductionThroughput(SimulationWorld world, StationRuntime station, RecipeDefinition recipe) internal static float GetStationProductionThroughput(SimulationWorld world, StationRuntime station, RecipeDefinition recipe)
{ {
@@ -408,7 +404,7 @@ internal sealed class StationSimulationService
return false; return false;
} }
var storageKind = itemDefinition.CargoStorageKind; var storageKind = itemDefinition.CargoKind;
if (storageKind is null) if (storageKind is null)
{ {
return false; return false;
@@ -426,7 +422,7 @@ internal sealed class StationSimulationService
} }
var used = station.Inventory var used = station.Inventory
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoStorageKind == storageKind) .Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoKind == storageKind)
.Sum(entry => entry.Value); .Sum(entry => entry.Value);
return used + amount <= capacity + 0.001f; return used + amount <= capacity + 0.001f;
} }

View File

@@ -16,8 +16,8 @@ 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 = NormalizeShips(Read<List<ShipDefinition>>("ships.json")); var ships = Read<List<ShipDefinition>>("ships.json");
var items = NormalizeItems(Read<List<ItemDefinition>>("items.json")); var items = 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);
var moduleRecipes = BuildModuleRecipes(modules); var moduleRecipes = BuildModuleRecipes(modules);
@@ -103,12 +103,12 @@ internal sealed class DataCatalogLoader(string dataRoot)
private static List<ModuleRecipeDefinition> BuildModuleRecipes(IEnumerable<ModuleDefinition> modules) => private static List<ModuleRecipeDefinition> BuildModuleRecipes(IEnumerable<ModuleDefinition> modules) =>
modules modules
.Where(module => module.Construction is not null || module.Production.Count > 0) .Where(module => module.BuildRecipes.Count > 0)
.Select(module => new ModuleRecipeDefinition .Select(module => new ModuleRecipeDefinition
{ {
ModuleId = module.Id, ModuleId = module.Id,
Duration = module.Construction?.ProductionTime ?? module.Production[0].Time, Duration = module.BuildRecipes[0].Time,
Inputs = (module.Construction?.Requirements ?? module.Production[0].Wares) Inputs = module.BuildRecipes[0].Wares
.Select(input => new RecipeInputDefinition .Select(input => new RecipeInputDefinition
{ {
ItemId = input.ItemId, ItemId = input.ItemId,
@@ -122,8 +122,8 @@ internal sealed class DataCatalogLoader(string dataRoot)
{ {
var recipes = new List<RecipeDefinition>(); var recipes = new List<RecipeDefinition>();
var preferredProducerByItemId = modules var preferredProducerByItemId = modules
.Where(module => module.Products.Count > 0) .Where(module => module.ProductItemIds.Count > 0)
.GroupBy(module => module.Products[0], StringComparer.Ordinal) .GroupBy(module => module.ProductItemIds[0], StringComparer.Ordinal)
.ToDictionary( .ToDictionary(
group => group.Key, group => group.Key,
group => group.OrderBy(module => module.Id, StringComparer.Ordinal).First().Id, group => group.OrderBy(module => module.Id, StringComparer.Ordinal).First().Id,
@@ -255,60 +255,6 @@ internal sealed class DataCatalogLoader(string dataRoot)
_ => 60, _ => 60,
}; };
private static List<ItemDefinition> NormalizeItems(List<ItemDefinition> items)
{
foreach (var item in items)
{
if (string.IsNullOrWhiteSpace(item.Type))
{
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;
}
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)
{ {
for (var index = 0; index < modules.Count; index += 1) for (var index = 0; index < modules.Count; index += 1)
@@ -325,23 +271,6 @@ internal sealed class DataCatalogLoader(string dataRoot)
module.Type = module.ModuleType.ToDataValue(); module.Type = module.ModuleType.ToDataValue();
if (module.Products.Count == 0 && !string.IsNullOrWhiteSpace(module.Product))
{
module.Products = [module.Product];
}
if (string.IsNullOrWhiteSpace(module.ProductionMode))
{
module.ProductionMode = module.ModuleType == ModuleType.BuildModule
? "commanded"
: "passive";
}
if (module.WorkforceNeeded <= 0f)
{
module.WorkforceNeeded = module.WorkForce?.Max ?? 0f;
}
modules[index] = CreateSpecializedModuleDefinition(module); modules[index] = CreateSpecializedModuleDefinition(module);
} }
@@ -367,9 +296,19 @@ internal sealed class DataCatalogLoader(string dataRoot)
} }
} }
if (module.Products.Count > 0) if (module.ModuleType == ModuleType.Habitation)
{ {
return new ProductionModuleDefinition(module); return new HabitationModuleDefinition(module, module.SerializedWorkforce?.SupportedPopulation ?? 0f);
}
if (module.ModuleType == ModuleType.BuildModule)
{
return new BuildModuleDefinition(module, module.SerializedWorkforce?.RequiredWorkforce ?? 0f);
}
if (module.ModuleType == ModuleType.Production)
{
return new ProductionModuleDefinition(module, module.SerializedWorkforce?.RequiredWorkforce ?? 0f);
} }
return module; return module;

View File

@@ -38,7 +38,7 @@ internal sealed class WorldBuilder(
catalog.ModuleDefinitions, catalog.ModuleDefinitions,
catalog.ItemDefinitions); catalog.ItemDefinitions);
seedingService.InitializeStationStockpiles(stations); seedingService.InitializeStationStockpiles(stations, catalog.ModuleDefinitions);
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);
@@ -208,9 +208,9 @@ internal sealed class WorldBuilder(
yield break; yield break;
} }
foreach (var wareId in moduleDefinition.Production foreach (var wareId in moduleDefinition.BuildRecipes
.SelectMany(production => production.Wares.Select(ware => ware.ItemId)) .SelectMany(production => production.Wares.Select(ware => ware.ItemId))
.Concat(moduleDefinition.Products) .Concat(moduleDefinition.ProductItemIds)
.Distinct(StringComparer.Ordinal)) .Distinct(StringComparer.Ordinal))
{ {
if (!itemDefinitions.TryGetValue(wareId, out var itemDefinition)) if (!itemDefinitions.TryGetValue(wareId, out var itemDefinition))
@@ -218,7 +218,7 @@ internal sealed class WorldBuilder(
continue; continue;
} }
if (SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport.GetStorageRequirement(moduleDefinitions, itemDefinition.CargoStorageKind) is { } storageModuleId) if (SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport.GetStorageRequirement(moduleDefinitions, itemDefinition.CargoKind) is { } storageModuleId)
{ {
yield return storageModuleId; yield return storageModuleId;
} }

View File

@@ -60,11 +60,13 @@ internal sealed class WorldSeedingService
} }
} }
internal void InitializeStationStockpiles(IReadOnlyCollection<StationRuntime> stations) internal void InitializeStationStockpiles(
IReadOnlyCollection<StationRuntime> stations,
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions)
{ {
foreach (var station in stations) foreach (var station in stations)
{ {
InitializeStationPopulation(station); InitializeStationPopulation(station, moduleDefinitions);
} }
} }
@@ -231,9 +233,9 @@ internal sealed class WorldSeedingService
yield break; yield break;
} }
foreach (var wareId in moduleDefinition.Production foreach (var wareId in moduleDefinition.BuildRecipes
.SelectMany(production => production.Wares.Select(ware => ware.ItemId)) .SelectMany(production => production.Wares.Select(ware => ware.ItemId))
.Concat(moduleDefinition.Products) .Concat(moduleDefinition.ProductItemIds)
.Distinct(StringComparer.Ordinal)) .Distinct(StringComparer.Ordinal))
{ {
if (!world.ItemDefinitions.TryGetValue(wareId, out var itemDefinition)) if (!world.ItemDefinitions.TryGetValue(wareId, out var itemDefinition))
@@ -241,7 +243,7 @@ internal sealed class WorldSeedingService
continue; continue;
} }
if (SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport.GetStorageRequirement(world.ModuleDefinitions, itemDefinition.CargoStorageKind) is { } storageModuleId) if (SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport.GetStorageRequirement(world.ModuleDefinitions, itemDefinition.CargoKind) is { } storageModuleId)
{ {
yield return storageModuleId; yield return storageModuleId;
} }
@@ -549,12 +551,13 @@ internal sealed class WorldSeedingService
}; };
} }
private static void InitializeStationPopulation(StationRuntime station) private static void InitializeStationPopulation(
StationRuntime station,
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions)
{ {
var habitatModules = station.Modules.Count(module => module.ModuleType == ModuleType.Habitation); station.PopulationCapacity = SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport.GetStationSupportedPopulation(moduleDefinitions, station);
station.PopulationCapacity = 40f + (habitatModules * 220f); station.WorkforceRequired = SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport.GetStationRequiredWorkforce(moduleDefinitions, station);
station.WorkforceRequired = MathF.Max(12f, station.Modules.Count * 14f); station.Population = station.PopulationCapacity > 40f
station.Population = habitatModules > 0
? MathF.Min(station.PopulationCapacity * 0.65f, station.WorkforceRequired * 1.05f) ? MathF.Min(station.PopulationCapacity * 0.65f, station.WorkforceRequired * 1.05f)
: MathF.Min(28f, station.PopulationCapacity); : MathF.Min(28f, station.PopulationCapacity);
station.WorkforceEffectiveRatio = ComputeWorkforceRatio(station.Population, station.WorkforceRequired); station.WorkforceEffectiveRatio = ComputeWorkforceRatio(station.Population, station.WorkforceRequired);