Add world delta streaming and viewer smoothing
This commit is contained in:
@@ -1,27 +1,81 @@
|
||||
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);
|
||||
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)
|
||||
{
|
||||
_engine.Tick(_world, deltaSeconds);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +84,48 @@ public sealed class WorldService(IWebHostEnvironment environment)
|
||||
lock (_sync)
|
||||
{
|
||||
_world = _loader.Load();
|
||||
return _engine.BuildSnapshot(_world);
|
||||
_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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user