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> _subscribers = []; private readonly Queue _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 Subscribe(long afterSequence, CancellationToken cancellationToken) { var channel = Channel.CreateUnbounded(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(); } } }