Files
space-game/apps/backend/Simulation/WorldService.cs

132 lines
3.1 KiB
C#

using System.Threading.Channels;
using SpaceGame.Simulation.Api.Contracts;
namespace SpaceGame.Simulation.Api.Simulation;
public sealed class WorldService(IWebHostEnvironment environment)
{
private const int DeltaHistoryLimit = 256;
private readonly object _sync = new();
private readonly ScenarioLoader _loader = new(environment.ContentRootPath);
private readonly SimulationEngine _engine = new();
private readonly Dictionary<Guid, Channel<WorldDelta>> _subscribers = [];
private readonly Queue<WorldDelta> _history = [];
private SimulationWorld _world = new ScenarioLoader(environment.ContentRootPath).Load();
private long _sequence;
public WorldSnapshot GetSnapshot()
{
lock (_sync)
{
return _engine.BuildSnapshot(_world, _sequence);
}
}
public (long Sequence, DateTimeOffset GeneratedAtUtc) GetStatus()
{
lock (_sync)
{
return (_sequence, _world.GeneratedAtUtc);
}
}
public ChannelReader<WorldDelta> Subscribe(long afterSequence, CancellationToken cancellationToken)
{
var channel = Channel.CreateUnbounded<WorldDelta>(new UnboundedChannelOptions
{
SingleReader = true,
SingleWriter = false,
});
Guid subscriberId;
lock (_sync)
{
subscriberId = Guid.NewGuid();
_subscribers.Add(subscriberId, channel);
foreach (var delta in _history.Where((candidate) => candidate.Sequence > afterSequence))
{
channel.Writer.TryWrite(delta);
}
}
cancellationToken.Register(() => Unsubscribe(subscriberId));
return channel.Reader;
}
public void Tick(float deltaSeconds)
{
WorldDelta? delta = null;
lock (_sync)
{
delta = _engine.Tick(_world, deltaSeconds, ++_sequence);
if (!HasMeaningfulDelta(delta))
{
return;
}
_history.Enqueue(delta);
while (_history.Count > DeltaHistoryLimit)
{
_history.Dequeue();
}
foreach (var subscriber in _subscribers.Values.ToList())
{
subscriber.Writer.TryWrite(delta);
}
}
}
public WorldSnapshot Reset()
{
lock (_sync)
{
_world = _loader.Load();
_sequence += 1;
_history.Clear();
var resetDelta = new WorldDelta(
_sequence,
_world.TickIntervalMs,
DateTimeOffset.UtcNow,
true,
[new SimulationEventRecord("world", "world", "reset", "World reset requested", DateTimeOffset.UtcNow)],
[],
[],
[],
[]);
_history.Enqueue(resetDelta);
foreach (var subscriber in _subscribers.Values.ToList())
{
subscriber.Writer.TryWrite(resetDelta);
}
return _engine.BuildSnapshot(_world, _sequence);
}
}
private static bool HasMeaningfulDelta(WorldDelta delta) =>
delta.RequiresSnapshotRefresh
|| delta.Events.Count > 0
|| delta.Nodes.Count > 0
|| delta.Stations.Count > 0
|| delta.Ships.Count > 0
|| delta.Factions.Count > 0;
private void Unsubscribe(Guid subscriberId)
{
lock (_sync)
{
if (!_subscribers.Remove(subscriberId, out var channel))
{
return;
}
channel.Writer.TryComplete();
}
}
}