using static SpaceGame.Api.Universe.Scenario.LoaderSupport; using SpaceGame.Api.Universe.Bootstrap; using Microsoft.Extensions.Options; namespace SpaceGame.Api.Universe.Scenario; public sealed class WorldBuilder( StaticDataCatalog staticData, IOptions balance, SystemGenerationService generationService, SpatialBuilder spatialBuilder, WorldSeedingService seedingService) { public SimulationWorld Build( GameStartOptionsDefinition gameStartOptions, ScenarioDefinition? scenarioDefinition) { var systems = generationService.ExpandSystems( generationService.PrepareAuthoredSystems(authoredSystems), gameStartOptions.WorldGeneration.TargetSystemCount); var scenario = NormalizeScenarioToAvailableSystems( scenarioDefinition, systems.Select(system => system.Id).ToList()); var systemRuntimes = systems .Select(definition => new SystemRuntime { Definition = definition, Position = ToVector(definition.Position), }) .ToList(); var systemsById = systemRuntimes.ToDictionary(system => system.Definition.Id, StringComparer.Ordinal); var spatialLayout = spatialBuilder.BuildLayout(systemRuntimes, balance.Value); var stations = CreateStations( scenario, systemsById, spatialLayout.SystemGraphs, spatialLayout.Celestials, staticData.ModuleDefinitions, staticData.ItemDefinitions); seedingService.InitializeStationStockpiles(stations, staticData.ModuleDefinitions); var refinery = seedingService.SelectRefineryStation(stations, scenario); var patrolRoutes = BuildPatrolRoutes(scenario, systemsById); var ships = CreateShips(scenario, systemsById, spatialLayout.Celestials, staticData.ShipDefinitions, patrolRoutes, stations, refinery); if (gameStartOptions.WorldGeneration.AiControllerFactionCount < int.MaxValue) { var aiFactionIds = stations .Select(s => s.FactionId) .Concat(ships.Select(s => s.FactionId)) .Where(id => !string.IsNullOrWhiteSpace(id) && !string.Equals(id, DefaultFactionId, StringComparison.Ordinal)) .Distinct(StringComparer.Ordinal) .OrderBy(id => id, StringComparer.Ordinal) .Take(gameStartOptions.WorldGeneration.AiControllerFactionCount) .ToHashSet(StringComparer.Ordinal); aiFactionIds.Add(DefaultFactionId); stations = stations.Where(s => aiFactionIds.Contains(s.FactionId)).ToList(); ships = ships.Where(s => aiFactionIds.Contains(s.FactionId)).ToList(); } var factions = seedingService.CreateFactions(stations, ships); seedingService.BootstrapFactionEconomy(factions, stations); var policies = seedingService.CreatePolicies(factions); var commanders = seedingService.CreateCommanders(factions, stations, ships); var nowUtc = DateTimeOffset.UtcNow; var playerFaction = gameStartOptions.WorldGeneration.GeneratePlayerFaction ? seedingService.CreatePlayerFaction(factions, stations, ships, commanders, policies, nowUtc) : null; var claims = seedingService.CreateClaims(stations, spatialLayout.Celestials, nowUtc); var world = new SimulationWorld { Label = "Split Viewer / Simulation World", Seed = gameStartOptions.Seed, Systems = systemRuntimes, Celestials = spatialLayout.Celestials, Nodes = spatialLayout.Nodes, Wrecks = [], Stations = stations, Ships = ships, Factions = factions, PlayerFaction = playerFaction, Geopolitics = null, Commanders = commanders, Claims = claims, ConstructionSites = [], MarketOrders = [], Policies = policies, ShipDefinitions = new Dictionary(staticData.ShipDefinitions, StringComparer.Ordinal), ItemDefinitions = new Dictionary(staticData.ItemDefinitions, StringComparer.Ordinal), ModuleDefinitions = new Dictionary(staticData.ModuleDefinitions, StringComparer.Ordinal), ModuleRecipes = new Dictionary(staticData.ModuleRecipes, StringComparer.Ordinal), Recipes = new Dictionary(staticData.Recipes, StringComparer.Ordinal), ProductionGraph = staticData.ProductionGraph, OrbitalTimeSeconds = gameStartOptions.Seed * 97d, GeneratedAtUtc = nowUtc, }; var (constructionSites, marketOrders) = seedingService.CreateConstructionSites(world); world.ConstructionSites.AddRange(constructionSites); world.MarketOrders.AddRange(marketOrders); var geopolitics = new GeopoliticalSimulationService(); geopolitics.Update(world, 0f, []); return world; } private static ScenarioDefinition NormalizeScenarioToAvailableSystems( ScenarioDefinition? scenario, IReadOnlyList availableSystemIds) { var fallbackSystemId = SystemSelectionPolicy.SelectFallbackSystemId(availableSystemIds); if (scenario is null) { return new ScenarioDefinition { GameStartOptions = new GameStartOptionsDefinition(), InitialStations = [], ShipFormations = [], PatrolRoutes = [], MiningDefaults = new MiningDefaultsDefinition { NodeSystemId = fallbackSystemId, RefinerySystemId = fallbackSystemId, }, }; } if (availableSystemIds.Count == 0) { return scenario; } string ResolveSystemId(string systemId) => availableSystemIds.Contains(systemId, StringComparer.Ordinal) ? systemId : fallbackSystemId; return new ScenarioDefinition { GameStartOptions = scenario.GameStartOptions, InitialStations = scenario.InitialStations .Select(station => new InitialStationDefinition { SystemId = ResolveSystemId(station.SystemId), Label = station.Label, Color = station.Color, Objective = station.Objective, StartingModules = station.StartingModules.ToList(), FactionId = station.FactionId, PlanetIndex = station.PlanetIndex, LagrangeSide = station.LagrangeSide, Position = station.Position?.ToArray(), }) .ToList(), ShipFormations = scenario.ShipFormations .Select(formation => new ShipFormationDefinition { ShipId = formation.ShipId, Count = formation.Count, Center = formation.Center.ToArray(), SystemId = ResolveSystemId(formation.SystemId), FactionId = formation.FactionId, StartingInventory = new Dictionary(formation.StartingInventory, StringComparer.Ordinal), }) .ToList(), PatrolRoutes = scenario.PatrolRoutes .Select(route => new PatrolRouteDefinition { SystemId = ResolveSystemId(route.SystemId), Points = route.Points.Select(point => point.ToArray()).ToList(), }) .ToList(), MiningDefaults = new MiningDefaultsDefinition { NodeSystemId = ResolveSystemId(scenario.MiningDefaults.NodeSystemId), RefinerySystemId = ResolveSystemId(scenario.MiningDefaults.RefinerySystemId), }, }; } private static List CreateStations( ScenarioDefinition scenario, IReadOnlyDictionary systemsById, IReadOnlyDictionary systemGraphs, IReadOnlyCollection celestials, IReadOnlyDictionary moduleDefinitions, IReadOnlyDictionary itemDefinitions) { var stations = new List(); var stationIdCounter = 0; foreach (var plan in scenario.InitialStations) { if (!systemsById.TryGetValue(plan.SystemId, out var system)) { continue; } var placement = SpatialBuilder.ResolveStationPlacement(plan, system, systemGraphs[system.Definition.Id], celestials); var station = new StationRuntime { Id = $"station-{++stationIdCounter}", SystemId = system.Definition.Id, Label = plan.Label, Color = plan.Color, Objective = StationSimulationService.NormalizeStationObjective(plan.Objective), Position = placement.Position, FactionId = plan.FactionId ?? DefaultFactionId, CelestialId = placement.AnchorCelestial.Id, Health = 600f, MaxHealth = 600f, }; stations.Add(station); placement.AnchorCelestial.OccupyingStructureId = station.Id; var startingModules = BuildStartingModules(plan, moduleDefinitions, itemDefinitions); foreach (var moduleId in startingModules) { AddStationModule(station, moduleDefinitions, moduleId); } } return stations; } private static IReadOnlyList BuildStartingModules( InitialStationDefinition plan, IReadOnlyDictionary moduleDefinitions, IReadOnlyDictionary itemDefinitions) { var startingModules = new List(plan.StartingModules.Count > 0 ? plan.StartingModules : ["module_arg_dock_m_01_lowtech", "module_gen_prod_energycells_01", "module_arg_stor_container_m_01"]); EnsureStartingModule(startingModules, "module_arg_dock_m_01_lowtech"); var objectiveModuleId = GetObjectiveStartingModuleId(plan.Objective); if (!string.IsNullOrWhiteSpace(objectiveModuleId)) { EnsureStartingModule(startingModules, objectiveModuleId); if (!string.Equals(objectiveModuleId, "module_gen_prod_energycells_01", StringComparison.Ordinal)) { EnsureStartingModule(startingModules, "module_gen_prod_energycells_01"); } foreach (var storageModuleId in GetRequiredStartingStorageModules(objectiveModuleId, moduleDefinitions, itemDefinitions)) { EnsureStartingModule(startingModules, storageModuleId); } } return startingModules; } private static string? GetObjectiveStartingModuleId(string? objective) => StationSimulationService.NormalizeStationObjective(objective) switch { "power" => "module_gen_prod_energycells_01", "refinery" => "module_gen_ref_ore_01", "graphene" => "module_gen_prod_graphene_01", "siliconwafers" => "module_gen_prod_siliconwafers_01", "hullparts" => "module_gen_prod_hullparts_01", "claytronics" => "module_gen_prod_claytronics_01", "quantumtubes" => "module_gen_prod_quantumtubes_01", "antimattercells" => "module_gen_prod_antimattercells_01", "superfluidcoolant" => "module_gen_prod_superfluidcoolant_01", "water" => "module_gen_prod_water_01", _ => null, }; private static IEnumerable GetRequiredStartingStorageModules( string moduleId, IReadOnlyDictionary moduleDefinitions, IReadOnlyDictionary itemDefinitions) { if (!moduleDefinitions.TryGetValue(moduleId, out var moduleDefinition)) { yield break; } foreach (var wareId in moduleDefinition.BuildRecipes .SelectMany(production => production.Wares.Select(ware => ware.ItemId)) .Concat(moduleDefinition.ProductItemIds) .Distinct(StringComparer.Ordinal)) { if (!itemDefinitions.TryGetValue(wareId, out var itemDefinition)) { continue; } if (SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport.GetStorageRequirement(moduleDefinitions, itemDefinition.CargoKind) is { } storageModuleId) { yield return storageModuleId; } } } private static void EnsureStartingModule(List modules, string moduleId) { if (!modules.Contains(moduleId, StringComparer.Ordinal)) { modules.Add(moduleId); } } private static Dictionary> BuildPatrolRoutes( ScenarioDefinition scenario, IReadOnlyDictionary systemsById) { return scenario.PatrolRoutes .GroupBy(route => route.SystemId, StringComparer.Ordinal) .ToDictionary( group => group.Key, group => group .SelectMany(route => route.Points) .Select(point => NormalizeScenarioPoint(systemsById[group.Key], point)) .ToList(), StringComparer.Ordinal); } private List CreateShips( ScenarioDefinition scenario, IReadOnlyDictionary systemsById, IReadOnlyCollection celestials, IReadOnlyDictionary shipDefinitions, IReadOnlyDictionary> patrolRoutes, IReadOnlyCollection stations, StationRuntime? refinery) { var ships = new List(); var shipIdCounter = 0; foreach (var formation in scenario.ShipFormations) { if (!shipDefinitions.TryGetValue(formation.ShipId, out var definition)) { continue; } for (var index = 0; index < formation.Count; index += 1) { var offset = new Vector3((index % 3) * 18f, balance.Value.YPlane, (index / 3) * 18f); var position = Add(NormalizeScenarioPoint(systemsById[formation.SystemId], formation.Center), offset); ships.Add(new ShipRuntime { Id = $"ship-{++shipIdCounter}", SystemId = formation.SystemId, Definition = definition, FactionId = formation.FactionId ?? DefaultFactionId, Position = position, TargetPosition = position, SpatialState = SpatialBuilder.CreateInitialShipSpatialState(formation.SystemId, position, celestials), DefaultBehavior = WorldSeedingService.CreateBehavior( definition, formation.SystemId, formation.FactionId ?? DefaultFactionId, scenario, patrolRoutes, stations, refinery), Skills = WorldSeedingService.CreateSkills(definition), Health = definition.MaxHealth, }); foreach (var (itemId, amount) in formation.StartingInventory) { if (amount > 0f) { ships[^1].Inventory[itemId] = amount; } } } } return ships; } }