using System.Threading.Channels; using Microsoft.Extensions.Options; namespace SpaceGame.Api.Universe.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 PlayerFactionService _playerFaction = new(); private readonly Dictionary _subscribers = []; private readonly Queue _history = []; private SimulationWorld _world = new ScenarioLoader(environment.ContentRootPath, worldGenerationOptions.Value).Load(); private long _sequence; private BalanceDefinition? _balanceOverride; public WorldSnapshot GetSnapshot() { lock (_sync) { return _engine.BuildSnapshot(_world, _sequence); } } public (long Sequence, DateTimeOffset GeneratedAtUtc) GetStatus() { lock (_sync) { return (_sequence, _world.GeneratedAtUtc); } } public (int ConnectedClients, int DeltaHistoryCount) GetConnectionStats() { lock (_sync) { return (_subscribers.Count, _history.Count); } } public BalanceDefinition GetBalance() { lock (_sync) { var b = _world.Balance; return new BalanceDefinition { SimulationSpeedMultiplier = b.SimulationSpeedMultiplier, YPlane = b.YPlane, ArrivalThreshold = b.ArrivalThreshold, MiningRate = b.MiningRate, MiningCycleSeconds = b.MiningCycleSeconds, TransferRate = b.TransferRate, DockingDuration = b.DockingDuration, UndockingDuration = b.UndockingDuration, UndockDistance = b.UndockDistance, }; } } public BalanceDefinition UpdateBalance(BalanceDefinition balance) { lock (_sync) { _balanceOverride = SanitizeBalance(balance); ApplyBalance(_world, _balanceOverride); return GetBalance(); } } public ShipSnapshot? EnqueueShipOrder(string shipId, ShipOrderCommandRequest request) { lock (_sync) { var ship = _playerFaction.EnqueueDirectShipOrder(_world, shipId, request); if (ship is null) { return null; } return GetShipSnapshotUnsafe(ship.Id); } } public ShipSnapshot? RemoveShipOrder(string shipId, string orderId) { lock (_sync) { var ship = _playerFaction.RemoveDirectShipOrder(_world, shipId, orderId); if (ship is null) { return null; } return GetShipSnapshotUnsafe(ship.Id); } } public ShipSnapshot? UpdateShipDefaultBehavior(string shipId, ShipDefaultBehaviorCommandRequest request) { lock (_sync) { var ship = _playerFaction.ConfigureDirectShipBehavior(_world, shipId, request); if (ship is null) { return null; } return GetShipSnapshotUnsafe(ship.Id); } } public PlayerFactionSnapshot? GetPlayerFaction() { lock (_sync) { _playerFaction.EnsureDomain(_world); return GetPlayerFactionSnapshotUnsafe(); } } public PlayerFactionSnapshot? CreatePlayerOrganization(PlayerOrganizationCommandRequest request) { lock (_sync) { _playerFaction.CreateOrganization(_world, request); return GetPlayerFactionSnapshotUnsafe(); } } public PlayerFactionSnapshot? DeletePlayerOrganization(string organizationId) { lock (_sync) { _playerFaction.DeleteOrganization(_world, organizationId); return GetPlayerFactionSnapshotUnsafe(); } } public PlayerFactionSnapshot? UpdatePlayerOrganizationMembership(string organizationId, PlayerOrganizationMembershipCommandRequest request) { lock (_sync) { _playerFaction.UpdateOrganizationMembership(_world, organizationId, request); return GetPlayerFactionSnapshotUnsafe(); } } public PlayerFactionSnapshot? UpsertPlayerDirective(string? directiveId, PlayerDirectiveCommandRequest request) { lock (_sync) { _playerFaction.UpsertDirective(_world, directiveId, request); return GetPlayerFactionSnapshotUnsafe(); } } public PlayerFactionSnapshot? DeletePlayerDirective(string directiveId) { lock (_sync) { _playerFaction.DeleteDirective(_world, directiveId); return GetPlayerFactionSnapshotUnsafe(); } } public PlayerFactionSnapshot? UpsertPlayerPolicy(string? policyId, PlayerPolicyCommandRequest request) { lock (_sync) { _playerFaction.UpsertPolicy(_world, policyId, request); return GetPlayerFactionSnapshotUnsafe(); } } public PlayerFactionSnapshot? UpsertPlayerAutomationPolicy(string? automationPolicyId, PlayerAutomationPolicyCommandRequest request) { lock (_sync) { _playerFaction.UpsertAutomationPolicy(_world, automationPolicyId, request); return GetPlayerFactionSnapshotUnsafe(); } } public PlayerFactionSnapshot? UpsertPlayerReinforcementPolicy(string? reinforcementPolicyId, PlayerReinforcementPolicyCommandRequest request) { lock (_sync) { _playerFaction.UpsertReinforcementPolicy(_world, reinforcementPolicyId, request); return GetPlayerFactionSnapshotUnsafe(); } } public PlayerFactionSnapshot? UpsertPlayerProductionProgram(string? productionProgramId, PlayerProductionProgramCommandRequest request) { lock (_sync) { _playerFaction.UpsertProductionProgram(_world, productionProgramId, request); return GetPlayerFactionSnapshotUnsafe(); } } public PlayerFactionSnapshot? UpsertPlayerAssignment(string assetId, PlayerAssetAssignmentCommandRequest request) { lock (_sync) { _playerFaction.UpsertAssignment(_world, assetId, request); return GetPlayerFactionSnapshotUnsafe(); } } public PlayerFactionSnapshot? UpdatePlayerStrategicIntent(PlayerStrategicIntentCommandRequest request) { lock (_sync) { _playerFaction.UpdateStrategicIntent(_world, request); return GetPlayerFactionSnapshotUnsafe(); } } 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(); if (_balanceOverride is not null) { ApplyBalance(_world, _balanceOverride); } _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()) { subscriber.Channel.Writer.TryWrite(FilterDeltaForScope(resetDelta, subscriber.Scope)); } return _engine.BuildSnapshot(_world, _sequence); } } private static void ApplyBalance(SimulationWorld world, BalanceDefinition balance) => world.Balance = new BalanceDefinition { SimulationSpeedMultiplier = balance.SimulationSpeedMultiplier, YPlane = balance.YPlane, ArrivalThreshold = balance.ArrivalThreshold, MiningRate = balance.MiningRate, MiningCycleSeconds = balance.MiningCycleSeconds, TransferRate = balance.TransferRate, DockingDuration = balance.DockingDuration, UndockingDuration = balance.UndockingDuration, UndockDistance = balance.UndockDistance, }; private static BalanceDefinition SanitizeBalance(BalanceDefinition candidate) { static float finiteOr(float value, float fallback) => float.IsFinite(value) ? value : fallback; return new BalanceDefinition { 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)), }; } private ShipSnapshot? GetShipSnapshotUnsafe(string shipId) => _engine.BuildSnapshot(_world, _sequence).Ships.FirstOrDefault(ship => ship.Id == shipId); private PlayerFactionSnapshot? GetPlayerFactionSnapshotUnsafe() => _engine.BuildSnapshot(_world, _sequence).PlayerFaction; 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 || delta.PlayerFaction is not null || delta.Geopolitics is not null; 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 : [], PlayerFaction = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.PlayerFaction : null, Geopolitics = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.Geopolitics : null, 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); }