Refactor world bootstrap and allow empty startup worlds
This commit is contained in:
@@ -1,26 +1,57 @@
|
||||
using System.Threading.Channels;
|
||||
using Microsoft.Extensions.Options;
|
||||
using SpaceGame.Api.Universe.Bootstrap;
|
||||
using SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Simulation;
|
||||
|
||||
public sealed class WorldService(
|
||||
WorldBootstrapper worldBootstrapper,
|
||||
IOptions<BalanceOptions> balance,
|
||||
IOptions<OrbitalSimulationOptions> orbitalSimulationOptions)
|
||||
public sealed class WorldService
|
||||
{
|
||||
private const int DeltaHistoryLimit = 256;
|
||||
|
||||
private readonly Lock _sync = new();
|
||||
private readonly OrbitalSimulationSnapshot _orbitalSimulation = new(orbitalSimulationOptions.Value.SimulatedSecondsPerRealSecond);
|
||||
private readonly WorldBootstrapper _bootstrapper = worldBootstrapper;
|
||||
private readonly SimulationEngine _engine = new(orbitalSimulationOptions.Value);
|
||||
private readonly OrbitalSimulationSnapshot _orbitalSimulation;
|
||||
private readonly SimulationEngine _engine;
|
||||
private readonly ScenarioLoader _scenarioLoader;
|
||||
private readonly WorldBuilder _worldBuilder;
|
||||
private readonly PlayerFactionService _playerFaction = new();
|
||||
private readonly Dictionary<Guid, SubscriptionState> _subscribers = [];
|
||||
private readonly Queue<WorldDelta> _history = [];
|
||||
private SimulationWorld _world = worldBootstrapper.Bootstrap();
|
||||
private SimulationWorld _world = null!;
|
||||
private string? _currentScenarioPath;
|
||||
private WorldGenerationOptions? _currentWorldGenerationOptions;
|
||||
private long _sequence;
|
||||
private BalanceOptions? _balanceOverride;
|
||||
|
||||
public WorldService(
|
||||
ScenarioLoader scenarioLoader,
|
||||
WorldBuilder worldBuilder,
|
||||
IBalanceService balance,
|
||||
IOptions<OrbitalSimulationOptions> orbitalSimulationOptions)
|
||||
{
|
||||
_orbitalSimulation = new OrbitalSimulationSnapshot(orbitalSimulationOptions.Value.SimulatedSecondsPerRealSecond);
|
||||
_scenarioLoader = scenarioLoader;
|
||||
_worldBuilder = worldBuilder;
|
||||
_engine = new SimulationEngine(orbitalSimulationOptions.Value, balance);
|
||||
}
|
||||
|
||||
public void New(WorldGenerationOptions options)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_currentScenarioPath = null;
|
||||
_currentWorldGenerationOptions = options;
|
||||
ReplaceWorldUnsafe(_worldBuilder.BuildFromGeneration(options), "new", "Generated new world");
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadFromScenario(string scenarioPath)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_currentScenarioPath = scenarioPath;
|
||||
_currentWorldGenerationOptions = null;
|
||||
ReplaceWorldUnsafe(_worldBuilder.BuildFromScenario(_scenarioLoader.Load(scenarioPath)), "load-scenario", $"Loaded scenario {scenarioPath}");
|
||||
}
|
||||
}
|
||||
|
||||
public WorldSnapshot GetSnapshot()
|
||||
{
|
||||
@@ -46,35 +77,6 @@ public sealed class WorldService(
|
||||
}
|
||||
}
|
||||
|
||||
public BalanceOptions GetBalance()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return new BalanceOptions
|
||||
{
|
||||
SimulationSpeedMultiplier = balance.Value.SimulationSpeedMultiplier,
|
||||
YPlane = balance.Value.YPlane,
|
||||
ArrivalThreshold = balance.Value.ArrivalThreshold,
|
||||
MiningRate = balance.Value.MiningRate,
|
||||
MiningCycleSeconds = balance.Value.MiningCycleSeconds,
|
||||
TransferRate = balance.Value.TransferRate,
|
||||
DockingDuration = balance.Value.DockingDuration,
|
||||
UndockingDuration = balance.Value.UndockingDuration,
|
||||
UndockDistance = balance.Value.UndockDistance,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public BalanceOptions UpdateBalance(BalanceOptions balance)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_balanceOverride = SanitizeBalance(balance);
|
||||
ApplyBalance(_balanceOverride);
|
||||
return GetBalance();
|
||||
}
|
||||
}
|
||||
|
||||
public ShipSnapshot? EnqueueShipOrder(string shipId, ShipOrderCommandRequest request)
|
||||
{
|
||||
lock (_sync)
|
||||
@@ -121,6 +123,11 @@ public sealed class WorldService(
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
if (_world.PlayerFaction is null && _world.Factions.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_playerFaction.EnsureDomain(_world);
|
||||
return GetPlayerFactionSnapshotUnsafe();
|
||||
}
|
||||
@@ -285,74 +292,61 @@ public sealed class WorldService(
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_world = _bootstrapper.Bootstrap();
|
||||
if (_balanceOverride is not null)
|
||||
if (_currentScenarioPath is not null)
|
||||
{
|
||||
ApplyBalance(_balanceOverride);
|
||||
ReplaceWorldUnsafe(
|
||||
_worldBuilder.BuildFromScenario(_scenarioLoader.Load(_currentScenarioPath)),
|
||||
"reset",
|
||||
"World reset requested");
|
||||
}
|
||||
_sequence += 1;
|
||||
_history.Clear();
|
||||
|
||||
var resetDelta = new WorldDelta(
|
||||
_sequence,
|
||||
_world.TickIntervalMs,
|
||||
_world.OrbitalTimeSeconds,
|
||||
_orbitalSimulation,
|
||||
DateTimeOffset.UtcNow,
|
||||
true,
|
||||
[new SimulationEventRecord("world", "world", "reset", "World reset requested", DateTimeOffset.UtcNow, "world", "universe", "world")],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
null,
|
||||
null);
|
||||
|
||||
_history.Enqueue(resetDelta);
|
||||
foreach (var subscriber in _subscribers.Values.ToList())
|
||||
else if (_currentWorldGenerationOptions is not null)
|
||||
{
|
||||
subscriber.Channel.Writer.TryWrite(FilterDeltaForScope(resetDelta, subscriber.Scope));
|
||||
ReplaceWorldUnsafe(
|
||||
_worldBuilder.BuildFromGeneration(_currentWorldGenerationOptions),
|
||||
"reset",
|
||||
"World reset requested");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("No world source is configured.");
|
||||
}
|
||||
|
||||
return _engine.BuildSnapshot(_world, _sequence);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyBalance(BalanceOptions value)
|
||||
private void ReplaceWorldUnsafe(SimulationWorld world, string eventKind, string eventMessage)
|
||||
{
|
||||
balance.Value.SimulationSpeedMultiplier = value.SimulationSpeedMultiplier;
|
||||
balance.Value.YPlane = value.YPlane;
|
||||
balance.Value.ArrivalThreshold = value.ArrivalThreshold;
|
||||
balance.Value.MiningRate = value.MiningRate;
|
||||
balance.Value.MiningCycleSeconds = value.MiningCycleSeconds;
|
||||
balance.Value.TransferRate = value.TransferRate;
|
||||
balance.Value.DockingDuration = value.DockingDuration;
|
||||
balance.Value.UndockingDuration = value.UndockingDuration;
|
||||
balance.Value.UndockDistance = value.UndockDistance;
|
||||
}
|
||||
_world = world;
|
||||
_sequence += 1;
|
||||
_history.Clear();
|
||||
|
||||
private static BalanceOptions SanitizeBalance(BalanceOptions candidate)
|
||||
{
|
||||
static float finiteOr(float value, float fallback) =>
|
||||
float.IsFinite(value) ? value : fallback;
|
||||
var eventTime = DateTimeOffset.UtcNow;
|
||||
var worldDelta = new WorldDelta(
|
||||
_sequence,
|
||||
_world.TickIntervalMs,
|
||||
_world.OrbitalTimeSeconds,
|
||||
_orbitalSimulation,
|
||||
eventTime,
|
||||
true,
|
||||
[new SimulationEventRecord("world", "world", eventKind, eventMessage, eventTime, "world", "universe", "world")],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
null,
|
||||
null);
|
||||
|
||||
return new BalanceOptions
|
||||
_history.Enqueue(worldDelta);
|
||||
foreach (var subscriber in _subscribers.Values.ToList())
|
||||
{
|
||||
SimulationSpeedMultiplier = MathF.Max(0.01f, finiteOr(candidate.SimulationSpeedMultiplier, 1f)),
|
||||
YPlane = MathF.Max(0f, finiteOr(candidate.YPlane, 0f)),
|
||||
ArrivalThreshold = MathF.Max(0.1f, finiteOr(candidate.ArrivalThreshold, 16f)),
|
||||
MiningRate = MathF.Max(0f, finiteOr(candidate.MiningRate, 10f)),
|
||||
MiningCycleSeconds = MathF.Max(0.1f, finiteOr(candidate.MiningCycleSeconds, 10f)),
|
||||
TransferRate = MathF.Max(0f, finiteOr(candidate.TransferRate, 56f)),
|
||||
DockingDuration = MathF.Max(0.1f, finiteOr(candidate.DockingDuration, 1.2f)),
|
||||
UndockingDuration = MathF.Max(0.1f, finiteOr(candidate.UndockingDuration, 1.2f)),
|
||||
UndockDistance = MathF.Max(0f, finiteOr(candidate.UndockDistance, 42f)),
|
||||
};
|
||||
subscriber.Channel.Writer.TryWrite(FilterDeltaForScope(worldDelta, subscriber.Scope));
|
||||
}
|
||||
}
|
||||
|
||||
private ShipSnapshot? GetShipSnapshotUnsafe(string shipId) =>
|
||||
|
||||
Reference in New Issue
Block a user