|
|
|
|
@@ -10,6 +10,7 @@ namespace SpaceGame.Api.Universe.Simulation;
|
|
|
|
|
public sealed class WorldService
|
|
|
|
|
{
|
|
|
|
|
private const int DeltaHistoryLimit = 256;
|
|
|
|
|
private const string StarterPlayerShipId = "ship_arg_s_scout_01_a";
|
|
|
|
|
|
|
|
|
|
private readonly Lock _sync = new();
|
|
|
|
|
private readonly OrbitalSimulationSnapshot _orbitalSimulation;
|
|
|
|
|
@@ -148,11 +149,6 @@ public sealed class WorldService
|
|
|
|
|
{
|
|
|
|
|
lock (_sync)
|
|
|
|
|
{
|
|
|
|
|
if (_world.Factions.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var playerKey = GetCurrentPlayerKey();
|
|
|
|
|
var player = _playerFaction.TryGetDomain(_playerStateStore, playerKey)
|
|
|
|
|
?? _playerFaction.EnsureDomain(_world, _playerStateStore, playerKey);
|
|
|
|
|
@@ -160,6 +156,26 @@ public sealed class WorldService
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public PlayerFactionSnapshot? CompletePlayerOnboarding(CompletePlayerOnboardingRequest request)
|
|
|
|
|
{
|
|
|
|
|
lock (_sync)
|
|
|
|
|
{
|
|
|
|
|
if (!_staticData.RaceDefinitions.TryGetValue(request.RaceId.Trim(), out var race))
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException($"Race '{request.RaceId}' is not defined in static data.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var playerKey = GetCurrentPlayerKey();
|
|
|
|
|
var player = _playerFaction.CompleteOnboarding(_world, _playerStateStore, playerKey, request);
|
|
|
|
|
var playerFaction = CreatePlayerOwnedFactionUnsafe(player, race);
|
|
|
|
|
var starterSystemId = ResolveStarterSystemIdUnsafe();
|
|
|
|
|
SpawnPlayerStarterShipUnsafe(playerFaction, starterSystemId);
|
|
|
|
|
_playerFaction.EnsureInitializedDomain(_world, _playerStateStore, playerKey);
|
|
|
|
|
PublishSnapshotRefreshUnsafe("player-onboarding", $"Initialized player {player.PersonaName}", "faction", playerFaction.Id);
|
|
|
|
|
return GetPlayerFactionSnapshotUnsafe();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public PlayerFactionSnapshot? CreatePlayerOrganization(PlayerOrganizationCommandRequest request)
|
|
|
|
|
{
|
|
|
|
|
lock (_sync)
|
|
|
|
|
@@ -530,7 +546,102 @@ public sealed class WorldService
|
|
|
|
|
private PlayerFactionSnapshot? GetPlayerFactionSnapshotUnsafe() =>
|
|
|
|
|
_playerFactionProjection.ToSnapshot(_playerFaction.TryGetDomain(_playerStateStore, GetCurrentPlayerKey()));
|
|
|
|
|
|
|
|
|
|
private string GetCurrentPlayerKey() => _playerIdentityResolver.GetRequiredPlayerId().ToString("N");
|
|
|
|
|
private FactionRuntime CreatePlayerOwnedFactionUnsafe(PlayerFactionRuntime player, RaceDefinition race)
|
|
|
|
|
{
|
|
|
|
|
var playerFaction = new FactionRuntime
|
|
|
|
|
{
|
|
|
|
|
Id = player.SovereignFactionId,
|
|
|
|
|
Label = player.PersonaName ?? player.Label,
|
|
|
|
|
Color = ResolvePlayerFactionColor(race.Id),
|
|
|
|
|
Credits = 25000f,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_world.Factions.Add(playerFaction);
|
|
|
|
|
|
|
|
|
|
var policy = _worldSeedingService.CreatePolicies([playerFaction]).Single();
|
|
|
|
|
var templateFaction = _staticData.FactionDefinitions.Values
|
|
|
|
|
.Where(candidate => string.Equals(candidate.RaceId, race.Id, StringComparison.Ordinal))
|
|
|
|
|
.OrderBy(candidate => candidate.Id, StringComparer.Ordinal)
|
|
|
|
|
.Select(candidate => _world.Factions.FirstOrDefault(worldFaction => string.Equals(worldFaction.Id, candidate.Id, StringComparison.Ordinal)))
|
|
|
|
|
.FirstOrDefault(candidate => candidate is not null);
|
|
|
|
|
if (templateFaction?.DefaultPolicySetId is { } racePolicyId
|
|
|
|
|
&& _world.Policies.FirstOrDefault(candidate => candidate.Id == racePolicyId) is { } racePolicy)
|
|
|
|
|
{
|
|
|
|
|
policy.TradeAccessPolicy = racePolicy.TradeAccessPolicy;
|
|
|
|
|
policy.DockingAccessPolicy = racePolicy.DockingAccessPolicy;
|
|
|
|
|
policy.ConstructionAccessPolicy = racePolicy.ConstructionAccessPolicy;
|
|
|
|
|
policy.OperationalRangePolicy = racePolicy.OperationalRangePolicy;
|
|
|
|
|
policy.CombatEngagementPolicy = racePolicy.CombatEngagementPolicy;
|
|
|
|
|
policy.FleeHullRatio = racePolicy.FleeHullRatio;
|
|
|
|
|
policy.AvoidHostileSystems = racePolicy.AvoidHostileSystems;
|
|
|
|
|
foreach (var systemId in racePolicy.BlacklistedSystemIds)
|
|
|
|
|
{
|
|
|
|
|
policy.BlacklistedSystemIds.Add(systemId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_world.Policies.Add(policy);
|
|
|
|
|
|
|
|
|
|
var factionCommander = CreateFactionCommander(playerFaction);
|
|
|
|
|
_world.Commanders.Add(factionCommander);
|
|
|
|
|
playerFaction.CommanderIds.Add(factionCommander.Id);
|
|
|
|
|
return playerFaction;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string ResolveStarterSystemIdUnsafe()
|
|
|
|
|
{
|
|
|
|
|
return _world.Systems
|
|
|
|
|
.Select(system => system.Definition.Id)
|
|
|
|
|
.OrderBy(systemId => systemId, StringComparer.Ordinal)
|
|
|
|
|
.FirstOrDefault()
|
|
|
|
|
?? throw new InvalidOperationException("No systems are available for player onboarding.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SpawnPlayerStarterShipUnsafe(FactionRuntime playerFaction, string systemId)
|
|
|
|
|
{
|
|
|
|
|
var request = new SpawnShipCommandRequest(
|
|
|
|
|
playerFaction.Id,
|
|
|
|
|
systemId,
|
|
|
|
|
StarterPlayerShipId,
|
|
|
|
|
Idle);
|
|
|
|
|
var system = _world.Systems.First(candidate => string.Equals(candidate.Definition.Id, request.SystemId, StringComparison.Ordinal));
|
|
|
|
|
var definition = ResolveShipDefinition(request, playerFaction.Id);
|
|
|
|
|
var shipId = $"ship-{playerFaction.Id}-{definition.Id}-{Guid.NewGuid():N}".ToLowerInvariant();
|
|
|
|
|
var spawnPosition = ResolveSpawnPosition(system.Definition.Id);
|
|
|
|
|
var defaultBehavior = CreateSpawnBehavior(request, definition, system.Definition.Id, null);
|
|
|
|
|
|
|
|
|
|
var ship = new ShipRuntime
|
|
|
|
|
{
|
|
|
|
|
Id = shipId,
|
|
|
|
|
SystemId = system.Definition.Id,
|
|
|
|
|
Definition = definition,
|
|
|
|
|
FactionId = playerFaction.Id,
|
|
|
|
|
Position = spawnPosition,
|
|
|
|
|
TargetPosition = spawnPosition,
|
|
|
|
|
SpatialState = SpatialBuilder.CreateInitialShipSpatialState(system.Definition.Id, spawnPosition, _world.Celestials),
|
|
|
|
|
DefaultBehavior = defaultBehavior,
|
|
|
|
|
Skills = ShipBootstrapPolicy.CreateSkills(definition),
|
|
|
|
|
Health = definition.Hull,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_world.Ships.Add(ship);
|
|
|
|
|
EnsureShipCommander(playerFaction, ship);
|
|
|
|
|
new GeopoliticalSimulationService().Update(_world, 0f, []);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string ResolvePlayerFactionColor(string raceId) =>
|
|
|
|
|
raceId switch
|
|
|
|
|
{
|
|
|
|
|
"argon" => "#3b82f6",
|
|
|
|
|
"boron" => "#14b8a6",
|
|
|
|
|
"paranid" => "#eab308",
|
|
|
|
|
"split" => "#b91c1c",
|
|
|
|
|
"teladi" => "#22c55e",
|
|
|
|
|
"terran" => "#38bdf8",
|
|
|
|
|
"xenon" => "#9ca3af",
|
|
|
|
|
_ => "#94a3b8",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private string GetCurrentPlayerKey() => _playerIdentityResolver.GetRequiredEffectivePlayerId().ToString("N");
|
|
|
|
|
|
|
|
|
|
private bool CanCurrentActorAccessGm() => _playerIdentityResolver.CanAccessGm();
|
|
|
|
|
|
|
|
|
|
@@ -807,7 +918,8 @@ public sealed class WorldService
|
|
|
|
|
powerModuleId,
|
|
|
|
|
factionId,
|
|
|
|
|
_staticData.ModuleDefinitions,
|
|
|
|
|
_staticData.ItemDefinitions)
|
|
|
|
|
_staticData.ItemDefinitions,
|
|
|
|
|
_staticData.Recipes)
|
|
|
|
|
.FirstOrDefault(moduleId =>
|
|
|
|
|
{
|
|
|
|
|
return _staticData.ModuleDefinitions.TryGetValue(moduleId, out var definition)
|
|
|
|
|
@@ -820,6 +932,24 @@ public sealed class WorldService
|
|
|
|
|
EnsureStationModule(modules, defaultContainerStorageModuleId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var defaultSolidStorageModuleId = StarterStationLayoutResolver.ResolveRequiredStorageModuleIds(
|
|
|
|
|
powerModuleId,
|
|
|
|
|
factionId,
|
|
|
|
|
_staticData.ModuleDefinitions,
|
|
|
|
|
_staticData.ItemDefinitions,
|
|
|
|
|
_staticData.Recipes)
|
|
|
|
|
.FirstOrDefault(moduleId =>
|
|
|
|
|
{
|
|
|
|
|
return _staticData.ModuleDefinitions.TryGetValue(moduleId, out var definition)
|
|
|
|
|
&& definition is StorageModuleDefinition storageDefinition
|
|
|
|
|
&& storageDefinition.StorageKind == StorageKind.Solid;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (defaultSolidStorageModuleId is not null)
|
|
|
|
|
{
|
|
|
|
|
EnsureStationModule(modules, defaultSolidStorageModuleId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var objectiveModuleId = StarterStationLayoutResolver.ResolveObjectiveModuleId(objective, factionId, _staticData.ModuleDefinitions);
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(objectiveModuleId))
|
|
|
|
|
{
|
|
|
|
|
@@ -828,7 +958,8 @@ public sealed class WorldService
|
|
|
|
|
objectiveModuleId,
|
|
|
|
|
factionId,
|
|
|
|
|
_staticData.ModuleDefinitions,
|
|
|
|
|
_staticData.ItemDefinitions))
|
|
|
|
|
_staticData.ItemDefinitions,
|
|
|
|
|
_staticData.Recipes))
|
|
|
|
|
{
|
|
|
|
|
EnsureStationModule(modules, storageModuleId);
|
|
|
|
|
}
|
|
|
|
|
|