feat: massive AI generation

This commit is contained in:
2026-03-21 02:21:05 -04:00
parent 3b56785f9a
commit 6ccc708ae1
80 changed files with 16929 additions and 5427 deletions

View File

@@ -17,7 +17,9 @@ public sealed record WorldSnapshot(
IReadOnlyList<MarketOrderSnapshot> MarketOrders,
IReadOnlyList<PolicySetSnapshot> Policies,
IReadOnlyList<ShipSnapshot> Ships,
IReadOnlyList<FactionSnapshot> Factions);
IReadOnlyList<FactionSnapshot> Factions,
PlayerFactionSnapshot? PlayerFaction,
GeopoliticalStateSnapshot? Geopolitics);
public sealed record WorldDelta(
long Sequence,
@@ -36,6 +38,8 @@ public sealed record WorldDelta(
IReadOnlyList<PolicySetDelta> Policies,
IReadOnlyList<ShipDelta> Ships,
IReadOnlyList<FactionDelta> Factions,
PlayerFactionSnapshot? PlayerFaction,
GeopoliticalStateSnapshot? Geopolitics,
ObserverScope? Scope = null);
public sealed record SimulationEventRecord(

View File

@@ -9,9 +9,12 @@ public sealed class SimulationWorld
public required List<SystemRuntime> Systems { get; init; }
public required List<ResourceNodeRuntime> Nodes { get; init; }
public required List<CelestialRuntime> Celestials { get; init; }
public required List<WreckRuntime> Wrecks { get; init; }
public required List<StationRuntime> Stations { get; init; }
public required List<ShipRuntime> Ships { get; init; }
public required List<FactionRuntime> Factions { get; init; }
public PlayerFactionRuntime? PlayerFaction { get; set; }
public GeopoliticalStateRuntime? Geopolitics { get; set; }
public required List<CommanderRuntime> Commanders { get; init; }
public required List<ClaimRuntime> Claims { get; init; }
public required List<ConstructionSiteRuntime> ConstructionSites { get; init; }

View File

@@ -36,6 +36,18 @@ public sealed class CelestialRuntime
public string LastDeltaSignature { get; set; } = string.Empty;
}
public sealed class WreckRuntime
{
public required string Id { get; init; }
public required string SourceKind { get; init; }
public required string SourceEntityId { get; init; }
public required string SystemId { get; set; }
public required Vector3 Position { get; set; }
public required string ItemId { get; set; }
public float RemainingAmount { get; set; }
public float MaxAmount { get; init; }
}
public sealed class ShipSpatialStateRuntime
{
public string SpaceLayer { get; set; } = SpaceLayerKinds.LocalSpace;

View File

@@ -15,10 +15,16 @@ internal sealed class WorldBuilder(
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
{
@@ -42,11 +48,29 @@ internal sealed class WorldBuilder(
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
{
@@ -56,9 +80,11 @@ internal sealed class WorldBuilder(
Systems = systemRuntimes,
Celestials = spatialLayout.Celestials,
Nodes = spatialLayout.Nodes,
Wrecks = [],
Stations = stations,
Ships = ships,
Factions = factions,
PlayerFaction = playerFaction,
Commanders = commanders,
Claims = claims,
ConstructionSites = [],
@@ -75,7 +101,7 @@ internal sealed class WorldBuilder(
};
var (constructionSites, marketOrders) = seedingService.CreateConstructionSites(bootstrapWorld);
return new SimulationWorld
var world = new SimulationWorld
{
Label = "Split Viewer / Simulation World",
Seed = WorldSeed,
@@ -83,9 +109,12 @@ internal sealed class WorldBuilder(
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,
@@ -100,6 +129,10 @@ internal sealed class WorldBuilder(
OrbitalTimeSeconds = WorldSeed * 97d,
GeneratedAtUtc = DateTimeOffset.UtcNow,
};
var geopolitics = new GeopoliticalSimulationService();
geopolitics.Update(world, 0f, []);
return world;
}
private static List<StationRuntime> CreateStations(
@@ -291,7 +324,7 @@ internal sealed class WorldBuilder(
patrolRoutes,
stations,
refinery),
ControllerTask = new ControllerTaskRuntime { Kind = ControllerTaskKind.Idle, Threshold = balance.ArrivalThreshold, Status = WorkStatus.Pending },
Skills = WorldSeedingService.CreateSkills(definition),
Health = definition.MaxHealth,
});

View File

@@ -286,16 +286,9 @@ internal sealed class WorldSeedingService
FactionId = faction.Id,
ControlledEntityId = faction.Id,
PolicySetId = faction.DefaultPolicySetId,
Doctrine = "strategic-expansionist",
Doctrine = "strategic-control",
};
commander.Goals.Add("control-all-systems");
commander.Goals.Add("control-five-systems-fast");
commander.Goals.Add("expand-industrial-base");
commander.Goals.Add("grow-war-fleet");
commander.Goals.Add("deter-pirate-harassment");
commander.Goals.Add("contest-rival-expansion");
commanders.Add(commander);
factionCommanders[faction.Id] = commander;
faction.CommanderIds.Add(commander.Id);
@@ -316,7 +309,7 @@ internal sealed class WorldSeedingService
ParentCommanderId = parentCommander.Id,
ControlledEntityId = station.Id,
PolicySetId = parentCommander.PolicySetId,
Doctrine = "station-default",
Doctrine = "station-control",
};
station.CommanderId = commander.Id;
@@ -341,16 +334,9 @@ internal sealed class WorldSeedingService
ParentCommanderId = parentCommander.Id,
ControlledEntityId = ship.Id,
PolicySetId = parentCommander.PolicySetId,
Doctrine = "ship-default",
ActiveBehavior = CopyBehavior(ship.DefaultBehavior),
ActiveTask = CopyTask(ship.ControllerTask, null),
Doctrine = "ship-control",
};
if (ship.Order is not null)
{
commander.ActiveOrder = CopyOrder(ship.Order);
}
ship.CommanderId = commander.Id;
ship.PolicySetId = parentCommander.PolicySetId;
parentCommander.SubordinateCommanderIds.Add(commander.Id);
@@ -361,6 +347,93 @@ internal sealed class WorldSeedingService
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.FirstOrDefault(faction => string.Equals(faction.Id, DefaultFactionId, StringComparison.Ordinal))
?? factions.OrderBy(faction => faction.Id, StringComparer.Ordinal).First();
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 static DefaultBehaviorRuntime CreateBehavior(
ShipDefinition definition,
string systemId,
@@ -381,22 +454,32 @@ internal sealed class WorldSeedingService
return new DefaultBehaviorRuntime
{
Kind = "construct-station",
StationId = homeStation.Id,
Phase = "travel-to-station",
HomeSystemId = homeStation.SystemId,
HomeStationId = homeStation.Id,
PreferredConstructionSiteId = null,
};
}
if (HasCapabilities(definition, "mining") && homeStation is not null)
{
return CreateResourceHarvestBehavior("auto-mine", scenario.MiningDefaults.NodeSystemId, homeStation.Id);
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 = "trade-haul",
Phase = "travel-to-source",
Kind = "advanced-auto-trade",
HomeSystemId = homeStation?.SystemId ?? systemId,
HomeStationId = homeStation?.Id,
MaxSystemRange = 2,
};
}
@@ -405,7 +488,9 @@ internal sealed class WorldSeedingService
return new DefaultBehaviorRuntime
{
Kind = "patrol",
StationId = homeStation?.Id,
HomeSystemId = homeStation?.SystemId ?? systemId,
HomeStationId = homeStation?.Id,
AreaSystemId = systemId,
PatrolPoints = route,
PatrolIndex = 0,
};
@@ -414,6 +499,20 @@ internal sealed class WorldSeedingService
return new DefaultBehaviorRuntime
{
Kind = "idle",
HomeSystemId = homeStation?.SystemId ?? systemId,
HomeStationId = homeStation?.Id,
};
}
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 },
};
}
@@ -471,43 +570,4 @@ internal sealed class WorldSeedingService
.Select(segment => char.ToUpperInvariant(segment[0]) + segment[1..]));
}
private static DefaultBehaviorRuntime CreateResourceHarvestBehavior(string kind, string areaSystemId, string stationId) => new()
{
Kind = kind,
AreaSystemId = areaSystemId,
StationId = stationId,
Phase = "travel-to-node",
};
private static CommanderBehaviorRuntime CopyBehavior(DefaultBehaviorRuntime behavior) => new()
{
Kind = behavior.Kind,
AreaSystemId = behavior.AreaSystemId,
TargetEntityId = behavior.TargetEntityId,
ItemId = behavior.ItemId,
ModuleId = behavior.ModuleId,
NodeId = behavior.NodeId,
Phase = behavior.Phase,
PatrolIndex = behavior.PatrolIndex,
StationId = behavior.StationId,
};
private static CommanderOrderRuntime CopyOrder(ShipOrderRuntime order) => new()
{
Kind = order.Kind,
Status = order.Status,
DestinationSystemId = order.DestinationSystemId,
DestinationPosition = order.DestinationPosition,
};
private static CommanderTaskRuntime CopyTask(ControllerTaskRuntime task, string? targetNodeId) => new()
{
Kind = task.Kind.ToContractValue(),
Status = task.Status,
TargetEntityId = task.TargetEntityId,
TargetNodeId = targetNodeId ?? task.TargetNodeId,
TargetPosition = task.TargetPosition,
TargetSystemId = task.TargetSystemId,
Threshold = task.Threshold,
};
}

View File

@@ -2,6 +2,7 @@ namespace SpaceGame.Api.Universe.Simulation;
public sealed class WorldGenerationOptions
{
public int TargetSystemCount { get; init; } = 160;
public int TargetSystemCount { get; init; }
public int AiControllerFactionCount { get; init; }
public bool GeneratePlayerFaction { get; init; }
}

View File

@@ -14,6 +14,7 @@ public sealed class WorldService(
private readonly OrbitalSimulationSnapshot _orbitalSimulation = new(orbitalSimulationOptions.Value.SimulatedSecondsPerRealSecond);
private readonly ScenarioLoader _loader = new(environment.ContentRootPath, worldGenerationOptions.Value);
private readonly SimulationEngine _engine = new(orbitalSimulationOptions.Value);
private readonly PlayerFactionService _playerFaction = new();
private readonly Dictionary<Guid, SubscriptionState> _subscribers = [];
private readonly Queue<WorldDelta> _history = [];
private SimulationWorld _world = new ScenarioLoader(environment.ContentRootPath, worldGenerationOptions.Value).Load();
@@ -74,6 +75,156 @@ public sealed class WorldService(
}
}
public ShipSnapshot? EnqueueShipOrder(string shipId, ShipOrderCommandRequest request)
{
lock (_sync)
{
var ship = _playerFaction.EnqueueDirectShipOrder(_world, shipId, request);
if (ship is null)
{
return null;
}
return GetShipSnapshotUnsafe(ship.Id);
}
}
public ShipSnapshot? RemoveShipOrder(string shipId, string orderId)
{
lock (_sync)
{
var ship = _playerFaction.RemoveDirectShipOrder(_world, shipId, orderId);
if (ship is null)
{
return null;
}
return GetShipSnapshotUnsafe(ship.Id);
}
}
public ShipSnapshot? UpdateShipDefaultBehavior(string shipId, ShipDefaultBehaviorCommandRequest request)
{
lock (_sync)
{
var ship = _playerFaction.ConfigureDirectShipBehavior(_world, shipId, request);
if (ship is null)
{
return null;
}
return GetShipSnapshotUnsafe(ship.Id);
}
}
public PlayerFactionSnapshot? GetPlayerFaction()
{
lock (_sync)
{
_playerFaction.EnsureDomain(_world);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? CreatePlayerOrganization(PlayerOrganizationCommandRequest request)
{
lock (_sync)
{
_playerFaction.CreateOrganization(_world, request);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? DeletePlayerOrganization(string organizationId)
{
lock (_sync)
{
_playerFaction.DeleteOrganization(_world, organizationId);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? UpdatePlayerOrganizationMembership(string organizationId, PlayerOrganizationMembershipCommandRequest request)
{
lock (_sync)
{
_playerFaction.UpdateOrganizationMembership(_world, organizationId, request);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? UpsertPlayerDirective(string? directiveId, PlayerDirectiveCommandRequest request)
{
lock (_sync)
{
_playerFaction.UpsertDirective(_world, directiveId, request);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? DeletePlayerDirective(string directiveId)
{
lock (_sync)
{
_playerFaction.DeleteDirective(_world, directiveId);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? UpsertPlayerPolicy(string? policyId, PlayerPolicyCommandRequest request)
{
lock (_sync)
{
_playerFaction.UpsertPolicy(_world, policyId, request);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? UpsertPlayerAutomationPolicy(string? automationPolicyId, PlayerAutomationPolicyCommandRequest request)
{
lock (_sync)
{
_playerFaction.UpsertAutomationPolicy(_world, automationPolicyId, request);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? UpsertPlayerReinforcementPolicy(string? reinforcementPolicyId, PlayerReinforcementPolicyCommandRequest request)
{
lock (_sync)
{
_playerFaction.UpsertReinforcementPolicy(_world, reinforcementPolicyId, request);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? UpsertPlayerProductionProgram(string? productionProgramId, PlayerProductionProgramCommandRequest request)
{
lock (_sync)
{
_playerFaction.UpsertProductionProgram(_world, productionProgramId, request);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? UpsertPlayerAssignment(string assetId, PlayerAssetAssignmentCommandRequest request)
{
lock (_sync)
{
_playerFaction.UpsertAssignment(_world, assetId, request);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? UpdatePlayerStrategicIntent(PlayerStrategicIntentCommandRequest request)
{
lock (_sync)
{
_playerFaction.UpdateStrategicIntent(_world, request);
return GetPlayerFactionSnapshotUnsafe();
}
}
public ChannelReader<WorldDelta> Subscribe(ObserverScope scope, long afterSequence, CancellationToken cancellationToken)
{
var channel = Channel.CreateUnbounded<WorldDelta>(new UnboundedChannelOptions
@@ -158,7 +309,9 @@ public sealed class WorldService(
[],
[],
[],
[]);
[],
null,
null);
_history.Enqueue(resetDelta);
foreach (var subscriber in _subscribers.Values.ToList())
@@ -203,6 +356,12 @@ public sealed class WorldService(
};
}
private ShipSnapshot? GetShipSnapshotUnsafe(string shipId) =>
_engine.BuildSnapshot(_world, _sequence).Ships.FirstOrDefault(ship => ship.Id == shipId);
private PlayerFactionSnapshot? GetPlayerFactionSnapshotUnsafe() =>
_engine.BuildSnapshot(_world, _sequence).PlayerFaction;
private static bool HasMeaningfulDelta(WorldDelta delta) =>
delta.RequiresSnapshotRefresh
|| delta.Events.Count > 0
@@ -214,7 +373,9 @@ public sealed class WorldService(
|| delta.MarketOrders.Count > 0
|| delta.Policies.Count > 0
|| delta.Ships.Count > 0
|| delta.Factions.Count > 0;
|| delta.Factions.Count > 0
|| delta.PlayerFaction is not null
|| delta.Geopolitics is not null;
private void Unsubscribe(Guid subscriberId)
{
@@ -261,6 +422,8 @@ public sealed class WorldService(
Policies = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.Policies : [],
Ships = delta.Ships.Where((ship) => systemFilter is null || ship.SystemId == systemFilter).ToList(),
Factions = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.Factions : [],
PlayerFaction = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.PlayerFaction : null,
Geopolitics = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.Geopolitics : null,
Scope = scope,
};
}