using SpaceGame.Api.Universe.Bootstrap; using static SpaceGame.Api.Shared.Runtime.ShipBehaviorKinds; using static SpaceGame.Api.Universe.Scenario.LoaderSupport; namespace SpaceGame.Api.Universe.Scenario; public sealed class WorldSeedingService(IStaticDataProvider staticData) { internal List CreateFactions( IReadOnlyCollection stations, IReadOnlyCollection ships) { var factionIds = stations .Select(station => station.FactionId) .Concat(ships.Select(ship => ship.FactionId)) .Where(factionId => !string.IsNullOrWhiteSpace(factionId)) .Distinct(StringComparer.Ordinal) .OrderBy(factionId => factionId, StringComparer.Ordinal) .ToList(); if (factionIds.Count == 0) { return []; } return factionIds.Select(CreateFaction).ToList(); } internal void BootstrapFactionEconomy( IReadOnlyCollection factions, IReadOnlyCollection stations) { foreach (var faction in factions) { faction.Credits = MathF.Max(faction.Credits, MinimumFactionCredits); var ownedStations = stations .Where(station => station.FactionId == faction.Id) .ToList(); var refineries = ownedStations .Where(station => string.Equals(StationSimulationService.DetermineStationRole(station), "refinery", StringComparison.Ordinal)) .ToList(); if (refineries.Count > 0) { foreach (var refinery in refineries) { refinery.Inventory["refinedmetals"] = MathF.Max(GetInventoryAmount(refinery.Inventory, "refinedmetals"), MinimumRefineryStock); } if (refineries.All(station => GetInventoryAmount(station.Inventory, "ore") < MinimumRefineryOre)) { refineries[0].Inventory["ore"] = MinimumRefineryOre; } } foreach (var shipyard in ownedStations.Where(station => HasInstalledModules(station, "module_gen_build_l_01"))) { shipyard.Inventory["refinedmetals"] = MathF.Max(GetInventoryAmount(shipyard.Inventory, "refinedmetals"), MinimumShipyardStock); } } } internal void InitializeStationStockpiles( IReadOnlyCollection stations, IReadOnlyDictionary moduleDefinitions) { foreach (var station in stations) { InitializeStationPopulation(station, moduleDefinitions); } } internal List CreateClaims( IReadOnlyCollection stations, IReadOnlyCollection anchors, DateTimeOffset nowUtc) { var stationsByAnchorId = stations .Where(station => !string.IsNullOrWhiteSpace(station.AnchorId)) .ToDictionary(station => station.AnchorId!, StringComparer.Ordinal); var claims = new List(); foreach (var anchor in anchors.Where(candidate => candidate.Kind == SpatialNodeKind.LagrangePoint)) { if (!stationsByAnchorId.TryGetValue(anchor.Id, out var station)) { continue; } claims.Add(new ClaimRuntime { Id = $"claim-{anchor.Id}", FactionId = station.FactionId, SystemId = anchor.SystemId, AnchorId = anchor.Id, PlacedAtUtc = nowUtc, ActivatesAtUtc = nowUtc.AddSeconds(8), State = ClaimStateKinds.Activating, Health = 100f, }); } return claims; } internal (List ConstructionSites, List MarketOrders) CreateConstructionSites( SimulationWorld world) { var sites = new List(); var orders = new List(); foreach (var station in world.Stations) { if (HasSatisfiedStarterObjectiveLayout(world, station)) { continue; } var moduleId = InfrastructureSimulationService.GetNextStationModuleToBuild(station, world); if (moduleId is null || station.AnchorId is null) { continue; } var claim = world.Claims.FirstOrDefault(candidate => string.Equals(candidate.AnchorId, station.AnchorId, StringComparison.Ordinal)); if (claim is null || !world.ModuleRecipes.TryGetValue(moduleId, out var recipe)) { continue; } var site = new ConstructionSiteRuntime { Id = $"site-{station.Id}", FactionId = station.FactionId, SystemId = station.SystemId, AnchorId = station.AnchorId, TargetKind = "station-module", TargetDefinitionId = "station", BlueprintId = moduleId, ClaimId = claim.Id, StationId = station.Id, State = claim.State == ClaimStateKinds.Active ? ConstructionSiteStateKinds.Active : ConstructionSiteStateKinds.Planned, }; foreach (var input in recipe.Inputs) { site.RequiredItems[input.ItemId] = input.Amount; site.DeliveredItems[input.ItemId] = 0f; var orderId = $"market-order-{station.Id}-{moduleId}-{input.ItemId}"; site.MarketOrderIds.Add(orderId); station.MarketOrderIds.Add(orderId); orders.Add(new MarketOrderRuntime { Id = orderId, FactionId = station.FactionId, StationId = station.Id, ConstructionSiteId = site.Id, Kind = MarketOrderKinds.Buy, ItemId = input.ItemId, Amount = input.Amount, RemainingAmount = input.Amount, Valuation = 1f, State = MarketOrderStateKinds.Open, }); } sites.Add(site); } return (sites, orders); } private static bool HasSatisfiedStarterObjectiveLayout(SimulationWorld world, StationRuntime station) { var role = StationSimulationService.DetermineStationRole(station); var objectiveModuleId = StarterStationLayoutResolver.ResolveObjectiveModuleId(role, station.FactionId, world.ModuleDefinitions); if (objectiveModuleId is null) { return false; } var requiredDockModuleId = StarterStationLayoutResolver.ResolveDockModuleId(station.FactionId, world.ModuleDefinitions); if (!station.InstalledModules.Contains(requiredDockModuleId, StringComparer.Ordinal) || !station.InstalledModules.Contains(objectiveModuleId, StringComparer.Ordinal)) { return false; } var powerModuleId = StarterStationLayoutResolver.ResolvePowerModuleId(station.FactionId, world.ModuleDefinitions); if (!string.Equals(objectiveModuleId, powerModuleId, StringComparison.Ordinal) && !station.InstalledModules.Contains(powerModuleId, StringComparer.Ordinal)) { return false; } foreach (var storageModuleId in StarterStationLayoutResolver.ResolveRequiredStorageModuleIds( objectiveModuleId, station.FactionId, world.ModuleDefinitions, world.ItemDefinitions, world.Recipes)) { if (!station.InstalledModules.Contains(storageModuleId, StringComparer.Ordinal)) { return false; } } return true; } internal List CreatePolicies(IReadOnlyCollection factions) { var policies = new List(factions.Count); foreach (var faction in factions) { var policyId = $"policy-{faction.Id}"; faction.DefaultPolicySetId = policyId; policies.Add(new PolicySetRuntime { Id = policyId, OwnerKind = "faction", OwnerId = faction.Id, }); } return policies; } internal List CreateCommanders( IReadOnlyCollection factions, IReadOnlyCollection stations, IReadOnlyCollection ships) { var commanders = new List(); var factionCommanders = new Dictionary(StringComparer.Ordinal); var factionsById = factions.ToDictionary(faction => faction.Id, StringComparer.Ordinal); foreach (var faction in factions) { var commander = new CommanderRuntime { Id = $"commander-faction-{faction.Id}", Kind = CommanderKind.Faction, FactionId = faction.Id, ControlledEntityId = faction.Id, PolicySetId = faction.DefaultPolicySetId, Doctrine = "strategic-control", }; commanders.Add(commander); factionCommanders[faction.Id] = commander; faction.CommanderIds.Add(commander.Id); } foreach (var station in stations) { if (!factionCommanders.TryGetValue(station.FactionId, out var parentCommander)) { continue; } var commander = new CommanderRuntime { Id = $"commander-station-{station.Id}", Kind = CommanderKind.Station, FactionId = station.FactionId, ParentCommanderId = parentCommander.Id, ControlledEntityId = station.Id, PolicySetId = parentCommander.PolicySetId, Doctrine = "station-control", }; station.CommanderId = commander.Id; station.PolicySetId = parentCommander.PolicySetId; parentCommander.SubordinateCommanderIds.Add(commander.Id); factionsById[station.FactionId].CommanderIds.Add(commander.Id); commanders.Add(commander); } foreach (var ship in ships) { if (!factionCommanders.TryGetValue(ship.FactionId, out var parentCommander)) { continue; } var commander = new CommanderRuntime { Id = $"commander-ship-{ship.Id}", Kind = CommanderKind.Ship, FactionId = ship.FactionId, ParentCommanderId = parentCommander.Id, ControlledEntityId = ship.Id, PolicySetId = parentCommander.PolicySetId, Doctrine = "ship-control", }; ship.CommanderId = commander.Id; ship.PolicySetId = parentCommander.PolicySetId; parentCommander.SubordinateCommanderIds.Add(commander.Id); factionsById[ship.FactionId].CommanderIds.Add(commander.Id); commanders.Add(commander); } return commanders; } internal PlayerFactionRuntime CreatePlayerFaction( IReadOnlyCollection factions, IReadOnlyCollection stations, IReadOnlyCollection ships, IReadOnlyCollection commanders, IReadOnlyCollection policies, DateTimeOffset nowUtc) { var sovereignFaction = factions.OrderBy(faction => faction.Id, StringComparer.Ordinal).FirstOrDefault() ?? throw new InvalidOperationException("Cannot create a player faction without at least one faction in the world."); var player = new PlayerFactionRuntime { Id = "player-faction", Label = $"{sovereignFaction.Label} Command", SovereignFactionId = sovereignFaction.Id, CreatedAtUtc = nowUtc, UpdatedAtUtc = nowUtc, }; foreach (var shipId in ships.Where(ship => ship.FactionId == sovereignFaction.Id).Select(ship => ship.Id)) { player.AssetRegistry.ShipIds.Add(shipId); } foreach (var stationId in stations.Where(station => station.FactionId == sovereignFaction.Id).Select(station => station.Id)) { player.AssetRegistry.StationIds.Add(stationId); } foreach (var commanderId in commanders.Where(commander => commander.FactionId == sovereignFaction.Id).Select(commander => commander.Id)) { player.AssetRegistry.CommanderIds.Add(commanderId); } foreach (var policy in policies.Where(policy => string.Equals(policy.OwnerId, sovereignFaction.Id, StringComparison.Ordinal))) { player.AssetRegistry.PolicySetIds.Add(policy.Id); } player.Policies.Add(new PlayerFactionPolicyRuntime { Id = "player-core-policy", Label = "Core Empire Policy", ScopeKind = "player-faction", ScopeId = player.Id, PolicySetId = sovereignFaction.DefaultPolicySetId, TradeAccessPolicy = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.TradeAccessPolicy ?? "owner-and-allies", DockingAccessPolicy = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.DockingAccessPolicy ?? "owner-and-allies", ConstructionAccessPolicy = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.ConstructionAccessPolicy ?? "owner-only", OperationalRangePolicy = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.OperationalRangePolicy ?? "unrestricted", CombatEngagementPolicy = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.CombatEngagementPolicy ?? "defensive", AvoidHostileSystems = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.AvoidHostileSystems ?? true, FleeHullRatio = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.FleeHullRatio ?? 0.35f, UpdatedAtUtc = nowUtc, }); if (policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId) is { } defaultPolicy) { foreach (var systemId in defaultPolicy.BlacklistedSystemIds) { player.Policies[0].BlacklistedSystemIds.Add(systemId); } } player.AutomationPolicies.Add(new PlayerAutomationPolicyRuntime { Id = "player-core-automation", Label = "Core Automation", ScopeKind = "player-faction", ScopeId = player.Id, BehaviorKind = Idle, UpdatedAtUtc = nowUtc, }); player.Reserves.Add(new PlayerReserveGroupRuntime { Id = "player-core-reserve", Label = "Strategic Reserve", ReserveKind = "military", UpdatedAtUtc = nowUtc, }); player.AssetRegistry.ReserveIds.Add("player-core-reserve"); return player; } internal FactionRuntime CreateFaction(string factionId) { if (!staticData.FactionDefinitions.TryGetValue(factionId, out var definition)) { throw new InvalidOperationException($"Faction '{factionId}' is not defined in static data."); } return new FactionRuntime { Id = definition.Id, Label = definition.Label, Color = ResolveFactionColor(definition), Credits = MinimumFactionCredits, }; } private static string ResolveFactionColor(FactionDefinition definition) => definition.Id switch { "alliance" => "#c084fc", "antigone" => "#f97316", "argon" => "#3b82f6", "boron" => "#14b8a6", "freesplit" => "#ef4444", "hatikvah" => "#84cc16", "holyorder" => "#d97706", "loanshark" => "#f59e0b", "ministry" => "#a3e635", "paranid" => "#eab308", "pioneers" => "#60a5fa", "scaleplate" => "#94a3b8", "scavenger" => "#64748b", "split" => "#b91c1c", "teladi" => "#22c55e", "terran" => "#38bdf8", "trinity" => "#2dd4bf", "xenon" => "#9ca3af", _ => definition.RaceId switch { "argon" => "#3b82f6", "boron" => "#14b8a6", "paranid" => "#eab308", "split" => "#b91c1c", "teladi" => "#22c55e", "terran" => "#38bdf8", "xenon" => "#9ca3af", _ => "#94a3b8", }, }; private static void InitializeStationPopulation( StationRuntime station, IReadOnlyDictionary moduleDefinitions) { station.PopulationCapacity = SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport.GetStationSupportedPopulation(moduleDefinitions, station); station.WorkforceRequired = SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport.GetStationRequiredWorkforce(moduleDefinitions, station); station.Population = station.PopulationCapacity > 40f ? MathF.Min(station.PopulationCapacity * 0.65f, station.WorkforceRequired * 1.05f) : MathF.Min(28f, station.PopulationCapacity); station.WorkforceEffectiveRatio = ComputeWorkforceRatio(station.Population, station.WorkforceRequired); } }