using static SpaceGame.Api.Factions.AI.CommanderPlanningService; using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; namespace SpaceGame.Api.Stations.Simulation; internal sealed class StationSimulationService { internal const int StrategicControlTargetSystems = 5; internal void ReviewStationMarketOrders(SimulationWorld world, StationRuntime station) { if (station.CommanderId is null) { return; } var desiredOrders = new List(); var economy = FactionEconomyAnalyzer.Build(world, station.FactionId); var role = DetermineStationRole(station); var site = GetConstructionSiteForStation(world, station.Id); var waterReserve = MathF.Max(30f, station.Population * 3f); var constructionEnergyReserve = GetConstructionDemandForItem(world, site, "energycells"); var constructionHullpartsReserve = GetConstructionDemandForItem(world, site, "hullparts"); var constructionClayReserve = GetConstructionDemandForItem(world, site, "claytronics"); var constructionRefinedReserve = GetConstructionDemandForItem(world, site, "refinedmetals"); var iceReserve = role == "water" ? 260f : 0f; var methaneReserve = role == "graphene" ? 320f : 0f; var hydrogenReserve = role == "antimattercells" ? 320f : 0f; var heliumReserve = role == "superfluidcoolant" ? 320f : 0f; var siliconReserve = role == "siliconwafers" ? 240f : 0f; var grapheneInputReserve = role == "quantumtubes" ? 160f : 0f; var superfluidCoolantInputReserve = role == "quantumtubes" ? 120f : 0f; var antimatterCellsInputReserve = role == "claytronics" ? 120f : 0f; var quantumTubesInputReserve = role == "claytronics" ? 120f : 0f; var energyReserve = role switch { "power" => 120f, "refinery" => 160f, "hullparts" => 180f, "claytronics" => 220f, "graphene" => 160f, "siliconwafers" => 160f, "antimattercells" => 160f, "superfluidcoolant" => 160f, "quantumtubes" => 160f, "water" => 140f, _ => 60f, } + constructionEnergyReserve; var refinedReserve = role switch { "hullparts" => 220f, "shipyard" => 260f, "refinery" => 80f, _ => 0f, }; var oreReserve = role == "refinery" ? 260f : 0f; var hullpartsReserve = MathF.Max(constructionHullpartsReserve, HasStationModules(station, "module_gen_build_l_01") ? 120f : 0f); var claytronicsReserve = MathF.Max(constructionClayReserve, HasStationModules(station, "module_gen_build_l_01") ? 120f : 0f); var grapheneReserve = role == "graphene" ? 120f : 0f; var siliconWafersReserve = role == "siliconwafers" ? 120f : 0f; var antimatterCellsReserve = role == "antimattercells" ? 120f : 0f; var superfluidCoolantReserve = role == "superfluidcoolant" ? 120f : 0f; var quantumTubesReserve = role == "quantumtubes" ? 120f : 0f; var shipPartsReserve = HasStationModules(station, "module_gen_build_l_01") && GetShipProductionPressure(world, station.FactionId, "military") > 0.2f ? 90f : 0f; AddDemandOrder(desiredOrders, station, "water", ScaleReserveByEconomy(economy, "water", waterReserve), valuationBase: ScaleDemandValuation(economy, "water", 1.1f)); AddDemandOrder(desiredOrders, station, "energycells", ScaleReserveByEconomy(economy, "energycells", energyReserve), valuationBase: ScaleDemandValuation(economy, "energycells", 1.0f)); AddDemandOrder(desiredOrders, station, "ice", ScaleReserveByEconomy(economy, "ice", iceReserve), valuationBase: ScaleDemandValuation(economy, "ice", 1.0f)); AddDemandOrder(desiredOrders, station, "methane", ScaleReserveByEconomy(economy, "methane", methaneReserve), valuationBase: ScaleDemandValuation(economy, "methane", 1.0f)); AddDemandOrder(desiredOrders, station, "hydrogen", ScaleReserveByEconomy(economy, "hydrogen", hydrogenReserve), valuationBase: ScaleDemandValuation(economy, "hydrogen", 1.0f)); AddDemandOrder(desiredOrders, station, "helium", ScaleReserveByEconomy(economy, "helium", heliumReserve), valuationBase: ScaleDemandValuation(economy, "helium", 1.0f)); AddDemandOrder(desiredOrders, station, "ore", ScaleReserveByEconomy(economy, "ore", oreReserve), valuationBase: ScaleDemandValuation(economy, "ore", 1.0f)); AddDemandOrder(desiredOrders, station, "silicon", ScaleReserveByEconomy(economy, "silicon", siliconReserve), valuationBase: ScaleDemandValuation(economy, "silicon", 1.0f)); AddDemandOrder(desiredOrders, station, "refinedmetals", ScaleReserveByEconomy(economy, "refinedmetals", MathF.Max(refinedReserve, constructionRefinedReserve)), valuationBase: ScaleDemandValuation(economy, "refinedmetals", 1.15f)); AddDemandOrder(desiredOrders, station, "hullparts", ScaleReserveByEconomy(economy, "hullparts", hullpartsReserve + shipPartsReserve), valuationBase: ScaleDemandValuation(economy, "hullparts", 1.3f)); AddDemandOrder(desiredOrders, station, "claytronics", ScaleReserveByEconomy(economy, "claytronics", claytronicsReserve), valuationBase: ScaleDemandValuation(economy, "claytronics", 1.35f)); AddDemandOrder(desiredOrders, station, "graphene", ScaleReserveByEconomy(economy, "graphene", grapheneReserve), valuationBase: ScaleDemandValuation(economy, "graphene", 1.05f)); AddDemandOrder(desiredOrders, station, "siliconwafers", ScaleReserveByEconomy(economy, "siliconwafers", siliconWafersReserve), valuationBase: ScaleDemandValuation(economy, "siliconwafers", 1.05f)); AddDemandOrder(desiredOrders, station, "antimattercells", ScaleReserveByEconomy(economy, "antimattercells", antimatterCellsReserve), valuationBase: ScaleDemandValuation(economy, "antimattercells", 1.05f)); AddDemandOrder(desiredOrders, station, "superfluidcoolant", ScaleReserveByEconomy(economy, "superfluidcoolant", superfluidCoolantReserve), valuationBase: ScaleDemandValuation(economy, "superfluidcoolant", 1.05f)); AddDemandOrder(desiredOrders, station, "graphene", ScaleReserveByEconomy(economy, "graphene", grapheneInputReserve), valuationBase: ScaleDemandValuation(economy, "graphene", 1.1f)); AddDemandOrder(desiredOrders, station, "superfluidcoolant", ScaleReserveByEconomy(economy, "superfluidcoolant", superfluidCoolantInputReserve), valuationBase: ScaleDemandValuation(economy, "superfluidcoolant", 1.1f)); AddDemandOrder(desiredOrders, station, "antimattercells", ScaleReserveByEconomy(economy, "antimattercells", antimatterCellsInputReserve), valuationBase: ScaleDemandValuation(economy, "antimattercells", 1.1f)); AddDemandOrder(desiredOrders, station, "quantumtubes", ScaleReserveByEconomy(economy, "quantumtubes", quantumTubesInputReserve), valuationBase: ScaleDemandValuation(economy, "quantumtubes", 1.1f)); AddDemandOrder(desiredOrders, station, "quantumtubes", ScaleReserveByEconomy(economy, "quantumtubes", quantumTubesReserve), valuationBase: ScaleDemandValuation(economy, "quantumtubes", 1.05f)); AddSupplyOrder(desiredOrders, station, "water", ScaleSupplyTriggerByEconomy(economy, "water", waterReserve * 1.5f), reserveFloor: waterReserve, valuationBase: ScaleSupplyValuation(economy, "water", 0.65f)); AddSupplyOrder(desiredOrders, station, "energycells", ScaleSupplyTriggerByEconomy(economy, "energycells", energyReserve * 1.4f), reserveFloor: energyReserve, valuationBase: ScaleSupplyValuation(economy, "energycells", 0.7f)); AddSupplyOrder(desiredOrders, station, "ice", ScaleSupplyTriggerByEconomy(economy, "ice", iceReserve * 1.4f), reserveFloor: iceReserve, valuationBase: ScaleSupplyValuation(economy, "ice", 0.5f)); AddSupplyOrder(desiredOrders, station, "methane", ScaleSupplyTriggerByEconomy(economy, "methane", methaneReserve * 1.4f), reserveFloor: methaneReserve, valuationBase: ScaleSupplyValuation(economy, "methane", 0.7f)); AddSupplyOrder(desiredOrders, station, "hydrogen", ScaleSupplyTriggerByEconomy(economy, "hydrogen", hydrogenReserve * 1.4f), reserveFloor: hydrogenReserve, valuationBase: ScaleSupplyValuation(economy, "hydrogen", 0.7f)); AddSupplyOrder(desiredOrders, station, "helium", ScaleSupplyTriggerByEconomy(economy, "helium", heliumReserve * 1.4f), reserveFloor: heliumReserve, valuationBase: ScaleSupplyValuation(economy, "helium", 0.7f)); AddSupplyOrder(desiredOrders, station, "ore", ScaleSupplyTriggerByEconomy(economy, "ore", oreReserve * 1.4f), reserveFloor: oreReserve, valuationBase: ScaleSupplyValuation(economy, "ore", 0.7f)); AddSupplyOrder(desiredOrders, station, "silicon", ScaleSupplyTriggerByEconomy(economy, "silicon", siliconReserve * 1.4f), reserveFloor: siliconReserve, valuationBase: ScaleSupplyValuation(economy, "silicon", 0.7f)); AddSupplyOrder(desiredOrders, station, "refinedmetals", ScaleSupplyTriggerByEconomy(economy, "refinedmetals", MathF.Max(refinedReserve, constructionRefinedReserve) * 1.4f), reserveFloor: MathF.Max(refinedReserve, constructionRefinedReserve), valuationBase: ScaleSupplyValuation(economy, "refinedmetals", 0.95f)); AddSupplyOrder(desiredOrders, station, "hullparts", ScaleSupplyTriggerByEconomy(economy, "hullparts", MathF.Max(hullpartsReserve * 1.35f, hullpartsReserve + 40f)), reserveFloor: hullpartsReserve, valuationBase: ScaleSupplyValuation(economy, "hullparts", 1.05f)); AddSupplyOrder(desiredOrders, station, "claytronics", ScaleSupplyTriggerByEconomy(economy, "claytronics", MathF.Max(claytronicsReserve * 1.35f, claytronicsReserve + 30f)), reserveFloor: claytronicsReserve, valuationBase: ScaleSupplyValuation(economy, "claytronics", 1.1f)); AddSupplyOrder(desiredOrders, station, "graphene", ScaleSupplyTriggerByEconomy(economy, "graphene", MathF.Max(grapheneReserve * 1.35f, grapheneReserve + 30f)), reserveFloor: grapheneReserve, valuationBase: ScaleSupplyValuation(economy, "graphene", 0.9f)); AddSupplyOrder(desiredOrders, station, "siliconwafers", ScaleSupplyTriggerByEconomy(economy, "siliconwafers", MathF.Max(siliconWafersReserve * 1.35f, siliconWafersReserve + 30f)), reserveFloor: siliconWafersReserve, valuationBase: ScaleSupplyValuation(economy, "siliconwafers", 0.9f)); AddSupplyOrder(desiredOrders, station, "antimattercells", ScaleSupplyTriggerByEconomy(economy, "antimattercells", MathF.Max(antimatterCellsReserve * 1.35f, antimatterCellsReserve + 30f)), reserveFloor: antimatterCellsReserve, valuationBase: ScaleSupplyValuation(economy, "antimattercells", 0.9f)); AddSupplyOrder(desiredOrders, station, "superfluidcoolant", ScaleSupplyTriggerByEconomy(economy, "superfluidcoolant", MathF.Max(superfluidCoolantReserve * 1.35f, superfluidCoolantReserve + 30f)), reserveFloor: superfluidCoolantReserve, valuationBase: ScaleSupplyValuation(economy, "superfluidcoolant", 0.9f)); AddSupplyOrder(desiredOrders, station, "quantumtubes", ScaleSupplyTriggerByEconomy(economy, "quantumtubes", MathF.Max(quantumTubesReserve * 1.35f, quantumTubesReserve + 30f)), reserveFloor: quantumTubesReserve, valuationBase: ScaleSupplyValuation(economy, "quantumtubes", 0.9f)); desiredOrders = ApplyRegionalMarketModifiers(world, station, desiredOrders); ReconcileStationMarketOrders(world, station, desiredOrders); } internal static float GetStationReserveFloor(SimulationWorld world, StationRuntime station, string itemId) { var role = DetermineStationRole(station); var site = GetConstructionSiteForStation(world, station.Id); var constructionEnergyReserve = GetConstructionDemandForItem(world, site, "energycells"); var constructionHullpartsReserve = GetConstructionDemandForItem(world, site, "hullparts"); var constructionClayReserve = GetConstructionDemandForItem(world, site, "claytronics"); var constructionRefinedReserve = GetConstructionDemandForItem(world, site, "refinedmetals"); var shipPartsReserve = HasStationModules(station, "module_gen_build_l_01") && GetShipProductionPressure(world, station.FactionId, "military") > 0.2f ? 90f : 0f; return itemId switch { "water" => MathF.Max(30f, station.Population * 3f), "energycells" => role switch { "power" => 120f, "refinery" => 160f, "hullparts" => 180f, "claytronics" => 220f, "graphene" => 160f, "siliconwafers" => 160f, "antimattercells" => 160f, "superfluidcoolant" => 160f, "quantumtubes" => 160f, "water" => 140f, _ => 60f, } + constructionEnergyReserve, "ice" => role == "water" ? 260f : 0f, "methane" => role == "graphene" ? 320f : 0f, "hydrogen" => role == "antimattercells" ? 320f : 0f, "helium" => role == "superfluidcoolant" ? 320f : 0f, "ore" => role == "refinery" ? 260f : 0f, "silicon" => role == "siliconwafers" ? 240f : 0f, "refinedmetals" => MathF.Max(role switch { "hullparts" => 220f, "shipyard" => 260f, "refinery" => 80f, _ => 0f, }, constructionRefinedReserve), "hullparts" => MathF.Max(constructionHullpartsReserve, HasStationModules(station, "module_gen_build_l_01") ? 120f : 0f) + shipPartsReserve, "claytronics" => MathF.Max(constructionClayReserve, HasStationModules(station, "module_gen_build_l_01") ? 120f : 0f), "graphene" => MathF.Max(role == "graphene" ? 120f : 0f, role == "quantumtubes" ? 160f : 0f), "siliconwafers" => role == "siliconwafers" ? 120f : 0f, "antimattercells" => MathF.Max(role == "antimattercells" ? 120f : 0f, role == "claytronics" ? 120f : 0f), "superfluidcoolant" => MathF.Max(role == "superfluidcoolant" ? 120f : 0f, role == "quantumtubes" ? 120f : 0f), "quantumtubes" => MathF.Max(role == "quantumtubes" ? 120f : 0f, role == "claytronics" ? 120f : 0f), _ => 0f, }; } internal void RunStationProduction(SimulationWorld world, StationRuntime station, float deltaSeconds, ICollection events) { var faction = world.Factions.FirstOrDefault(candidate => candidate.Id == station.FactionId); foreach (var laneKey in GetStationProductionLanes(world, station)) { var recipe = SelectProductionRecipe(world, station, laneKey); if (recipe is null) { station.ProductionLaneTimers[laneKey] = 0f; continue; } var throughput = GetStationProductionThroughput(world, station, recipe); var produced = 0f; station.ProductionLaneTimers[laneKey] = GetStationProductionTimer(station, laneKey) + (deltaSeconds * station.WorkforceEffectiveRatio * throughput); while (station.ProductionLaneTimers[laneKey] >= recipe.Duration && CanRunRecipe(world, station, recipe)) { station.ProductionLaneTimers[laneKey] -= recipe.Duration; foreach (var input in recipe.Inputs) { RemoveInventory(station.Inventory, input.ItemId, input.Amount); } if (recipe.ShipOutputId is not null) { produced += StationLifecycleService.CompleteShipRecipe(world, station, recipe, events); continue; } foreach (var output in recipe.Outputs) { produced += TryAddStationInventory(world, station, output.ItemId, output.Amount); } } if (produced <= 0.01f) { continue; } events.Add(new SimulationEventRecord("station", station.Id, "production-complete", $"{station.Label} completed {recipe.Label}.", DateTimeOffset.UtcNow)); if (faction is not null) { faction.GoodsProduced += produced; } } } internal static IEnumerable GetStationProductionLanes(SimulationWorld world, StationRuntime station) { foreach (var moduleId in station.InstalledModules.Distinct(StringComparer.Ordinal)) { if (!world.ModuleDefinitions.TryGetValue(moduleId, out var def) || string.IsNullOrEmpty(def.ProductionMode)) { continue; } if (string.Equals(def.ProductionMode, "commanded", StringComparison.Ordinal) && station.CommanderId is null) { continue; } yield return moduleId; } } internal static float GetStationProductionTimer(StationRuntime station, string laneKey) => station.ProductionLaneTimers.TryGetValue(laneKey, out var timer) ? timer : 0f; internal static RecipeDefinition? SelectProductionRecipe(SimulationWorld world, StationRuntime station, string laneKey) => world.Recipes.Values .Where(recipe => RecipeAppliesToStation(station, recipe) && string.Equals(GetStationProductionLaneKey(world, recipe), laneKey, StringComparison.Ordinal)) .OrderByDescending(recipe => GetStationRecipePriority(world, station, recipe)) .FirstOrDefault(recipe => CanRunRecipe(world, station, recipe)); internal static string? GetStationProductionLaneKey(SimulationWorld world, RecipeDefinition recipe) => recipe.RequiredModules.FirstOrDefault(moduleId => world.ModuleDefinitions.TryGetValue(moduleId, out var def) && !string.IsNullOrEmpty(def.ProductionMode)); internal static float GetStationProductionThroughput(SimulationWorld world, StationRuntime station, RecipeDefinition recipe) { var laneModuleId = GetStationProductionLaneKey(world, recipe); if (laneModuleId is null) { return 1f; } return Math.Max(1, CountModules(station.InstalledModules, laneModuleId)); } private static float GetStationRecipePriority(SimulationWorld world, StationRuntime station, RecipeDefinition recipe) { var priority = (float)recipe.Priority; var expansionPressure = GetFactionExpansionPressure(world, station.FactionId); var fleetPressure = GetShipProductionPressure(world, station.FactionId, "military"); priority += GetStationRecipePriorityAdjustment(world, station, recipe, expansionPressure, fleetPressure); priority += GetStrategicRecipeBias(world, station, recipe); return priority; } private static float GetStationRecipePriorityAdjustment(SimulationWorld world, StationRuntime station, RecipeDefinition recipe, float expansionPressure, float fleetPressure) { if (recipe.ShipOutputId is not null && world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var shipDefinition)) { var shipPressure = GetShipProductionPressure(world, station.FactionId, shipDefinition.Kind); return shipDefinition.Kind switch { "military" => recipe.Id switch { "frigate-construction" => 320f * shipPressure, "destroyer-construction" => 200f * shipPressure, "cruiser-construction" => 120f * shipPressure, _ => 160f * shipPressure, }, "construction" => 260f * shipPressure, "mining" => 250f * shipPressure, "transport" => 230f * shipPressure, _ => 0f, }; } var outputItemIds = recipe.Outputs .Select(output => output.ItemId) .ToHashSet(StringComparer.Ordinal); if (outputItemIds.Contains("hullparts")) { return HasStationModules(station, "module_gen_prod_advancedelectronics_01", "module_gen_build_l_01") ? -140f * MathF.Max(expansionPressure, fleetPressure) : 280f * MathF.Max(expansionPressure, fleetPressure); } if (outputItemIds.Contains("refinedmetals")) { return 180f * expansionPressure; } if (outputItemIds.Overlaps(["advancedelectronics", "dronecomponents", "engineparts", "fieldcoils", "missilecomponents", "shieldcomponents", "smartchips"])) { return 170f * expansionPressure; } if (outputItemIds.Overlaps(["turretcomponents", "weaponcomponents"])) { return 160f * expansionPressure; } return recipe.Id switch { "command-bridge-module-assembly" or "reactor-core-module-assembly" or "capacitor-bank-module-assembly" or "ion-drive-module-assembly" or "ftl-core-module-assembly" or "gun-turret-module-assembly" => 220f * MathF.Max(expansionPressure, fleetPressure), "ammo-fabrication" => -80f * expansionPressure, "trade-hub-assembly" or "refinery-assembly" or "farm-ring-assembly" or "manufactory-assembly" or "shipyard-assembly" or "defense-grid-assembly" or "stargate-assembly" => -120f * expansionPressure, _ => 0f, }; } private static float GetStrategicRecipeBias(SimulationWorld world, StationRuntime station, RecipeDefinition recipe) { var commander = station.CommanderId is null ? null : world.Commanders.FirstOrDefault(candidate => candidate.Id == station.CommanderId); var assignment = commander?.Assignment; if (assignment is null) { return 0f; } var outputItemIds = recipe.Outputs .Select(output => output.ItemId) .ToHashSet(StringComparer.Ordinal); if (string.Equals(assignment.Kind, "ship-production-focus", StringComparison.Ordinal) && recipe.ShipOutputId is not null && world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var shipDefinition) && string.Equals(shipDefinition.Kind, "military", StringComparison.Ordinal)) { return 260f; } if (string.Equals(assignment.Kind, "commodity-focus", StringComparison.Ordinal) && assignment.ItemId is not null && outputItemIds.Contains(assignment.ItemId)) { return 220f; } if (string.Equals(assignment.Kind, "expansion-support", StringComparison.Ordinal) && outputItemIds.Overlaps(["energycells", "refinedmetals", "hullparts", "claytronics"])) { return 180f; } if (string.Equals(assignment.Kind, "station-oversight", StringComparison.Ordinal) && assignment.ItemId is not null && outputItemIds.Contains(assignment.ItemId)) { return 90f; } return 0f; } internal static bool RecipeAppliesToStation(StationRuntime station, RecipeDefinition recipe) { var categoryMatch = string.Equals(recipe.FacilityCategory, "station", StringComparison.Ordinal) || string.Equals(recipe.FacilityCategory, "farm", StringComparison.Ordinal) || string.Equals(recipe.FacilityCategory, station.Category, StringComparison.Ordinal); return categoryMatch && recipe.RequiredModules.All(moduleId => station.InstalledModules.Contains(moduleId, StringComparer.Ordinal)); } private static bool CanRunRecipe(SimulationWorld world, StationRuntime station, RecipeDefinition recipe) { if (recipe.ShipOutputId is not null) { if (!world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var shipDefinition)) { return false; } if (GetShipProductionPressure(world, station.FactionId, shipDefinition.Kind) <= 0.05f) { return false; } } if (recipe.Inputs.Any(input => GetInventoryAmount(station.Inventory, input.ItemId) + 0.001f < input.Amount)) { return false; } return recipe.Outputs.All(output => CanAcceptStationInventory(world, station, output.ItemId, output.Amount)); } private static bool CanAcceptStationInventory(SimulationWorld world, StationRuntime station, string itemId, float amount) { if (amount <= 0f || !world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition)) { return false; } var requiredModule = GetStorageRequirement(itemDefinition.CargoKind); if (requiredModule is not null && !station.InstalledModules.Contains(requiredModule, StringComparer.Ordinal)) { return false; } var capacity = GetStationStorageCapacity(station, itemDefinition.CargoKind); if (capacity <= 0.01f) { return false; } var used = station.Inventory .Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoKind == itemDefinition.CargoKind) .Sum(entry => entry.Value); return used + amount <= capacity + 0.001f; } private static bool HasRefineryCapability(StationRuntime station) => HasStationModules(station, "module_gen_prod_refinedmetals_01", "module_gen_prod_energycells_01", "module_arg_stor_solid_m_01"); internal static string NormalizeStationObjective(string? objective) { return objective?.Trim().ToLowerInvariant() switch { "power" or "energy" or "energycells" => "power", "water" or "ice-refinery" => "water", "refinery" or "refinedmetals" => "refinery", "hullparts" or "hull" => "hullparts", "claytronics" or "clay" => "claytronics", "graphene" => "graphene", "siliconwafers" or "silicon-wafers" or "silicon" => "siliconwafers", "antimattercells" or "antimatter-cells" => "antimattercells", "superfluidcoolant" or "superfluid-coolant" => "superfluidcoolant", "quantumtubes" or "quantum-tubes" => "quantumtubes", "shipyard" or "ship-production" => "shipyard", _ => "general", }; } internal static string DetermineStationRole(StationRuntime station) { var objective = NormalizeStationObjective(station.Objective); if (!string.Equals(objective, "general", StringComparison.Ordinal)) { return objective; } if (HasStationModules(station, "module_gen_build_l_01")) { return "shipyard"; } if (HasStationModules(station, "module_gen_prod_water_01")) { return "water"; } if (HasStationModules(station, "module_gen_prod_superfluidcoolant_01")) { return "superfluidcoolant"; } if (HasStationModules(station, "module_gen_prod_quantumtubes_01")) { return "quantumtubes"; } if (HasStationModules(station, "module_gen_prod_antimattercells_01")) { return "antimattercells"; } if (HasStationModules(station, "module_gen_prod_siliconwafers_01")) { return "siliconwafers"; } if (HasStationModules(station, "module_gen_prod_graphene_01")) { return "graphene"; } if (HasStationModules(station, "module_gen_prod_claytronics_01")) { return "claytronics"; } if (HasStationModules(station, "module_gen_prod_hullparts_01")) { return "hullparts"; } if (HasStationModules(station, "module_gen_prod_refinedmetals_01")) { return "refinery"; } if (HasStationModules(station, "module_gen_prod_energycells_01")) { return "power"; } return "general"; } private static float GetConstructionDemandForItem(SimulationWorld world, ConstructionSiteRuntime? site, string itemId) { if (site is null || !site.RequiredItems.TryGetValue(itemId, out var required)) { return 0f; } return MathF.Max(0f, required - GetConstructionDeliveredAmount(world, site, itemId)); } private static void AddDemandOrder(ICollection desiredOrders, StationRuntime station, string itemId, float targetAmount, float valuationBase) { var current = GetInventoryAmount(station.Inventory, itemId); if (current >= targetAmount - 0.01f) { return; } var deficit = targetAmount - current; var scarcity = targetAmount <= 0.01f ? 1f : MathF.Min(1f, deficit / targetAmount); desiredOrders.Add(new DesiredMarketOrder(MarketOrderKinds.Buy, itemId, deficit, valuationBase + scarcity, null)); } private static void AddSupplyOrder(ICollection desiredOrders, StationRuntime station, string itemId, float triggerAmount, float reserveFloor, float valuationBase) { var current = GetInventoryAmount(station.Inventory, itemId); if (current <= triggerAmount + 0.01f) { return; } var surplus = current - reserveFloor; if (surplus <= 0.01f) { return; } var surplusRatio = triggerAmount <= 0.01f ? 1f : MathF.Min(1f, surplus / triggerAmount); var liquidationValuation = MathF.Max(0.05f, valuationBase * (1f - (0.85f * surplusRatio))); desiredOrders.Add(new DesiredMarketOrder(MarketOrderKinds.Sell, itemId, surplus, liquidationValuation, reserveFloor)); } private static void ReconcileStationMarketOrders(SimulationWorld world, StationRuntime station, IReadOnlyCollection desiredOrders) { var existingOrders = world.MarketOrders .Where(order => order.StationId == station.Id && order.ConstructionSiteId is null) .ToList(); foreach (var desired in desiredOrders) { var order = existingOrders.FirstOrDefault(candidate => candidate.Kind == desired.Kind && candidate.ItemId == desired.ItemId && candidate.ConstructionSiteId is null); if (order is null) { order = new MarketOrderRuntime { Id = $"market-order-{station.Id}-{desired.Kind}-{desired.ItemId}", FactionId = station.FactionId, StationId = station.Id, Kind = desired.Kind, ItemId = desired.ItemId, Amount = desired.Amount, RemainingAmount = desired.Amount, Valuation = desired.Valuation, ReserveThreshold = desired.ReserveThreshold, State = MarketOrderStateKinds.Open, }; world.MarketOrders.Add(order); station.MarketOrderIds.Add(order.Id); existingOrders.Add(order); continue; } order.RemainingAmount = desired.Amount; order.Valuation = desired.Valuation; order.ReserveThreshold = desired.ReserveThreshold; order.State = desired.Amount <= 0.01f ? MarketOrderStateKinds.Cancelled : MarketOrderStateKinds.Open; } foreach (var order in existingOrders.Where(order => desiredOrders.All(desired => desired.Kind != order.Kind || desired.ItemId != order.ItemId))) { order.RemainingAmount = 0f; order.State = MarketOrderStateKinds.Cancelled; } } internal static float GetFactionExpansionPressure(SimulationWorld world, string factionId) { var targetSystems = Math.Max(1, Math.Min(StrategicControlTargetSystems, world.Systems.Count)); var controlledSystems = GetFactionControlledSystemsCount(world, factionId); var deficit = Math.Max(0, targetSystems - controlledSystems); var contestedSystems = world.Geopolitics?.Territory.ControlStates.Count(state => state.IsContested && (string.Equals(state.ControllerFactionId, factionId, StringComparison.Ordinal) || string.Equals(state.PrimaryClaimantFactionId, factionId, StringComparison.Ordinal) || state.ClaimantFactionIds.Contains(factionId, StringComparer.Ordinal))) ?? 0; var frontierSystems = world.Geopolitics?.Territory.Zones.Count(zone => string.Equals(zone.FactionId, factionId, StringComparison.Ordinal) && zone.Kind is "frontier" or "corridor" or "contested") ?? 0; return Math.Clamp((deficit / (float)targetSystems) + (contestedSystems * 0.12f) + (frontierSystems * 0.04f), 0f, 1f); } internal static int GetFactionControlledSystemsCount(SimulationWorld world, string factionId) { return GeopoliticalSimulationService.GetControlledSystems(world, factionId).Count; } private static float ScaleReserveByEconomy(FactionEconomySnapshot economy, string itemId, float baseReserve) { var commodity = economy.GetCommodity(itemId); if (commodity.Level == CommodityLevelKind.Critical) { return baseReserve * 1.6f; } return commodity.Level switch { CommodityLevelKind.Low => baseReserve * 1.25f, CommodityLevelKind.Stable when commodity.ProjectedNetRatePerSecond < -0.01f => baseReserve * 1.1f, _ => MathF.Max(0f, baseReserve), }; } private static float ScaleSupplyTriggerByEconomy(FactionEconomySnapshot economy, string itemId, float baseTrigger) { var commodity = economy.GetCommodity(itemId); return commodity.NetRatePerSecond < -0.01f ? baseTrigger * 1.2f : baseTrigger; } private static float ScaleDemandValuation(FactionEconomySnapshot economy, string itemId, float baseValuation) { var commodity = economy.GetCommodity(itemId); return commodity.Level switch { CommodityLevelKind.Critical => baseValuation * 1.6f, CommodityLevelKind.Low => baseValuation * 1.3f, CommodityLevelKind.Stable when commodity.ProjectedNetRatePerSecond < -0.01f => baseValuation * 1.15f, CommodityLevelKind.Surplus when commodity.ProjectedNetRatePerSecond > 0.01f => baseValuation * 0.9f, _ => commodity.ProductionRatePerSecond > 0.01f ? baseValuation : baseValuation * 1.15f, }; } private static float ScaleSupplyValuation(FactionEconomySnapshot economy, string itemId, float baseValuation) { var commodity = economy.GetCommodity(itemId); return commodity.Level == CommodityLevelKind.Surplus && commodity.NetRatePerSecond > 0.01f ? baseValuation * 0.75f : commodity.Level == CommodityLevelKind.Critical ? baseValuation * 1.15f : baseValuation; } private static List ApplyRegionalMarketModifiers(SimulationWorld world, StationRuntime station, IReadOnlyCollection desiredOrders) { var region = GeopoliticalSimulationService.GetPrimaryEconomicRegion(world, station.FactionId, station.SystemId); if (region is null) { return desiredOrders.ToList(); } var security = world.Geopolitics?.EconomyRegions.SecurityAssessments.FirstOrDefault(assessment => string.Equals(assessment.RegionId, region.Id, StringComparison.Ordinal)); var economic = world.Geopolitics?.EconomyRegions.EconomicAssessments.FirstOrDefault(assessment => string.Equals(assessment.RegionId, region.Id, StringComparison.Ordinal)); var bottlenecks = world.Geopolitics?.EconomyRegions.Bottlenecks .Where(bottleneck => string.Equals(bottleneck.RegionId, region.Id, StringComparison.Ordinal)) .ToDictionary(bottleneck => bottleneck.ItemId, StringComparer.Ordinal) ?? new Dictionary(StringComparer.Ordinal); var riskMultiplier = 1f + ((security?.SupplyRisk ?? 0f) * 0.3f) + ((security?.AccessFriction ?? 0f) * 0.2f); var sustainmentFloor = 1f + MathF.Max(0f, 0.55f - (economic?.SustainmentScore ?? 1f)); return desiredOrders .Select(order => { bottlenecks.TryGetValue(order.ItemId, out var bottleneck); var severity = bottleneck?.Severity ?? 0f; var buyBias = order.Kind == MarketOrderKinds.Buy ? 1f + (severity * 0.08f) : 1f; var sellBias = order.Kind == MarketOrderKinds.Sell && severity > 0f ? MathF.Max(0.35f, 1f - (severity * 0.07f)) : 1f; var amount = order.Amount * (order.Kind == MarketOrderKinds.Buy ? riskMultiplier * buyBias * sustainmentFloor : sellBias); var valuation = order.Valuation * (order.Kind == MarketOrderKinds.Buy ? 1f + (severity * 0.06f) + ((security?.SupplyRisk ?? 0f) * 0.18f) : 1f + (severity * 0.04f)); float? reserveThreshold = order.ReserveThreshold.HasValue ? order.ReserveThreshold.Value * (1f + ((security?.SupplyRisk ?? 0f) * 0.15f)) : null; return new DesiredMarketOrder(order.Kind, order.ItemId, amount, valuation, reserveThreshold); }) .ToList(); } private static float GetShipProductionPressure(SimulationWorld world, string factionId, string shipKind) { var economic = FindFactionEconomicAssessment(world, factionId); var threat = FindFactionThreatAssessment(world, factionId); if (economic is null || threat is null) { return 0f; } return shipKind switch { "military" => threat.EnemyFactionCount > 0 ? economic.MilitaryShipCount < Math.Max(4, economic.ControlledSystemCount * 2) ? 1f : 0.25f : 0.1f, "construction" => economic.PrimaryExpansionSiteId is not null ? economic.ConstructorShipCount < 1 ? 1f : 0.35f : economic.ConstructorShipCount < 1 ? 0.5f : 0f, "transport" => economic.TransportShipCount < Math.Max(2, economic.ControlledSystemCount) ? 0.8f : 0.2f, _ when shipKind == "mining" || shipKind == "miner" => economic.MinerShipCount < Math.Max(2, economic.ControlledSystemCount) ? 0.85f : 0.2f, _ => 0.15f, }; } private static bool FactionControlsSystem(SimulationWorld world, string factionId, string systemId) => GeopoliticalSimulationService.FactionControlsSystem(world, factionId, systemId); private sealed record DesiredMarketOrder(string Kind, string ItemId, float Amount, float Valuation, float? ReserveThreshold); }