Refactor world bootstrap and allow empty startup worlds

This commit is contained in:
2026-03-29 13:22:48 -04:00
parent 640e147ea8
commit 0bb72bee35
79 changed files with 173146 additions and 9235 deletions

View File

@@ -1,8 +1,9 @@
using SpaceGame.Api.Universe.Bootstrap;
using static SpaceGame.Api.Universe.Scenario.LoaderSupport;
namespace SpaceGame.Api.Universe.Scenario;
public sealed class WorldSeedingService
public sealed class WorldSeedingService(IStaticDataProvider staticData)
{
internal List<FactionRuntime> CreateFactions(
IReadOnlyCollection<StationRuntime> stations,
@@ -18,7 +19,7 @@ public sealed class WorldSeedingService
if (factionIds.Count == 0)
{
factionIds.Add(DefaultFactionId);
return [];
}
return factionIds.Select(CreateFaction).ToList();
@@ -70,15 +71,6 @@ public sealed class WorldSeedingService
}
}
internal StationRuntime? SelectRefineryStation(IReadOnlyCollection<StationRuntime> stations, ScenarioDefinition scenario)
{
return stations.FirstOrDefault(station =>
string.Equals(StationSimulationService.DetermineStationRole(station), "refinery", StringComparison.Ordinal) &&
station.SystemId == scenario.MiningDefaults.RefinerySystemId)
?? stations.FirstOrDefault(station =>
string.Equals(StationSimulationService.DetermineStationRole(station), "refinery", StringComparison.Ordinal));
}
internal List<ClaimRuntime> CreateClaims(
IReadOnlyCollection<StationRuntime> stations,
IReadOnlyCollection<CelestialRuntime> celestials,
@@ -183,39 +175,32 @@ public sealed class WorldSeedingService
private static bool HasSatisfiedStarterObjectiveLayout(SimulationWorld world, StationRuntime station)
{
var role = StationSimulationService.DetermineStationRole(station);
var objectiveModuleId = role switch
{
"power" => "module_gen_prod_energycells_01",
"refinery" => "module_gen_prod_refinedmetals_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,
};
var objectiveModuleId = StarterStationLayoutResolver.ResolveObjectiveModuleId(role, station.FactionId, world.ModuleDefinitions);
if (objectiveModuleId is null)
{
return false;
}
if (!station.InstalledModules.Contains("module_arg_dock_m_01_lowtech", StringComparer.Ordinal)
var requiredDockModuleId = StarterStationLayoutResolver.ResolveDockModuleId(station.FactionId, world.ModuleDefinitions);
if (!station.InstalledModules.Contains(requiredDockModuleId, StringComparer.Ordinal)
|| !station.InstalledModules.Contains(objectiveModuleId, StringComparer.Ordinal))
{
return false;
}
if (!string.Equals(objectiveModuleId, "module_gen_prod_energycells_01", StringComparison.Ordinal)
&& !station.InstalledModules.Contains("module_gen_prod_energycells_01", StringComparer.Ordinal))
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 GetRequiredStorageModulesForInstalledObjective(world, objectiveModuleId))
foreach (var storageModuleId in StarterStationLayoutResolver.ResolveRequiredStorageModuleIds(
objectiveModuleId,
station.FactionId,
world.ModuleDefinitions,
world.ItemDefinitions))
{
if (!station.InstalledModules.Contains(storageModuleId, StringComparer.Ordinal))
{
@@ -226,30 +211,6 @@ public sealed class WorldSeedingService
return true;
}
private static IEnumerable<string> GetRequiredStorageModulesForInstalledObjective(SimulationWorld world, string moduleId)
{
if (!world.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 (!world.ItemDefinitions.TryGetValue(wareId, out var itemDefinition))
{
continue;
}
if (SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport.GetStorageRequirement(world.ModuleDefinitions, itemDefinition.CargoKind) is { } storageModuleId)
{
yield return storageModuleId;
}
}
}
internal List<PolicySetRuntime> CreatePolicies(IReadOnlyCollection<FactionRuntime> factions)
{
var policies = new List<PolicySetRuntime>(factions.Count);
@@ -355,8 +316,8 @@ public sealed class WorldSeedingService
IReadOnlyCollection<PolicySetRuntime> policies,
DateTimeOffset nowUtc)
{
var sovereignFaction = factions.FirstOrDefault(faction => string.Equals(faction.Id, DefaultFactionId, StringComparison.Ordinal))
?? factions.OrderBy(faction => faction.Id, StringComparer.Ordinal).First();
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
{
@@ -434,122 +395,55 @@ public sealed class WorldSeedingService
return player;
}
internal static DefaultBehaviorRuntime CreateBehavior(
ShipDefinition definition,
string systemId,
string factionId,
ScenarioDefinition scenario,
IReadOnlyDictionary<string, List<Vector3>> patrolRoutes,
IReadOnlyCollection<StationRuntime> stations,
StationRuntime? refinery)
private FactionRuntime CreateFaction(string factionId)
{
var homeStation = stations.FirstOrDefault(station =>
string.Equals(station.FactionId, factionId, StringComparison.Ordinal)
&& string.Equals(station.SystemId, systemId, StringComparison.Ordinal))
?? stations.FirstOrDefault(station => string.Equals(station.FactionId, factionId, StringComparison.Ordinal))
?? refinery;
if (string.Equals(definition.Kind, "construction", StringComparison.Ordinal) && homeStation is not null)
if (!staticData.FactionDefinitions.TryGetValue(factionId, out var definition))
{
return new DefaultBehaviorRuntime
{
Kind = "construct-station",
HomeSystemId = homeStation.SystemId,
HomeStationId = homeStation.Id,
PreferredConstructionSiteId = null,
};
throw new InvalidOperationException($"Faction '{factionId}' is not defined in static data.");
}
if (HasCapabilities(definition, "mining") && homeStation is not null)
return new FactionRuntime
{
return new DefaultBehaviorRuntime
{
Kind = definition.CargoCapacity >= 120f ? "expert-auto-mine" : "advanced-auto-mine",
HomeSystemId = homeStation.SystemId,
HomeStationId = homeStation.Id,
AreaSystemId = scenario.MiningDefaults.NodeSystemId,
MaxSystemRange = definition.CargoCapacity >= 120f ? 3 : 1,
};
}
if (string.Equals(definition.Kind, "transport", StringComparison.Ordinal))
{
return new DefaultBehaviorRuntime
{
Kind = "advanced-auto-trade",
HomeSystemId = homeStation?.SystemId ?? systemId,
HomeStationId = homeStation?.Id,
MaxSystemRange = 2,
};
}
if (string.Equals(definition.Kind, "military", StringComparison.Ordinal) && patrolRoutes.TryGetValue(systemId, out var route))
{
return new DefaultBehaviorRuntime
{
Kind = "patrol",
HomeSystemId = homeStation?.SystemId ?? systemId,
HomeStationId = homeStation?.Id,
AreaSystemId = systemId,
PatrolPoints = route,
PatrolIndex = 0,
};
}
return new DefaultBehaviorRuntime
{
Kind = "idle",
HomeSystemId = homeStation?.SystemId ?? systemId,
HomeStationId = homeStation?.Id,
Id = definition.Id,
Label = definition.Label,
Color = ResolveFactionColor(definition),
Credits = MinimumFactionCredits,
};
}
internal static ShipSkillProfileRuntime CreateSkills(ShipDefinition definition)
{
return definition.Kind switch
{
"transport" => new ShipSkillProfileRuntime { Navigation = 3, Trade = 4, Mining = 1, Combat = 1, Construction = 1 },
"construction" => new ShipSkillProfileRuntime { Navigation = 3, Trade = 1, Mining = 1, Combat = 1, Construction = 4 },
"military" => new ShipSkillProfileRuntime { Navigation = 4, Trade = 1, Mining = 1, Combat = 4, Construction = 1 },
_ when HasCapabilities(definition, "mining") => new ShipSkillProfileRuntime { Navigation = 3, Trade = 1, Mining = 4, Combat = 1, Construction = 1 },
_ => new ShipSkillProfileRuntime { Navigation = 3, Trade = 2, Mining = 1, Combat = 1, Construction = 1 },
};
}
private static FactionRuntime CreateFaction(string factionId)
{
return factionId switch
{
DefaultFactionId => new FactionRuntime
{
Id = factionId,
Label = "Sol Dominion",
Color = "#7ed4ff",
Credits = MinimumFactionCredits,
},
"asterion-league" => new FactionRuntime
{
Id = factionId,
Label = "Asterion League",
Color = "#ff8f70",
Credits = MinimumFactionCredits,
},
"nadir-syndicate" => new FactionRuntime
{
Id = factionId,
Label = "Nadir Syndicate",
Color = "#91e6a8",
Credits = MinimumFactionCredits,
},
_ => new FactionRuntime
{
Id = factionId,
Label = ToFactionLabel(factionId),
Color = "#c7d2e0",
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,
@@ -563,12 +457,4 @@ public sealed class WorldSeedingService
station.WorkforceEffectiveRatio = ComputeWorkforceRatio(station.Population, station.WorkforceRequired);
}
private static string ToFactionLabel(string factionId)
{
return string.Join(" ",
factionId
.Split('-', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(segment => char.ToUpperInvariant(segment[0]) + segment[1..]));
}
}