463 lines
18 KiB
C#
463 lines
18 KiB
C#
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<FactionRuntime> CreateFactions(
|
|
IReadOnlyCollection<StationRuntime> stations,
|
|
IReadOnlyCollection<ShipRuntime> 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<FactionRuntime> factions,
|
|
IReadOnlyCollection<StationRuntime> 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<StationRuntime> stations,
|
|
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions)
|
|
{
|
|
foreach (var station in stations)
|
|
{
|
|
InitializeStationPopulation(station, moduleDefinitions);
|
|
}
|
|
}
|
|
|
|
internal List<ClaimRuntime> CreateClaims(
|
|
IReadOnlyCollection<StationRuntime> stations,
|
|
IReadOnlyCollection<CelestialRuntime> celestials,
|
|
DateTimeOffset nowUtc)
|
|
{
|
|
var stationsByCelestialId = stations
|
|
.Where(station => station.CelestialId is not null)
|
|
.ToDictionary(station => station.CelestialId!, StringComparer.Ordinal);
|
|
var claims = new List<ClaimRuntime>();
|
|
|
|
foreach (var celestial in celestials.Where(c => c.Kind == SpatialNodeKind.LagrangePoint))
|
|
{
|
|
if (!stationsByCelestialId.TryGetValue(celestial.Id, out var station))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
claims.Add(new ClaimRuntime
|
|
{
|
|
Id = $"claim-{celestial.Id}",
|
|
FactionId = station.FactionId,
|
|
SystemId = celestial.SystemId,
|
|
CelestialId = celestial.Id,
|
|
PlacedAtUtc = nowUtc,
|
|
ActivatesAtUtc = nowUtc.AddSeconds(8),
|
|
State = ClaimStateKinds.Activating,
|
|
Health = 100f,
|
|
});
|
|
}
|
|
|
|
return claims;
|
|
}
|
|
|
|
internal (List<ConstructionSiteRuntime> ConstructionSites, List<MarketOrderRuntime> MarketOrders) CreateConstructionSites(
|
|
SimulationWorld world)
|
|
{
|
|
var sites = new List<ConstructionSiteRuntime>();
|
|
var orders = new List<MarketOrderRuntime>();
|
|
|
|
foreach (var station in world.Stations)
|
|
{
|
|
if (HasSatisfiedStarterObjectiveLayout(world, station))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var moduleId = InfrastructureSimulationService.GetNextStationModuleToBuild(station, world);
|
|
if (moduleId is null || station.CelestialId is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var claim = world.Claims.FirstOrDefault(candidate => candidate.CelestialId == station.CelestialId);
|
|
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,
|
|
CelestialId = station.CelestialId,
|
|
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<PolicySetRuntime> CreatePolicies(IReadOnlyCollection<FactionRuntime> factions)
|
|
{
|
|
var policies = new List<PolicySetRuntime>(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<CommanderRuntime> CreateCommanders(
|
|
IReadOnlyCollection<FactionRuntime> factions,
|
|
IReadOnlyCollection<StationRuntime> stations,
|
|
IReadOnlyCollection<ShipRuntime> ships)
|
|
{
|
|
var commanders = new List<CommanderRuntime>();
|
|
var factionCommanders = new Dictionary<string, CommanderRuntime>(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<FactionRuntime> factions,
|
|
IReadOnlyCollection<StationRuntime> stations,
|
|
IReadOnlyCollection<ShipRuntime> ships,
|
|
IReadOnlyCollection<CommanderRuntime> commanders,
|
|
IReadOnlyCollection<PolicySetRuntime> 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<string, ModuleDefinition> 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);
|
|
}
|
|
|
|
}
|