using static SpaceGame.Api.Universe.Scenario.LoaderSupport; namespace SpaceGame.Api.Universe.Scenario; internal sealed class WorldBuilder( WorldGenerationOptions worldGeneration, DataCatalogLoader dataLoader, SystemGenerationService generationService, SpatialBuilder spatialBuilder, WorldSeedingService seedingService) { internal SimulationWorld Build() { var catalog = dataLoader.LoadCatalog(); var systems = generationService.ExpandSystems( generationService.InjectSpecialSystems(catalog.AuthoredSystems), worldGeneration.TargetSystemCount); Console.WriteLine("TEST"); Console.WriteLine(string.Join(',', systems.Select(s => s.Id))); var scenario = dataLoader.NormalizeScenarioToAvailableSystems( catalog.Scenario, systems.Select(system => system.Id).ToList()); Console.WriteLine(string.Join(',', systems.Select(s => s.Id))); 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, catalog.Balance); var stations = CreateStations( scenario, systemsById, spatialLayout.SystemGraphs, spatialLayout.Celestials, catalog.ModuleDefinitions, catalog.ItemDefinitions); seedingService.InitializeStationStockpiles(stations); var refinery = seedingService.SelectRefineryStation(stations, scenario); var patrolRoutes = BuildPatrolRoutes(scenario, systemsById); var ships = CreateShips(scenario, systemsById, spatialLayout.Celestials, catalog.Balance, catalog.ShipDefinitions, patrolRoutes, stations, refinery); if (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(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 = worldGeneration.GeneratePlayerFaction ? seedingService.CreatePlayerFaction(factions, stations, ships, commanders, policies, nowUtc) : null; var claims = seedingService.CreateClaims(stations, spatialLayout.Celestials, nowUtc); var bootstrapWorld = new SimulationWorld { Label = "Split Viewer / Bootstrap World", Seed = WorldSeed, Balance = catalog.Balance, Systems = systemRuntimes, Celestials = spatialLayout.Celestials, Nodes = spatialLayout.Nodes, Wrecks = [], Stations = stations, Ships = ships, Factions = factions, PlayerFaction = playerFaction, Commanders = commanders, Claims = claims, ConstructionSites = [], MarketOrders = [], Policies = policies, ShipDefinitions = new Dictionary(catalog.ShipDefinitions, StringComparer.Ordinal), ItemDefinitions = new Dictionary(catalog.ItemDefinitions, StringComparer.Ordinal), ModuleDefinitions = new Dictionary(catalog.ModuleDefinitions, StringComparer.Ordinal), ModuleRecipes = new Dictionary(catalog.ModuleRecipes, StringComparer.Ordinal), Recipes = new Dictionary(catalog.Recipes, StringComparer.Ordinal), ProductionGraph = catalog.ProductionGraph, OrbitalTimeSeconds = WorldSeed * 97d, GeneratedAtUtc = nowUtc, }; var (constructionSites, marketOrders) = seedingService.CreateConstructionSites(bootstrapWorld); var world = new SimulationWorld { Label = "Split Viewer / Simulation World", Seed = WorldSeed, Balance = catalog.Balance, Systems = systemRuntimes, Celestials = spatialLayout.Celestials, Nodes = spatialLayout.Nodes, Wrecks = [], Stations = stations, Ships = ships, Factions = factions, PlayerFaction = playerFaction, Geopolitics = null, Commanders = commanders, Claims = claims, ConstructionSites = constructionSites, MarketOrders = marketOrders, Policies = policies, ShipDefinitions = new Dictionary(catalog.ShipDefinitions, StringComparer.Ordinal), ItemDefinitions = new Dictionary(catalog.ItemDefinitions, StringComparer.Ordinal), ModuleDefinitions = new Dictionary(catalog.ModuleDefinitions, StringComparer.Ordinal), ModuleRecipes = new Dictionary(catalog.ModuleRecipes, StringComparer.Ordinal), Recipes = new Dictionary(catalog.Recipes, StringComparer.Ordinal), ProductionGraph = catalog.ProductionGraph, OrbitalTimeSeconds = WorldSeed * 97d, GeneratedAtUtc = DateTimeOffset.UtcNow, }; var geopolitics = new GeopoliticalSimulationService(); geopolitics.Update(world, 0f, []); return world; } 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.Production .SelectMany(production => production.Wares.Select(ware => ware.ItemId)) .Concat(moduleDefinition.Products) .Distinct(StringComparer.Ordinal)) { if (!itemDefinitions.TryGetValue(wareId, out var itemDefinition)) { continue; } var storageModuleId = itemDefinition.CargoKind switch { "solid" => "module_arg_stor_solid_m_01", "liquid" => "module_arg_stor_liquid_m_01", _ => "module_arg_stor_container_m_01", }; 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 static List CreateShips( ScenarioDefinition scenario, IReadOnlyDictionary systemsById, IReadOnlyCollection celestials, BalanceDefinition balance, 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.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; } }