using System.Threading.Channels; using Microsoft.Extensions.Options; using SpaceGame.Api.Contracts; using SpaceGame.Api.Simulation.Engine; using SpaceGame.Api.Simulation.Model; namespace SpaceGame.Api.Simulation; public sealed class WorldService( IWebHostEnvironment environment, IOptions worldGenerationOptions, IOptions orbitalSimulationOptions) { private const int DeltaHistoryLimit = 256; private readonly Lock _sync = new(); private readonly OrbitalSimulationSnapshot _orbitalSimulation = new(orbitalSimulationOptions.Value.SimulatedSecondsPerRealSecond); private readonly ScenarioLoader _loader = new(environment.ContentRootPath, worldGenerationOptions.Value); private readonly SimulationEngine _engine = new(orbitalSimulationOptions.Value); private readonly Dictionary _subscribers = []; private readonly Queue _history = []; private SimulationWorld _world = new ScenarioLoader(environment.ContentRootPath, worldGenerationOptions.Value).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(ObserverScope scope, long afterSequence, CancellationToken cancellationToken) { var channel = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = true, SingleWriter = false, }); Guid subscriberId; lock (_sync) { subscriberId = Guid.NewGuid(); _subscribers.Add(subscriberId, new SubscriptionState(scope, channel)); foreach (var delta in _history.Where((candidate) => candidate.Sequence > afterSequence)) { var filtered = FilterDeltaForScope(delta, scope); if (HasMeaningfulDelta(filtered)) { channel.Writer.TryWrite(filtered); } } } 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()) { var filtered = FilterDeltaForScope(delta, subscriber.Scope); if (HasMeaningfulDelta(filtered)) { subscriber.Channel.Writer.TryWrite(filtered); } } } } public WorldSnapshot Reset() { lock (_sync) { _world = _loader.Load(); _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")], [], [], [], [], [], [], [], [], []); _history.Enqueue(resetDelta); foreach (var subscriber in _subscribers.Values.ToList()) { subscriber.Channel.Writer.TryWrite(FilterDeltaForScope(resetDelta, subscriber.Scope)); } return _engine.BuildSnapshot(_world, _sequence); } } private static bool HasMeaningfulDelta(WorldDelta delta) => delta.RequiresSnapshotRefresh || delta.Events.Count > 0 || delta.Celestials.Count > 0 || delta.Nodes.Count > 0 || delta.Stations.Count > 0 || delta.Claims.Count > 0 || delta.ConstructionSites.Count > 0 || delta.MarketOrders.Count > 0 || delta.Policies.Count > 0 || delta.Ships.Count > 0 || delta.Factions.Count > 0; private void Unsubscribe(Guid subscriberId) { lock (_sync) { if (!_subscribers.Remove(subscriberId, out var subscription)) { return; } subscription.Channel.Writer.TryComplete(); } } private WorldDelta FilterDeltaForScope(WorldDelta delta, ObserverScope scope) { if (string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase)) { return delta with { Events = delta.Events.Select((evt) => EnrichEventScope(evt)).ToList(), Scope = scope, }; } var systemFilter = scope.SystemId; if (string.Equals(scope.ScopeKind, "local-celestial", StringComparison.OrdinalIgnoreCase) && systemFilter is null && scope.CelestialId is not null) { systemFilter = ResolveCelestialSystemId(scope.CelestialId); } return delta with { Events = delta.Events .Select((evt) => EnrichEventScope(evt)) .Where((evt) => IsEventVisibleToScope(evt, scope, systemFilter)) .ToList(), Celestials = delta.Celestials.Where((celestial) => systemFilter is null || celestial.SystemId == systemFilter).ToList(), Nodes = delta.Nodes.Where((node) => systemFilter is null || node.SystemId == systemFilter).ToList(), Stations = delta.Stations.Where((station) => systemFilter is null || station.SystemId == systemFilter).ToList(), Claims = delta.Claims.Where((claim) => systemFilter is null || claim.SystemId == systemFilter).ToList(), ConstructionSites = delta.ConstructionSites.Where((site) => systemFilter is null || site.SystemId == systemFilter).ToList(), MarketOrders = delta.MarketOrders.Where((order) => IsOrderVisibleToScope(order, systemFilter)).ToList(), Policies = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.Policies : [], Ships = delta.Ships.Where((ship) => systemFilter is null || ship.SystemId == systemFilter).ToList(), Factions = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.Factions : [], Scope = scope, }; } private SimulationEventRecord EnrichEventScope(SimulationEventRecord evt) { if (!string.Equals(evt.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) || evt.ScopeEntityId is not null) { return evt; } return evt.EntityKind switch { "ship" => WithEntityScope(evt, "system", _world.Ships.FirstOrDefault((ship) => ship.Id == evt.EntityId)?.SystemId), "station" => WithEntityScope(evt, "system", _world.Stations.FirstOrDefault((station) => station.Id == evt.EntityId)?.SystemId), "node" => WithEntityScope(evt, "system", _world.Nodes.FirstOrDefault((node) => node.Id == evt.EntityId)?.SystemId), "celestial" => WithEntityScope(evt, "system", _world.Celestials.FirstOrDefault((c) => c.Id == evt.EntityId)?.SystemId), "claim" => WithEntityScope(evt, "system", _world.Claims.FirstOrDefault((claim) => claim.Id == evt.EntityId)?.SystemId), "construction-site" => WithEntityScope(evt, "system", _world.ConstructionSites.FirstOrDefault((site) => site.Id == evt.EntityId)?.SystemId), "market-order" => WithEntityScope(evt, "system", ResolveMarketOrderSystemId(evt.EntityId)), _ => evt, }; } private static SimulationEventRecord WithEntityScope(SimulationEventRecord evt, string scopeKind, string? scopeEntityId) => evt with { Family = evt.Kind.Contains("power", StringComparison.Ordinal) ? "power" : evt.Kind.Contains("construction", StringComparison.Ordinal) ? "construction" : evt.Kind.Contains("population", StringComparison.Ordinal) ? "population" : evt.Kind.Contains("claim", StringComparison.Ordinal) ? "claim" : "simulation", ScopeKind = scopeKind, ScopeEntityId = scopeEntityId, }; private string? ResolveCelestialSystemId(string celestialId) => _world.Celestials.FirstOrDefault((c) => c.Id == celestialId)?.SystemId; private string? ResolveMarketOrderSystemId(string orderId) { var order = _world.MarketOrders.FirstOrDefault((candidate) => candidate.Id == orderId); if (order?.StationId is not null) { return _world.Stations.FirstOrDefault((station) => station.Id == order.StationId)?.SystemId; } if (order?.ConstructionSiteId is not null) { return _world.ConstructionSites.FirstOrDefault((site) => site.Id == order.ConstructionSiteId)?.SystemId; } return null; } private bool IsOrderVisibleToScope(MarketOrderDelta order, string? systemFilter) { if (systemFilter is null) { return true; } if (order.StationId is not null) { return _world.Stations.Any((station) => station.Id == order.StationId && station.SystemId == systemFilter); } if (order.ConstructionSiteId is not null) { return _world.ConstructionSites.Any((site) => site.Id == order.ConstructionSiteId && site.SystemId == systemFilter); } return false; } private static bool IsEventVisibleToScope(SimulationEventRecord evt, ObserverScope scope, string? systemFilter) { return scope.ScopeKind switch { "universe" => true, "system" => evt.ScopeKind == "universe" || evt.ScopeEntityId == systemFilter, "local-celestial" => evt.ScopeKind == "universe" || evt.ScopeEntityId == systemFilter, _ => true, }; } private sealed record SubscriptionState(ObserverScope Scope, Channel Channel); }