384 lines
16 KiB
C#
384 lines
16 KiB
C#
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<BalanceOptions> 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<string, ShipDefinition>(staticData.ShipDefinitions, StringComparer.Ordinal),
|
|
ItemDefinitions = new Dictionary<string, ItemDefinition>(staticData.ItemDefinitions, StringComparer.Ordinal),
|
|
ModuleDefinitions = new Dictionary<string, ModuleDefinition>(staticData.ModuleDefinitions, StringComparer.Ordinal),
|
|
ModuleRecipes = new Dictionary<string, ModuleRecipeDefinition>(staticData.ModuleRecipes, StringComparer.Ordinal),
|
|
Recipes = new Dictionary<string, RecipeDefinition>(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<string> 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<string, float>(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<StationRuntime> CreateStations(
|
|
ScenarioDefinition scenario,
|
|
IReadOnlyDictionary<string, SystemRuntime> systemsById,
|
|
IReadOnlyDictionary<string, SystemSpatialGraph> systemGraphs,
|
|
IReadOnlyCollection<CelestialRuntime> celestials,
|
|
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
|
|
IReadOnlyDictionary<string, ItemDefinition> itemDefinitions)
|
|
{
|
|
var stations = new List<StationRuntime>();
|
|
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<string> BuildStartingModules(
|
|
InitialStationDefinition plan,
|
|
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
|
|
IReadOnlyDictionary<string, ItemDefinition> itemDefinitions)
|
|
{
|
|
var startingModules = new List<string>(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<string> GetRequiredStartingStorageModules(
|
|
string moduleId,
|
|
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
|
|
IReadOnlyDictionary<string, ItemDefinition> 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<string> modules, string moduleId)
|
|
{
|
|
if (!modules.Contains(moduleId, StringComparer.Ordinal))
|
|
{
|
|
modules.Add(moduleId);
|
|
}
|
|
}
|
|
|
|
private static Dictionary<string, List<Vector3>> BuildPatrolRoutes(
|
|
ScenarioDefinition scenario,
|
|
IReadOnlyDictionary<string, SystemRuntime> 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<ShipRuntime> CreateShips(
|
|
ScenarioDefinition scenario,
|
|
IReadOnlyDictionary<string, SystemRuntime> systemsById,
|
|
IReadOnlyCollection<CelestialRuntime> celestials,
|
|
IReadOnlyDictionary<string, ShipDefinition> shipDefinitions,
|
|
IReadOnlyDictionary<string, List<Vector3>> patrolRoutes,
|
|
IReadOnlyCollection<StationRuntime> stations,
|
|
StationRuntime? refinery)
|
|
{
|
|
var ships = new List<ShipRuntime>();
|
|
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;
|
|
}
|
|
}
|