diff --git a/Content.Server/Atmos/Components/FlammableComponent.cs b/Content.Server/Atmos/Components/FlammableComponent.cs index 6ca5e0a913..88ca5255b7 100644 --- a/Content.Server/Atmos/Components/FlammableComponent.cs +++ b/Content.Server/Atmos/Components/FlammableComponent.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Content.Server.Alert; +using Content.Server.Atmos.EntitySystems; using Content.Server.Stunnable.Components; using Content.Server.Temperature.Components; using Content.Shared.ActionBlocker; @@ -122,7 +123,7 @@ namespace Content.Server.Atmos.Components return; } - tile.HotspotExpose(700, 50, true); + EntitySystem.Get().HotspotExpose(tile.GridIndex, tile.GridIndices, 700f, 50f, true); var physics = Owner.GetComponent(); diff --git a/Content.Server/Atmos/Components/GridAtmosphereComponent.cs b/Content.Server/Atmos/Components/GridAtmosphereComponent.cs index 2e5a291e96..816fc09f4a 100644 --- a/Content.Server/Atmos/Components/GridAtmosphereComponent.cs +++ b/Content.Server/Atmos/Components/GridAtmosphereComponent.cs @@ -36,7 +36,6 @@ namespace Content.Server.Atmos.Components [Dependency] private IServerEntityManager _serverEntityManager = default!; [Dependency] private IGameTiming _gameTiming = default!; - public GridTileLookupSystem GridTileLookupSystem { get; private set; } = default!; internal GasTileOverlaySystem GasTileOverlaySystem { get; private set; } = default!; public AtmosphereSystem AtmosphereSystem { get; private set; } = default!; @@ -182,7 +181,6 @@ namespace Content.Server.Atmos.Components } } - GridTileLookupSystem = EntitySystem.Get(); GasTileOverlaySystem = EntitySystem.Get(); AtmosphereSystem = EntitySystem.Get(); diff --git a/Content.Server/Atmos/EntitySystems/AtmosDebugOverlaySystem.cs b/Content.Server/Atmos/EntitySystems/AtmosDebugOverlaySystem.cs index b2313f675f..f9d0288fc8 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosDebugOverlaySystem.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosDebugOverlaySystem.cs @@ -109,7 +109,7 @@ namespace Content.Server.Atmos.EntitySystems { gases[i] = tile.Air.GetMoles(i); } - return new AtmosDebugOverlayData(tile.Air.Temperature, gases, tile.PressureDirectionForDebugOverlay, tile.ExcitedGroup != null, tile.BlockedAirflow); + return new AtmosDebugOverlayData(tile.Air.Temperature, gases, tile.PressureDirection, tile.ExcitedGroup != null, tile.BlockedAirflow); } } diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.CVars.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.CVars.cs index f2baf06066..50576c5221 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.CVars.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.CVars.cs @@ -5,7 +5,9 @@ namespace Content.Server.Atmos.EntitySystems public partial class AtmosphereSystem { public bool SpaceWind { get; private set; } + public string? SpaceWindSound { get; private set; } public bool MonstermosEqualization { get; private set; } + public bool MonstermosDepressurization { get; private set; } public bool Superconduction { get; private set; } public bool ExcitedGroupsSpaceIsAllConsuming { get; private set; } public float AtmosMaxProcessTime { get; private set; } @@ -15,7 +17,9 @@ namespace Content.Server.Atmos.EntitySystems private void InitializeCVars() { _cfg.OnValueChanged(CCVars.SpaceWind, value => SpaceWind = value, true); + _cfg.OnValueChanged(CCVars.SpaceWindSound, value => SpaceWindSound = value, true); _cfg.OnValueChanged(CCVars.MonstermosEqualization, value => MonstermosEqualization = value, true); + _cfg.OnValueChanged(CCVars.MonstermosDepressurization, value => MonstermosDepressurization = value, true); _cfg.OnValueChanged(CCVars.Superconduction, value => Superconduction = value, true); _cfg.OnValueChanged(CCVars.AtmosMaxProcessTime, value => AtmosMaxProcessTime = value, true); _cfg.OnValueChanged(CCVars.AtmosTickRate, value => AtmosTickRate = value, true); diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Grid.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Grid.cs index dd31f27527..b159597378 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Grid.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Grid.cs @@ -163,15 +163,15 @@ namespace Content.Server.Atmos.EntitySystems if (ComponentManager.TryGetComponent(mapGrid.GridEntityId, out GridAtmosphereComponent? gridAtmosphere)) { - foreach (var tileAtmos in gridAtmosphere) + foreach (var tile in gridAtmosphere) { - if (tileAtmos?.Air == null) + if (tile?.Air == null) continue; if(invalidate) - tileAtmos.Invalidate(); + gridAtmosphere.Invalidate(tile.GridIndices); - yield return tileAtmos.Air; + yield return tile.Air; } } } @@ -560,7 +560,7 @@ namespace Content.Server.Atmos.EntitySystems { var tileAtmos = gridAtmosphere.GetTile(tile)!; - for (var i = 0; i < tileAtmos.AdjacentTiles.Count; i++) + for (var i = 0; i < tileAtmos.AdjacentTiles.Length; i++) { var adjacentTile = tileAtmos.AdjacentTiles[i]; // TileAtmosphere has nullable disabled, so just in case... @@ -634,7 +634,7 @@ namespace Content.Server.Atmos.EntitySystems { var tileAtmos = gridAtmosphere.GetTile(tile)!; - for (var i = 0; i < tileAtmos.AdjacentTiles.Count; i++) + for (var i = 0; i < tileAtmos.AdjacentTiles.Length; i++) { var adjacentTile = tileAtmos.AdjacentTiles[i]; @@ -651,7 +651,7 @@ namespace Content.Server.Atmos.EntitySystems } if (invalidate) - adjacentTile.Invalidate(); + gridAtmosphere.Invalidate(adjacentTile.GridIndices); yield return adjacentTile.Air; } @@ -747,7 +747,12 @@ namespace Content.Server.Atmos.EntitySystems if (ComponentManager.TryGetComponent(mapGrid.GridEntityId, out GridAtmosphereComponent? gridAtmosphere)) { - gridAtmosphere.GetTile(tile, false)?.HotspotExpose(exposedTemperature, exposedVolume, soh); + var tileAtmosphere = gridAtmosphere.GetTile(tile, false); + + if (tileAtmosphere == null) + return; + + HotspotExpose(gridAtmosphere, tileAtmosphere, exposedTemperature, exposedVolume, soh); gridAtmosphere.Invalidate(tile); return; } diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.HighPressureDelta.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.HighPressureDelta.cs new file mode 100644 index 0000000000..b4d0da13c1 --- /dev/null +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.HighPressureDelta.cs @@ -0,0 +1,69 @@ +using Content.Server.Atmos.Components; +using Content.Shared.Atmos; +using Content.Shared.Audio; +using Robust.Server.GameObjects; +using Robust.Shared.Audio; +using Robust.Shared.Containers; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Physics; +using Robust.Shared.Player; + +namespace Content.Server.Atmos.EntitySystems +{ + public partial class AtmosphereSystem + { + private int _spaceWindSoundCooldown = 0; + + private void HighPressureMovements(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile) + { + // TODO ATMOS finish this + + if(tile.PressureDifference > 15) + { + if(_spaceWindSoundCooldown == 0) + { + var coordinates = tile.GridIndices.ToEntityCoordinates(tile.GridIndex, _mapManager); + if(!string.IsNullOrEmpty(SpaceWindSound)) + SoundSystem.Play(Filter.Pvs(coordinates), SpaceWindSound, coordinates, + AudioHelpers.WithVariation(0.125f).WithVolume(MathHelper.Clamp(tile.PressureDifference / 10, 10, 100))); + } + } + + foreach (var entity in Get().GetEntitiesIntersecting(tile.GridIndex, tile.GridIndices)) + { + if (!entity.TryGetComponent(out IPhysBody? physics) + || !entity.IsMovedByPressure(out var pressure) + || entity.IsInContainer()) + continue; + + var pressureMovements = physics.Owner.EnsureComponent(); + if (pressure.LastHighPressureMovementAirCycle < gridAtmosphere.UpdateCounter) + { + pressureMovements.ExperiencePressureDifference(gridAtmosphere.UpdateCounter, tile.PressureDifference, tile.PressureDirection, 0, tile.PressureSpecificTarget?.GridIndices.ToEntityCoordinates(tile.GridIndex, _mapManager) ?? EntityCoordinates.Invalid); + } + + } + + if (tile.PressureDifference > 100) + { + // TODO ATMOS Do space wind graphics here! + } + + _spaceWindSoundCooldown++; + if (_spaceWindSoundCooldown > 75) + _spaceWindSoundCooldown = 0; + } + + private void ConsiderPressureDifference(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile, TileAtmosphere other, float difference) + { + gridAtmosphere.AddHighPressureDelta(tile); + if (difference > tile.PressureDifference) + { + tile.PressureDifference = difference; + tile.PressureDirection = ((Vector2i)(tile.GridIndices - other.GridIndices)).GetDir().ToAtmosDirection(); + } + } + } +} diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Hotspot.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Hotspot.cs new file mode 100644 index 0000000000..1b0690d579 --- /dev/null +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Hotspot.cs @@ -0,0 +1,150 @@ +#nullable disable warnings +#nullable enable annotations +using Content.Server.Atmos.Components; +using Content.Server.Atmos.Reactions; +using Content.Server.Coordinates.Helpers; +using Content.Shared.Atmos; +using Content.Shared.GameTicking; +using Content.Shared.Maps; + +namespace Content.Server.Atmos.EntitySystems +{ + public partial class AtmosphereSystem + { + private void ProcessHotspot(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile) + { + if (!tile.Hotspot.Valid) + { + gridAtmosphere.RemoveHotspotTile(tile); + return; + } + + if (!tile.Excited) + { + gridAtmosphere.AddActiveTile(tile); + } + + if (!tile.Hotspot.SkippedFirstProcess) + { + tile.Hotspot.SkippedFirstProcess = true; + return; + } + + tile.ExcitedGroup?.ResetCooldowns(); + + if ((tile.Hotspot.Temperature < Atmospherics.FireMinimumTemperatureToExist) || (tile.Hotspot.Volume <= 1f) + || tile.Air == null || tile.Air.GetMoles(Gas.Oxygen) < 0.5f || (tile.Air.GetMoles(Gas.Plasma) < 0.5f && tile.Air.GetMoles(Gas.Tritium) < 0.5f)) + { + tile.Hotspot = new Hotspot(); + tile.UpdateVisuals(); + return; + } + + PerformHotspotExposure(gridAtmosphere, tile); + + if (tile.Hotspot.Bypassing) + { + tile.Hotspot.State = 3; + gridAtmosphere.BurnTile(tile.GridIndices); + + if (tile.Air.Temperature > Atmospherics.FireMinimumTemperatureToSpread) + { + var radiatedTemperature = tile.Air.Temperature * Atmospherics.FireSpreadRadiosityScale; + foreach (var otherTile in tile.AdjacentTiles) + { + if(!otherTile.Hotspot.Valid) + HotspotExpose(gridAtmosphere, otherTile, radiatedTemperature, Atmospherics.CellVolume/4); + } + } + } + else + { + tile.Hotspot.State = (byte) (tile.Hotspot.Volume > Atmospherics.CellVolume * 0.4f ? 2 : 1); + } + + if (tile.Hotspot.Temperature > tile.MaxFireTemperatureSustained) + tile.MaxFireTemperatureSustained = tile.Hotspot.Temperature; + + // TODO ATMOS Maybe destroy location here? + } + + private void HotspotExpose(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile, float exposedTemperature, float exposedVolume, bool soh = false) + { + if (tile.Air == null) + return; + + var oxygen = tile.Air.GetMoles(Gas.Oxygen); + + if (oxygen < 0.5f) + return; + + var plasma = tile.Air.GetMoles(Gas.Plasma); + var tritium = tile.Air.GetMoles(Gas.Tritium); + + if (tile.Hotspot.Valid) + { + if (soh) + { + if (plasma > 0.5f || tritium > 0.5f) + { + if (tile.Hotspot.Temperature < exposedTemperature) + tile.Hotspot.Temperature = exposedTemperature; + if (tile.Hotspot.Volume < exposedVolume) + tile.Hotspot.Volume = exposedVolume; + } + } + + return; + } + + if ((exposedTemperature > Atmospherics.PlasmaMinimumBurnTemperature) && (plasma > 0.5f || tritium > 0.5f)) + { + tile.Hotspot = new Hotspot + { + Volume = exposedVolume * 25f, + Temperature = exposedTemperature, + SkippedFirstProcess = tile.CurrentCycle > gridAtmosphere.UpdateCounter + }; + + tile.Hotspot.Start(); + + gridAtmosphere.AddActiveTile(tile); + gridAtmosphere.AddHotspotTile(tile); + } + } + + private void PerformHotspotExposure(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile) + { + if (tile.Air == null || !tile.Hotspot.Valid) return; + + tile.Hotspot.Bypassing = tile.Hotspot.SkippedFirstProcess && tile.Hotspot.Volume > tile.Air.Volume*0.95f; + + if (tile.Hotspot.Bypassing) + { + tile.Hotspot.Volume = tile.Air.ReactionResults[GasReaction.Fire] * Atmospherics.FireGrowthRate; + tile.Hotspot.Temperature = tile.Air.Temperature; + } + else + { + var affected = tile.Air.RemoveRatio(tile.Hotspot.Volume / tile.Air.Volume); + affected.Temperature = tile.Hotspot.Temperature; + gridAtmosphere.AtmosphereSystem.React(affected, tile); + tile.Hotspot.Temperature = affected.Temperature; + tile.Hotspot.Volume = affected.ReactionResults[GasReaction.Fire] * Atmospherics.FireGrowthRate; + Merge(tile.Air, affected); + gridAtmosphere.Invalidate(tile.GridIndices); + } + + var tileRef = tile.GridIndices.GetTileRef(tile.GridIndex); + + foreach (var entity in tileRef.GetEntitiesInTileFast()) + { + foreach (var fireAct in entity.GetAllComponents()) + { + + fireAct.FireAct(tile.Hotspot.Temperature, tile.Hotspot.Volume); + } + } + } + } +} diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.LINDA.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.LINDA.cs new file mode 100644 index 0000000000..a6c7a2ef38 --- /dev/null +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.LINDA.cs @@ -0,0 +1,135 @@ +#nullable disable warnings +#nullable enable annotations +using Content.Server.Atmos.Components; +using Content.Shared.Atmos; + +namespace Content.Server.Atmos.EntitySystems +{ + public partial class AtmosphereSystem + { + private void ProcessCell(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile, int fireCount, bool spaceWind = true) + { + // Can't process a tile without air + if (tile.Air == null) + { + gridAtmosphere.RemoveActiveTile(tile); + return; + } + + if (tile.ArchivedCycle < fireCount) + Archive(tile, fireCount); + + tile.CurrentCycle = fireCount; + var adjacentTileLength = 0; + + for (var i = 0; i < Atmospherics.Directions; i++) + { + var direction = (AtmosDirection) (1 << i); + if(tile.AdjacentBits.IsFlagSet(direction)) + adjacentTileLength++; + } + + for(var i = 0; i < Atmospherics.Directions; i++) + { + var direction = (AtmosDirection) (1 << i); + if (!tile.AdjacentBits.IsFlagSet(direction)) continue; + var enemyTile = tile.AdjacentTiles[i]; + + // If the tile is null or has no air, we don't do anything for it. + if(enemyTile?.Air == null) continue; + if (fireCount <= enemyTile.CurrentCycle) continue; + Archive(enemyTile, fireCount); + + var shouldShareAir = false; + + if (tile.ExcitedGroup != null && enemyTile.ExcitedGroup != null) + { + if (tile.ExcitedGroup != enemyTile.ExcitedGroup) + { + tile.ExcitedGroup.MergeGroups(enemyTile.ExcitedGroup); + } + + shouldShareAir = true; + } else if (tile.Air!.Compare(enemyTile.Air!) != GasMixture.GasCompareResult.NoExchange) + { + if (!enemyTile.Excited) + { + gridAtmosphere.AddActiveTile(enemyTile); + } + + var excitedGroup = tile.ExcitedGroup; + excitedGroup ??= enemyTile.ExcitedGroup; + + if (excitedGroup == null) + { + excitedGroup = new ExcitedGroup(); + excitedGroup.Initialize(gridAtmosphere); + } + + if (tile.ExcitedGroup == null) + excitedGroup.AddTile(tile); + + if(enemyTile.ExcitedGroup == null) + excitedGroup.AddTile(enemyTile); + + shouldShareAir = true; + } + + if (shouldShareAir) + { + var difference = Share(tile.Air!, enemyTile.Air!, adjacentTileLength); + + if (spaceWind) + { + if (difference > 0) + { + ConsiderPressureDifference(gridAtmosphere, tile, enemyTile, difference); + } + else + { + ConsiderPressureDifference(gridAtmosphere, enemyTile, tile, -difference); + } + } + + LastShareCheck(tile); + } + } + + if(tile.Air != null) + React(tile.Air, tile); + tile.UpdateVisuals(); + + var remove = true; + + if(tile.Air!.Temperature > Atmospherics.MinimumTemperatureStartSuperConduction) + if (ConsiderSuperconductivity(gridAtmosphere, tile, true)) + remove = false; + + if(tile.ExcitedGroup == null && remove) + gridAtmosphere.RemoveActiveTile(tile); + } + + private void Archive(TileAtmosphere tile, int fireCount) + { + tile.Air?.Archive(); + tile.ArchivedCycle = fireCount; + tile.TemperatureArchived = tile.Temperature; + } + + private void LastShareCheck(TileAtmosphere tile) + { + if (tile.Air == null || tile.ExcitedGroup == null) + return; + + switch (tile.Air.LastShare) + { + case > Atmospherics.MinimumAirToSuspend: + tile.ExcitedGroup.ResetCooldowns(); + break; + case > Atmospherics.MinimumMolesDeltaToMove: + tile.ExcitedGroup.DismantleCooldown = 0; + break; + } + } + } +} diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Monstermos.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Monstermos.cs new file mode 100644 index 0000000000..e3517e05b4 --- /dev/null +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Monstermos.cs @@ -0,0 +1,576 @@ +#nullable disable warnings +#nullable enable annotations +using System; +using System.Buffers; +using System.Collections.Generic; +using Content.Server.Atmos.Components; +using Content.Server.Coordinates.Helpers; +using Content.Shared.Atmos; +using Robust.Shared.IoC; +using Robust.Shared.Maths; +using Robust.Shared.Random; + +namespace Content.Server.Atmos.EntitySystems +{ + public partial class AtmosphereSystem + { + [Dependency] private readonly IRobustRandom _robustRandom = default!; + + private readonly TileAtmosphereComparer _monstermosComparer = new(); + + public void EqualizePressureInZone(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile, int cycleNum) + { + if (tile.Air == null || (tile.MonstermosInfo.LastCycle >= cycleNum)) + return; // Already done. + + tile.MonstermosInfo = new MonstermosInfo(); + + var startingMoles = tile.Air.TotalMoles; + var runAtmos = false; + + // We need to figure if this is necessary + for (var i = 0; i < Atmospherics.Directions; i++) + { + var direction = (AtmosDirection) (1 << i); + if (!tile.AdjacentBits.IsFlagSet(direction)) continue; + var other = tile.AdjacentTiles[i]; + if (other?.Air == null) continue; + var comparisonMoles = other.Air.TotalMoles; + if (!(MathF.Abs(comparisonMoles - startingMoles) > Atmospherics.MinimumMolesDeltaToMove)) continue; + runAtmos = true; + break; + } + + if (!runAtmos) // There's no need so we don't bother. + { + tile.MonstermosInfo.LastCycle = cycleNum; + return; + } + + var queueCycle = ++gridAtmosphere.EqualizationQueueCycleControl; + var totalMoles = 0f; + var tiles = ArrayPool.Shared.Rent(Atmospherics.MonstermosHardTileLimit); + tiles[0] = tile; + tile.MonstermosInfo.LastQueueCycle = queueCycle; + var tileCount = 1; + for (var i = 0; i < tileCount; i++) + { + if (i > Atmospherics.MonstermosHardTileLimit) break; + var exploring = tiles[i]; + + if (i < Atmospherics.MonstermosTileLimit) + { + var tileMoles = exploring.Air.TotalMoles; + exploring.MonstermosInfo.MoleDelta = tileMoles; + totalMoles += tileMoles; + } + + for (var j = 0; j < Atmospherics.Directions; j++) + { + var direction = (AtmosDirection) (1 << j); + if (!exploring.AdjacentBits.IsFlagSet(direction)) continue; + var adj = exploring.AdjacentTiles[j]; + if (adj?.Air == null) continue; + if(adj.MonstermosInfo.LastQueueCycle == queueCycle) continue; + adj.MonstermosInfo = new MonstermosInfo {LastQueueCycle = queueCycle}; + + if(tileCount < Atmospherics.MonstermosHardTileLimit) + tiles[tileCount++] = adj; + + if (adj.Air.Immutable) + { + // Looks like someone opened an airlock to space! + ExplosivelyDepressurize(gridAtmosphere, tile, cycleNum); + return; + } + } + } + + if (tileCount > Atmospherics.MonstermosTileLimit) + { + for (var i = Atmospherics.MonstermosTileLimit; i < tileCount; i++) + { + //We unmark them. We shouldn't be pushing/pulling gases to/from them. + var otherTile = tiles[i]; + + if (otherTile == null) + continue; + + tiles[i].MonstermosInfo.LastQueueCycle = 0; + } + + tileCount = Atmospherics.MonstermosTileLimit; + } + + var averageMoles = totalMoles / (tileCount); + var giverTiles = ArrayPool.Shared.Rent(tileCount); + var takerTiles = ArrayPool.Shared.Rent(tileCount); + var giverTilesLength = 0; + var takerTilesLength = 0; + + for (var i = 0; i < tileCount; i++) + { + var otherTile = tiles[i]; + otherTile.MonstermosInfo.LastCycle = cycleNum; + otherTile.MonstermosInfo.MoleDelta -= averageMoles; + if (otherTile.MonstermosInfo.MoleDelta > 0) + { + giverTiles[giverTilesLength++] = otherTile; + } + else + { + takerTiles[takerTilesLength++] = otherTile; + } + } + + var logN = MathF.Log2(tileCount); + + // Optimization - try to spread gases using an O(nlogn) algorithm that has a chance of not working first to avoid O(n^2) + if (giverTilesLength > logN && takerTilesLength > logN) + { + // Even if it fails, it will speed up the next part. + Array.Sort(tiles, 0, tileCount, _monstermosComparer); + + for (var i = 0; i < tileCount; i++) + { + var otherTile = tiles[i]; + otherTile.MonstermosInfo.FastDone = true; + if (!(otherTile.MonstermosInfo.MoleDelta > 0)) continue; + var eligibleDirections = AtmosDirection.Invalid; + var eligibleDirectionCount = 0; + for (var j = 0; j < Atmospherics.Directions; j++) + { + var direction = (AtmosDirection) (1 << j); + if (!otherTile.AdjacentBits.IsFlagSet(direction)) continue; + var tile2 = otherTile.AdjacentTiles[j]; + + // skip anything that isn't part of our current processing block. + if (tile2.MonstermosInfo.FastDone || tile2.MonstermosInfo.LastQueueCycle != queueCycle) + continue; + + eligibleDirections |= direction; + eligibleDirectionCount++; + } + + if (eligibleDirectionCount <= 0) + continue; // Oof we've painted ourselves into a corner. Bad luck. Next part will handle this. + + var molesToMove = otherTile.MonstermosInfo.MoleDelta / eligibleDirectionCount; + for (var j = 0; j < Atmospherics.Directions; j++) + { + var direction = (AtmosDirection) (1 << j); + if (!eligibleDirections.IsFlagSet(direction)) continue; + + AdjustEqMovement(otherTile, direction, molesToMove); + otherTile.MonstermosInfo.MoleDelta -= molesToMove; + otherTile.AdjacentTiles[j].MonstermosInfo.MoleDelta += molesToMove; + } + } + + giverTilesLength = 0; + takerTilesLength = 0; + + for (var i = 0; i < tileCount; i++) + { + var otherTile = tiles[i]; + if (otherTile.MonstermosInfo.MoleDelta > 0) + { + giverTiles[giverTilesLength++] = otherTile; + } + else + { + takerTiles[takerTilesLength++] = otherTile; + } + } + } + + // This is the part that can become O(n^2). + if (giverTilesLength < takerTilesLength) + { + // as an optimization, we choose one of two methods based on which list is smaller. We really want to avoid O(n^2) if we can. + var queue = ArrayPool.Shared.Rent(tileCount); + for (var j = 0; j < giverTilesLength; j++) + { + var giver = giverTiles[j]; + giver.MonstermosInfo.CurrentTransferDirection = AtmosDirection.Invalid; + giver.MonstermosInfo.CurrentTransferAmount = 0; + var queueCycleSlow = ++gridAtmosphere.EqualizationQueueCycleControl; + var queueLength = 0; + queue[queueLength++] = giver; + giver.MonstermosInfo.LastSlowQueueCycle = queueCycleSlow; + for (var i = 0; i < queueLength; i++) + { + if (giver.MonstermosInfo.MoleDelta <= 0) + break; // We're done here now. Let's not do more work than needed. + + var otherTile = queue[i]; + for (var k = 0; k < Atmospherics.Directions; k++) + { + var direction = (AtmosDirection) (1 << k); + if (!otherTile.AdjacentBits.IsFlagSet(direction)) continue; + var otherTile2 = otherTile.AdjacentTiles[k]; + if (giver.MonstermosInfo.MoleDelta <= 0) break; // We're done here now. Let's not do more work than needed. + if (otherTile2 == null || otherTile2.MonstermosInfo.LastQueueCycle != queueCycle) continue; + if (otherTile2.MonstermosInfo.LastSlowQueueCycle == queueCycleSlow) continue; + + queue[queueLength++] = otherTile2; + otherTile2.MonstermosInfo.LastSlowQueueCycle = queueCycleSlow; + otherTile2.MonstermosInfo.CurrentTransferDirection = direction.GetOpposite(); + otherTile2.MonstermosInfo.CurrentTransferAmount = 0; + if (otherTile2.MonstermosInfo.MoleDelta < 0) + { + // This tile needs gas. Let's give it to 'em. + if (-otherTile2.MonstermosInfo.MoleDelta > giver.MonstermosInfo.MoleDelta) + { + // We don't have enough gas! + otherTile2.MonstermosInfo.CurrentTransferAmount -= giver.MonstermosInfo.MoleDelta; + otherTile2.MonstermosInfo.MoleDelta += giver.MonstermosInfo.MoleDelta; + giver.MonstermosInfo.MoleDelta = 0; + } + else + { + // We have enough gas. + otherTile2.MonstermosInfo.CurrentTransferAmount += otherTile2.MonstermosInfo.MoleDelta; + giver.MonstermosInfo.MoleDelta += otherTile2.MonstermosInfo.MoleDelta; + otherTile2.MonstermosInfo.MoleDelta = 0; + } + } + } + } + + // Putting this loop here helps make it O(n^2) over O(n^3) + for (var i = queueLength - 1; i >= 0; i--) + { + var otherTile = queue[i]; + if (otherTile.MonstermosInfo.CurrentTransferAmount != 0 && otherTile.MonstermosInfo.CurrentTransferDirection != AtmosDirection.Invalid) + { + AdjustEqMovement(otherTile, otherTile.MonstermosInfo.CurrentTransferDirection, otherTile.MonstermosInfo.CurrentTransferAmount); + otherTile.AdjacentTiles[otherTile.MonstermosInfo.CurrentTransferDirection.ToIndex()] + .MonstermosInfo.CurrentTransferAmount += otherTile.MonstermosInfo.CurrentTransferAmount; + otherTile.MonstermosInfo.CurrentTransferAmount = 0; + } + } + } + + ArrayPool.Shared.Return(queue); + } + else + { + var queue = ArrayPool.Shared.Rent(tileCount); + for (var j = 0; j < takerTilesLength; j++) + { + var taker = takerTiles[j]; + taker.MonstermosInfo.CurrentTransferDirection = AtmosDirection.Invalid; + taker.MonstermosInfo.CurrentTransferAmount = 0; + var queueCycleSlow = ++gridAtmosphere.EqualizationQueueCycleControl; + var queueLength = 0; + queue[queueLength++] = taker; + taker.MonstermosInfo.LastSlowQueueCycle = queueCycleSlow; + for (var i = 0; i < queueLength; i++) + { + if (taker.MonstermosInfo.MoleDelta >= 0) + break; // We're done here now. Let's not do more work than needed. + + var otherTile = queue[i]; + for (var k = 0; k < Atmospherics.Directions; k++) + { + var direction = (AtmosDirection) (1 << k); + if (!otherTile.AdjacentBits.IsFlagSet(direction)) continue; + var otherTile2 = otherTile.AdjacentTiles[k]; + + if (taker.MonstermosInfo.MoleDelta >= 0) break; // We're done here now. Let's not do more work than needed. + if (otherTile2 == null || otherTile2.MonstermosInfo.LastQueueCycle != queueCycle) continue; + if (otherTile2.MonstermosInfo.LastSlowQueueCycle == queueCycleSlow) continue; + queue[queueLength++] = otherTile2; + otherTile2.MonstermosInfo.LastSlowQueueCycle = queueCycleSlow; + otherTile2.MonstermosInfo.CurrentTransferDirection = direction.GetOpposite(); + otherTile2.MonstermosInfo.CurrentTransferAmount = 0; + + if (otherTile2.MonstermosInfo.MoleDelta > 0) + { + // This tile has gas we can suck, so let's + if (otherTile2.MonstermosInfo.MoleDelta > -taker.MonstermosInfo.MoleDelta) + { + // They have enough gas + otherTile2.MonstermosInfo.CurrentTransferAmount -= taker.MonstermosInfo.MoleDelta; + otherTile2.MonstermosInfo.MoleDelta += taker.MonstermosInfo.MoleDelta; + taker.MonstermosInfo.MoleDelta = 0; + } + else + { + // They don't have enough gas! + otherTile2.MonstermosInfo.CurrentTransferAmount += otherTile2.MonstermosInfo.MoleDelta; + taker.MonstermosInfo.MoleDelta += otherTile2.MonstermosInfo.MoleDelta; + otherTile2.MonstermosInfo.MoleDelta = 0; + } + } + } + } + + for (var i = queueLength - 1; i >= 0; i--) + { + var otherTile = queue[i]; + if (otherTile.MonstermosInfo.CurrentTransferAmount == 0 || otherTile.MonstermosInfo.CurrentTransferDirection == AtmosDirection.Invalid) + continue; + + AdjustEqMovement(otherTile, otherTile.MonstermosInfo.CurrentTransferDirection, otherTile.MonstermosInfo.CurrentTransferAmount); + + otherTile.AdjacentTiles[otherTile.MonstermosInfo.CurrentTransferDirection.ToIndex()] + .MonstermosInfo.CurrentTransferAmount += otherTile.MonstermosInfo.CurrentTransferAmount; + otherTile.MonstermosInfo.CurrentTransferAmount = 0; + } + } + + ArrayPool.Shared.Return(queue); + } + + for (var i = 0; i < tileCount; i++) + { + var otherTile = tiles[i]; + FinalizeEq(gridAtmosphere, otherTile); + } + + for (var i = 0; i < tileCount; i++) + { + var otherTile = tiles[i]; + for (var j = 0; j < Atmospherics.Directions; j++) + { + var direction = (AtmosDirection) (1 << j); + if (!otherTile.AdjacentBits.IsFlagSet(direction)) continue; + var otherTile2 = otherTile.AdjacentTiles[j]; + if (otherTile2?.Air?.Compare(tile.Air) == GasMixture.GasCompareResult.NoExchange) continue; + gridAtmosphere.AddActiveTile(otherTile2); + break; + } + } + + ArrayPool.Shared.Return(tiles); + ArrayPool.Shared.Return(giverTiles); + ArrayPool.Shared.Return(takerTiles); + } + + public void ExplosivelyDepressurize(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile, int cycleNum) + { + // Check if explosive depressurization is enabled and if the tile is valid. + if (!MonstermosDepressurization || tile.Air == null) + return; + + const int limit = Atmospherics.MonstermosHardTileLimit; + + var totalGasesRemoved = 0f; + var queueCycle = ++gridAtmosphere.EqualizationQueueCycleControl; + var tiles = ArrayPool.Shared.Rent(limit); + var spaceTiles = ArrayPool.Shared.Rent(limit); + + var tileCount = 0; + var spaceTileCount = 0; + + tiles[tileCount++] = tile; + + tile.MonstermosInfo = new MonstermosInfo {LastQueueCycle = queueCycle}; + + for (var i = 0; i < tileCount; i++) + { + var otherTile = tiles[i]; + otherTile.MonstermosInfo.LastCycle = cycleNum; + otherTile.MonstermosInfo.CurrentTransferDirection = AtmosDirection.Invalid; + if (otherTile.Air.Immutable) + { + spaceTiles[spaceTileCount++] = otherTile; + otherTile.PressureSpecificTarget = otherTile; + } + else + { + for (var j = 0; j < Atmospherics.Directions; j++) + { + var direction = (AtmosDirection) (1 << j); + if (!otherTile.AdjacentBits.IsFlagSet(direction)) continue; + var otherTile2 = otherTile.AdjacentTiles[j]; + if (otherTile2.Air == null) continue; + if (otherTile2.MonstermosInfo.LastQueueCycle == queueCycle) continue; + + ConsiderFirelocks(otherTile, otherTile2); + + // The firelocks might have closed on us. + if (!otherTile.AdjacentBits.IsFlagSet(direction)) continue; + otherTile2.MonstermosInfo = new MonstermosInfo {LastQueueCycle = queueCycle}; + tiles[tileCount++] = otherTile2; + } + } + + if (tileCount >= limit || spaceTileCount >= limit) + break; + } + + var queueCycleSlow = ++gridAtmosphere.EqualizationQueueCycleControl; + var progressionOrder = ArrayPool.Shared.Rent(limit * 2); + var progressionCount = 0; + + for (var i = 0; i < spaceTileCount; i++) + { + var otherTile = spaceTiles[i]; + progressionOrder[progressionCount++] = otherTile; + otherTile.MonstermosInfo.LastSlowQueueCycle = queueCycleSlow; + otherTile.MonstermosInfo.CurrentTransferDirection = AtmosDirection.Invalid; + } + + for (var i = 0; i < progressionCount; i++) + { + var otherTile = progressionOrder[i]; + for (var j = 0; j < Atmospherics.Directions; j++) + { + var direction = (AtmosDirection) (1 << j); + // TODO ATMOS This is a terrible hack that accounts for the mess that are space TileAtmospheres. + if (!otherTile.AdjacentBits.IsFlagSet(direction) && !otherTile.Air.Immutable) continue; + var tile2 = otherTile.AdjacentTiles[j]; + if (tile2?.MonstermosInfo.LastQueueCycle != queueCycle) continue; + if (tile2.MonstermosInfo.LastSlowQueueCycle == queueCycleSlow) continue; + if(tile2.Air?.Immutable ?? false) continue; + tile2.MonstermosInfo.CurrentTransferDirection = direction.GetOpposite(); + tile2.MonstermosInfo.CurrentTransferAmount = 0; + tile2.PressureSpecificTarget = otherTile.PressureSpecificTarget; + tile2.MonstermosInfo.LastSlowQueueCycle = queueCycleSlow; + progressionOrder[progressionCount++] = tile2; + } + } + + for (var i = progressionCount - 1; i >= 0; i--) + { + var otherTile = progressionOrder[i]; + if (otherTile.MonstermosInfo.CurrentTransferDirection == AtmosDirection.Invalid) continue; + gridAtmosphere.AddHighPressureDelta(otherTile); + gridAtmosphere.AddActiveTile(otherTile); + var otherTile2 = otherTile.AdjacentTiles[otherTile.MonstermosInfo.CurrentTransferDirection.ToIndex()]; + if (otherTile2?.Air == null) continue; + var sum = otherTile2.Air.TotalMoles; + totalGasesRemoved += sum; + otherTile.MonstermosInfo.CurrentTransferAmount += sum; + otherTile2.MonstermosInfo.CurrentTransferAmount += otherTile.MonstermosInfo.CurrentTransferAmount; + otherTile.PressureDifference = otherTile.MonstermosInfo.CurrentTransferAmount; + otherTile.PressureDirection = otherTile.MonstermosInfo.CurrentTransferDirection; + + if (otherTile2.MonstermosInfo.CurrentTransferDirection == AtmosDirection.Invalid) + { + otherTile2.PressureDifference = otherTile2.MonstermosInfo.CurrentTransferAmount; + otherTile2.PressureDirection = otherTile.MonstermosInfo.CurrentTransferDirection; + } + + otherTile.Air.Clear(); + otherTile.UpdateVisuals(); + HandleDecompressionFloorRip(gridAtmosphere, otherTile, sum); + } + + ArrayPool.Shared.Return(tiles); + ArrayPool.Shared.Return(spaceTiles); + ArrayPool.Shared.Return(progressionOrder); + } + + private void ConsiderFirelocks(TileAtmosphere tile, TileAtmosphere other) + { + if (!_mapManager.TryGetGrid(tile.GridIndex, out var mapGrid)) + return; + + var reconsiderAdjacent = false; + + foreach (var entity in mapGrid.GetAnchoredEntities(tile.GridIndices)) + { + if (!ComponentManager.TryGetComponent(entity, out FirelockComponent firelock)) + continue; + + reconsiderAdjacent |= firelock.EmergencyPressureStop(); + } + + foreach (var entity in mapGrid.GetAnchoredEntities(other.GridIndices)) + { + if (!ComponentManager.TryGetComponent(entity, out FirelockComponent firelock)) + continue; + + reconsiderAdjacent |= firelock.EmergencyPressureStop(); + } + + if (!reconsiderAdjacent) + return; + + tile.UpdateAdjacent(); + other.UpdateAdjacent(); + } + + public void FinalizeEq(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile) + { + Span transferDirections = stackalloc float[Atmospherics.Directions]; + var hasTransferDirs = false; + for (var i = 0; i < Atmospherics.Directions; i++) + { + var amount = tile.MonstermosInfo[i]; + if (amount == 0) continue; + transferDirections[i] = amount; + tile.MonstermosInfo[i] = 0; // Set them to 0 to prevent infinite recursion. + hasTransferDirs = true; + } + + if (!hasTransferDirs) return; + + for(var i = 0; i < Atmospherics.Directions; i++) + { + var direction = (AtmosDirection) (1 << i); + if (!tile.AdjacentBits.IsFlagSet(direction)) continue; + var amount = transferDirections[i]; + var otherTile = tile.AdjacentTiles[i]; + if (otherTile?.Air == null) continue; + if (amount > 0) + { + if (tile.Air.TotalMoles < amount) + FinalizeEqNeighbors(gridAtmosphere, tile, transferDirections); + + otherTile.MonstermosInfo[direction.GetOpposite()] = 0; + Merge(otherTile.Air, tile.Air.Remove(amount)); + tile.UpdateVisuals(); + otherTile.UpdateVisuals(); + ConsiderPressureDifference(gridAtmosphere, tile, otherTile, amount); + } + } + } + + private void FinalizeEqNeighbors(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile, ReadOnlySpan transferDirs) + { + for (var i = 0; i < Atmospherics.Directions; i++) + { + var direction = (AtmosDirection) (1 << i); + var amount = transferDirs[i]; + if(amount < 0 && tile.AdjacentBits.IsFlagSet(direction)) + FinalizeEq(gridAtmosphere, tile.AdjacentTiles[i]); // A bit of recursion if needed. + } + } + + private void AdjustEqMovement(TileAtmosphere tile, AtmosDirection direction, float amount) + { + tile.MonstermosInfo[direction] += amount; + tile.AdjacentTiles[direction.ToIndex()].MonstermosInfo[direction.GetOpposite()] -= amount; + } + + private void HandleDecompressionFloorRip(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile, float sum) + { + var chance = MathHelper.Clamp(sum / 500, 0.005f, 0.5f); + + if (sum > 20 && _robustRandom.Prob(chance)) + gridAtmosphere.PryTile(tile.GridIndices); + } + + private class TileAtmosphereComparer : IComparer + { + public int Compare(TileAtmosphere a, TileAtmosphere b) + { + if (a == null && b == null) + return 0; + + if (a == null) + return -1; + + if (b == null) + return 1; + + return a.MonstermosInfo.MoleDelta.CompareTo(b.MonstermosInfo.MoleDelta); + } + } + } +} diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Processing.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Processing.cs index f2627ca8fb..53a3a49e39 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Processing.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Processing.cs @@ -33,7 +33,7 @@ namespace Content.Server.Atmos.EntitySystems var number = 0; while (atmosphere.CurrentRunTiles.TryDequeue(out var tile)) { - tile.EqualizePressureInZone(this, atmosphere.UpdateCounter); + EqualizePressureInZone(atmosphere, tile, atmosphere.UpdateCounter); if (number++ < LagCheckIterations) continue; number = 0; @@ -55,7 +55,7 @@ namespace Content.Server.Atmos.EntitySystems var number = 0; while (atmosphere.CurrentRunTiles.TryDequeue(out var tile)) { - tile.ProcessCell(this, atmosphere.UpdateCounter, SpaceWind); + ProcessCell(atmosphere, tile, atmosphere.UpdateCounter, SpaceWind); if (number++ < LagCheckIterations) continue; number = 0; @@ -106,7 +106,7 @@ namespace Content.Server.Atmos.EntitySystems var number = 0; while (atmosphere.CurrentRunTiles.TryDequeue(out var tile)) { - tile.HighPressureMovements(); + HighPressureMovements(atmosphere, tile); tile.PressureDifference = 0f; tile.PressureSpecificTarget = null; atmosphere.HighPressureDelta.Remove(tile); @@ -131,7 +131,7 @@ namespace Content.Server.Atmos.EntitySystems var number = 0; while (atmosphere.CurrentRunTiles.TryDequeue(out var hotspot)) { - hotspot.ProcessHotspot(); + ProcessHotspot(atmosphere, hotspot); if (number++ < LagCheckIterations) continue; number = 0; @@ -153,7 +153,7 @@ namespace Content.Server.Atmos.EntitySystems var number = 0; while (atmosphere.CurrentRunTiles.TryDequeue(out var superconductivity)) { - superconductivity.Superconduct(this); + Superconduct(atmosphere, superconductivity); if (number++ < LagCheckIterations) continue; number = 0; diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Superconductivity.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Superconductivity.cs new file mode 100644 index 0000000000..c9f93505c7 --- /dev/null +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Superconductivity.cs @@ -0,0 +1,157 @@ +using System; +using Content.Server.Atmos.Components; +using Content.Shared.Atmos; + +namespace Content.Server.Atmos.EntitySystems +{ + public partial class AtmosphereSystem + { + private void Superconduct(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile) + { + var directions = ConductivityDirections(gridAtmosphere, tile); + + for(var i = 0; i < Atmospherics.Directions; i++) + { + var direction = (AtmosDirection) (1 << i); + if (!directions.IsFlagSet(direction)) continue; + + var adjacent = tile.AdjacentTiles[direction.ToIndex()]; + + // TODO ATMOS handle adjacent being null. + if (adjacent == null || adjacent.ThermalConductivity == 0f) + continue; + + if(adjacent.ArchivedCycle < gridAtmosphere.UpdateCounter) + Archive(adjacent, gridAtmosphere.UpdateCounter); + + NeighborConductWithSource(gridAtmosphere, adjacent, tile); + + ConsiderSuperconductivity(gridAtmosphere, adjacent); + } + + RadiateToSpace(tile); + FinishSuperconduction(gridAtmosphere, tile); + } + + private AtmosDirection ConductivityDirections(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile) + { + if(tile.Air == null) + { + if(tile.ArchivedCycle < gridAtmosphere.UpdateCounter) + Archive(tile, gridAtmosphere.UpdateCounter); + return AtmosDirection.All; + } + + // TODO ATMOS check if this is correct + return AtmosDirection.All; + } + + public bool ConsiderSuperconductivity(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile) + { + if (tile.ThermalConductivity == 0f) + return false; + + gridAtmosphere.AddSuperconductivityTile(tile); + return true; + } + + public bool ConsiderSuperconductivity(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile, bool starting) + { + if (tile.Air == null || tile.Air.Temperature < (starting + ? Atmospherics.MinimumTemperatureStartSuperConduction + : Atmospherics.MinimumTemperatureForSuperconduction)) + return false; + + return !(gridAtmosphere.AtmosphereSystem.GetHeatCapacity(tile.Air) < Atmospherics.MCellWithRatio) + && ConsiderSuperconductivity(gridAtmosphere, tile); + } + + public void FinishSuperconduction(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile) + { + // Conduct with air on my tile if I have it + if (tile.Air != null) + { + tile.Temperature = TemperatureShare(tile.Air, tile.ThermalConductivity, tile.Temperature, tile.HeatCapacity); + } + + FinishSuperconduction(gridAtmosphere, tile, tile.Air?.Temperature ?? tile.Temperature); + } + + public void FinishSuperconduction(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile, float temperature) + { + // Make sure it's still hot enough to continue conducting. + if (temperature < Atmospherics.MinimumTemperatureForSuperconduction) + { + gridAtmosphere.RemoveSuperconductivityTile(tile); + } + } + + public void NeighborConductWithSource(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile, TileAtmosphere other) + { + if (tile.Air == null) + { + if (other.Tile != null) + { + TemperatureShareOpenToSolid(other, tile); + } + else + { + TemperatureShareMutualSolid(other, tile, tile.ThermalConductivity); + } + + // TODO ATMOS: tile.TemperatureExpose(null, tile.Temperature, gridAtmosphere.GetVolumeForCells(1)); + return; + } + + if (other.Air != null) + { + TemperatureShare(other.Air, tile.Air, Atmospherics.WindowHeatTransferCoefficient); + } + else + { + TemperatureShareOpenToSolid(tile, other); + } + + gridAtmosphere.AddActiveTile(tile); + } + + private void TemperatureShareOpenToSolid(TileAtmosphere tile, TileAtmosphere other) + { + if (tile.Air == null) + return; + + other.Temperature = TemperatureShare(tile.Air, other.ThermalConductivity, other.Temperature, other.HeatCapacity); + } + + private void TemperatureShareMutualSolid(TileAtmosphere tile, TileAtmosphere other, float conductionCoefficient) + { + var deltaTemperature = (tile.TemperatureArchived - other.TemperatureArchived); + if (MathF.Abs(deltaTemperature) > Atmospherics.MinimumTemperatureDeltaToConsider + && tile.HeatCapacity != 0f && other.HeatCapacity != 0f) + { + var heat = conductionCoefficient * deltaTemperature * + (tile.HeatCapacity * other.HeatCapacity / (tile.HeatCapacity + other.HeatCapacity)); + + tile.Temperature -= heat / tile.HeatCapacity; + other.Temperature += heat / other.HeatCapacity; + } + } + + public void RadiateToSpace(TileAtmosphere tile) + { + // Considering 0ºC as the break even point for radiation in and out. + if (tile.Temperature > Atmospherics.T0C) + { + // Hardcoded space temperature. + var deltaTemperature = (tile.TemperatureArchived - Atmospherics.TCMB); + if ((tile.HeatCapacity > 0) && (MathF.Abs(deltaTemperature) > Atmospherics.MinimumTemperatureDeltaToConsider)) + { + var heat = tile.ThermalConductivity * deltaTemperature * (tile.HeatCapacity * + Atmospherics.HeatCapacityVacuum / (tile.HeatCapacity + Atmospherics.HeatCapacityVacuum)); + + tile.Temperature -= heat; + } + } + } + } +} diff --git a/Content.Server/Atmos/TileAtmosInfo.cs b/Content.Server/Atmos/MonstermosInfo.cs similarity index 97% rename from Content.Server/Atmos/TileAtmosInfo.cs rename to Content.Server/Atmos/MonstermosInfo.cs index 68a6dcfea1..cfb1ad7b4b 100644 --- a/Content.Server/Atmos/TileAtmosInfo.cs +++ b/Content.Server/Atmos/MonstermosInfo.cs @@ -4,7 +4,7 @@ using Robust.Shared.ViewVariables; namespace Content.Server.Atmos { - public struct TileAtmosInfo + public struct MonstermosInfo { [ViewVariables] public int LastCycle; @@ -30,6 +30,15 @@ namespace Content.Server.Atmos [ViewVariables] public float TransferDirectionSouth; + [ViewVariables] + public float CurrentTransferAmount; + + [ViewVariables] + public AtmosDirection CurrentTransferDirection; + + [ViewVariables] + public bool FastDone; + public float this[AtmosDirection direction] { get => @@ -69,13 +78,5 @@ namespace Content.Server.Atmos get => this[(AtmosDirection) (1 << index)]; set => this[(AtmosDirection) (1 << index)] = value; } - - [ViewVariables] - public float CurrentTransferAmount; - - public AtmosDirection CurrentTransferDirection; - - [ViewVariables] - public bool FastDone; } } diff --git a/Content.Server/Atmos/Reactions/PlasmaFireReaction.cs b/Content.Server/Atmos/Reactions/PlasmaFireReaction.cs index 3a5564c929..4df4ca508d 100644 --- a/Content.Server/Atmos/Reactions/PlasmaFireReaction.cs +++ b/Content.Server/Atmos/Reactions/PlasmaFireReaction.cs @@ -73,7 +73,7 @@ namespace Content.Server.Atmos.Reactions temperature = mixture.Temperature; if (temperature > Atmospherics.FireMinimumTemperatureToExist) { - location.HotspotExpose(temperature, mixture.Volume); + atmosphereSystem.HotspotExpose(location.GridIndex, location.GridIndices, temperature, mixture.Volume); foreach (var entity in location.GridIndices.GetEntitiesInTileFast(location.GridIndex)) { @@ -83,7 +83,7 @@ namespace Content.Server.Atmos.Reactions } } - location.TemperatureExpose(mixture, temperature, mixture.Volume); + // TODO ATMOS: location.TemperatureExpose(mixture, temperature, mixture.Volume); } } diff --git a/Content.Server/Atmos/Reactions/TritiumFireReaction.cs b/Content.Server/Atmos/Reactions/TritiumFireReaction.cs index 5739dc797a..1bb648e73b 100644 --- a/Content.Server/Atmos/Reactions/TritiumFireReaction.cs +++ b/Content.Server/Atmos/Reactions/TritiumFireReaction.cs @@ -63,7 +63,7 @@ namespace Content.Server.Atmos.Reactions temperature = mixture.Temperature; if (temperature > Atmospherics.FireMinimumTemperatureToExist) { - location.HotspotExpose(temperature, mixture.Volume); + atmosphereSystem.HotspotExpose(location.GridIndex, location.GridIndices, temperature, mixture.Volume); foreach (var entity in location.GridIndices.GetEntitiesInTileFast(location.GridIndex)) { @@ -73,7 +73,7 @@ namespace Content.Server.Atmos.Reactions } } - location.TemperatureExpose(mixture, temperature, mixture.Volume); + // TODO ATMOS: location.TemperatureExpose(mixture, temperature, mixture.Volume); } } diff --git a/Content.Server/Atmos/TileAtmosphere.cs b/Content.Server/Atmos/TileAtmosphere.cs index d3328ce7fb..d5f2881c05 100644 --- a/Content.Server/Atmos/TileAtmosphere.cs +++ b/Content.Server/Atmos/TileAtmosphere.cs @@ -1,54 +1,29 @@ #nullable disable warnings #nullable enable annotations -using System; -using System.Buffers; -using System.Collections.Generic; using System.Runtime.CompilerServices; using Content.Server.Atmos.Components; -using Content.Server.Atmos.EntitySystems; -using Content.Server.Atmos.Reactions; -using Content.Server.Coordinates.Helpers; using Content.Server.Interfaces; using Content.Shared.Atmos; -using Content.Shared.Audio; using Content.Shared.Maps; -using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Robust.Server.GameObjects; -using Robust.Shared.Audio; -using Robust.Shared.Containers; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; -using Robust.Shared.Physics; -using Robust.Shared.Player; -using Robust.Shared.Random; using Robust.Shared.ViewVariables; namespace Content.Server.Atmos { public class TileAtmosphere : IGasMixtureHolder { - [Robust.Shared.IoC.Dependency] private readonly IRobustRandom _robustRandom = default!; - [Robust.Shared.IoC.Dependency] private readonly IEntityManager _entityManager = default!; - [Robust.Shared.IoC.Dependency] private readonly IMapManager _mapManager = default!; - private readonly GridTileLookupSystem _gridTileLookupSystem = default!; - - - private static readonly TileAtmosphereComparer Comparer = new(); - - [ViewVariables] private int _archivedCycle; - [ViewVariables] private int _currentCycle; + [ViewVariables] + public int ArchivedCycle; [ViewVariables] - public float Temperature { get; private set; } = Atmospherics.T20C; + public int CurrentCycle; [ViewVariables] - private float _temperatureArchived = Atmospherics.T20C; + public float Temperature { get; set; } = Atmospherics.T20C; - // I know this being static is evil, but I seriously can't come up with a better solution to sound spam. - private static int _soundCooldown; + [ViewVariables] + public float TemperatureArchived { get; set; } = Atmospherics.T20C; [ViewVariables] public TileAtmosphere? PressureSpecificTarget { get; set; } @@ -72,28 +47,19 @@ namespace Content.Server.Atmos /// Adjacent tiles in the same order as . (NSEW) /// [ViewVariables] - private readonly TileAtmosphere[] _adjacentTiles = new TileAtmosphere[Atmospherics.Directions]; - - public IReadOnlyList AdjacentTiles => _adjacentTiles; - - private AtmosDirection _adjacentBits = AtmosDirection.Invalid; - - [ViewVariables, UsedImplicitly] - private int AdjacentBitsInt => (int)_adjacentBits; + public readonly TileAtmosphere?[] AdjacentTiles = new TileAtmosphere[Atmospherics.Directions]; [ViewVariables] - private TileAtmosInfo _tileAtmosInfo; + public AtmosDirection AdjacentBits = AtmosDirection.Invalid; + + [ViewVariables] + public MonstermosInfo MonstermosInfo; [ViewVariables] public Hotspot Hotspot; - private AtmosDirection _pressureDirection; - - // I'm assuming there's a good reason the original variable was made private, but this information is also important. - public AtmosDirection PressureDirectionForDebugOverlay => _pressureDirection; - - [ViewVariables, UsedImplicitly] - private int PressureDirectionInt => (int)_pressureDirection; + [ViewVariables] + public AtmosDirection PressureDirection; [ViewVariables] public GridId GridIndex { get; } @@ -108,25 +74,21 @@ namespace Content.Server.Atmos public ExcitedGroup? ExcitedGroup { get; set; } /// - /// The air in this tile. If null, this tile is completely airblocked. + /// The air in this tile. If null, this tile is completely air-blocked. /// This can be immutable if the tile is spaced. /// [ViewVariables] public GasMixture? Air { get; set; } - [ViewVariables, UsedImplicitly] - private int _blockedAirflow => (int)BlockedAirflow; - - public AtmosDirection BlockedAirflow { get; set; } = AtmosDirection.Invalid; + [ViewVariables] + public float MaxFireTemperatureSustained { get; set; } [ViewVariables] - public bool BlocksAllAir => BlockedAirflow == AtmosDirection.All; + public AtmosDirection BlockedAirflow { get; set; } = AtmosDirection.Invalid; public TileAtmosphere(GridAtmosphereComponent atmosphereComponent, GridId gridIndex, Vector2i gridIndices, GasMixture? mixture = null, bool immutable = false) { - IoCManager.InjectDependencies(this); _gridAtmosphereComponent = atmosphereComponent; - _gridTileLookupSystem = _entityManager.EntitySysManager.GetEntitySystem(); GridIndex = gridIndex; GridIndices = gridIndices; Air = mixture; @@ -135,1008 +97,6 @@ namespace Content.Server.Atmos Air?.MarkImmutable(); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void Archive(int fireCount) - { - Air?.Archive(); - _archivedCycle = fireCount; - _temperatureArchived = Temperature; - } - - public void HighPressureMovements() - { - // TODO ATMOS finish this - - if(PressureDifference > 15) - { - if(_soundCooldown == 0) - { - var coordinates = GridIndices.ToEntityCoordinates(GridIndex, _mapManager); - SoundSystem.Play(Filter.Pvs(coordinates), "/Audio/Effects/space_wind.ogg", - coordinates, AudioHelpers.WithVariation(0.125f).WithVolume(MathHelper.Clamp(PressureDifference / 10, 10, 100))); - } - } - - foreach (var entity in _gridTileLookupSystem.GetEntitiesIntersecting(GridIndex, GridIndices)) - { - if (!entity.TryGetComponent(out IPhysBody physics) - || !entity.IsMovedByPressure(out var pressure) - || entity.IsInContainer()) - continue; - - var pressureMovements = physics.Owner.EnsureComponent(); - if (pressure.LastHighPressureMovementAirCycle < _gridAtmosphereComponent.UpdateCounter) - { - pressureMovements.ExperiencePressureDifference(_gridAtmosphereComponent.UpdateCounter, PressureDifference, _pressureDirection, 0, PressureSpecificTarget?.GridIndices.ToEntityCoordinates(GridIndex, _mapManager) ?? EntityCoordinates.Invalid); - } - - } - - if (PressureDifference > 100) - { - // TODO ATMOS Do space wind graphics here! - } - - _soundCooldown++; - if (_soundCooldown > 75) - _soundCooldown = 0; - } - - private class TileAtmosphereComparer : IComparer - { - public int Compare(TileAtmosphere a, TileAtmosphere b) - { - if (a == null && b == null) - return 0; - - if (a == null) - return -1; - - if (b == null) - return 1; - - return a._tileAtmosInfo.MoleDelta.CompareTo(b._tileAtmosInfo.MoleDelta); - } - } - - public void EqualizePressureInZone(AtmosphereSystem atmosphereSystem, int cycleNum) - { - if (Air == null || (_tileAtmosInfo.LastCycle >= cycleNum)) return; // Already done. - - _tileAtmosInfo = new TileAtmosInfo(); - - var startingMoles = Air.TotalMoles; - var runAtmos = false; - - // We need to figure if this is necessary - for (var i = 0; i < Atmospherics.Directions; i++) - { - var direction = (AtmosDirection) (1 << i); - if (!_adjacentBits.IsFlagSet(direction)) continue; - var other = _adjacentTiles[i]; - if (other?.Air == null) continue; - var comparisonMoles = other.Air.TotalMoles; - if (!(MathF.Abs(comparisonMoles - startingMoles) > Atmospherics.MinimumMolesDeltaToMove)) continue; - runAtmos = true; - break; - } - - if (!runAtmos) // There's no need so we don't bother. - { - _tileAtmosInfo.LastCycle = cycleNum; - return; - } - - var queueCycle = ++_gridAtmosphereComponent.EqualizationQueueCycleControl; - var totalMoles = 0f; - var tiles = ArrayPool.Shared.Rent(Atmospherics.ZumosHardTileLimit); - tiles[0] = this; - _tileAtmosInfo.LastQueueCycle = queueCycle; - var tileCount = 1; - for (var i = 0; i < tileCount; i++) - { - if (i > Atmospherics.ZumosHardTileLimit) break; - var exploring = tiles[i]; - - if (i < Atmospherics.ZumosTileLimit) - { - var tileMoles = exploring.Air.TotalMoles; - exploring._tileAtmosInfo.MoleDelta = tileMoles; - totalMoles += tileMoles; - } - - for (var j = 0; j < Atmospherics.Directions; j++) - { - var direction = (AtmosDirection) (1 << j); - if (!exploring._adjacentBits.IsFlagSet(direction)) continue; - var adj = exploring._adjacentTiles[j]; - if (adj?.Air == null) continue; - if(adj._tileAtmosInfo.LastQueueCycle == queueCycle) continue; - adj._tileAtmosInfo = new TileAtmosInfo {LastQueueCycle = queueCycle}; - - if(tileCount < Atmospherics.ZumosHardTileLimit) - tiles[tileCount++] = adj; - if (adj.Air.Immutable) - { - // Looks like someone opened an airlock to space! - ExplosivelyDepressurize(cycleNum); - return; - } - } - } - - if (tileCount > Atmospherics.ZumosTileLimit) - { - for (var i = Atmospherics.ZumosTileLimit; i < tileCount; i++) - { - //We unmark them. We shouldn't be pushing/pulling gases to/from them. - var tile = tiles[i]; - if (tile == null) continue; - tiles[i]._tileAtmosInfo.LastQueueCycle = 0; - } - - tileCount = Atmospherics.ZumosTileLimit; - } - - //tiles = tiles.AsSpan().Slice(0, tileCount).ToArray(); // According to my benchmarks, this is much slower. - //Array.Resize(ref tiles, tileCount); - - var averageMoles = totalMoles / (tileCount); - var giverTiles = ArrayPool.Shared.Rent(tileCount); - var takerTiles = ArrayPool.Shared.Rent(tileCount); - var giverTilesLength = 0; - var takerTilesLength = 0; - - for (var i = 0; i < tileCount; i++) - { - var tile = tiles[i]; - tile._tileAtmosInfo.LastCycle = cycleNum; - tile._tileAtmosInfo.MoleDelta -= averageMoles; - if (tile._tileAtmosInfo.MoleDelta > 0) - { - giverTiles[giverTilesLength++] = tile; - } - else - { - takerTiles[takerTilesLength++] = tile; - } - } - - var logN = MathF.Log2(tileCount); - - // Optimization - try to spread gases using an O(nlogn) algorithm that has a chance of not working first to avoid O(n^2) - if (giverTilesLength > logN && takerTilesLength > logN) - { - // Even if it fails, it will speed up the next part. - Array.Sort(tiles, 0, tileCount, Comparer); - - for (var i = 0; i < tileCount; i++) - { - var tile = tiles[i]; - tile._tileAtmosInfo.FastDone = true; - if (!(tile._tileAtmosInfo.MoleDelta > 0)) continue; - var eligibleDirections = AtmosDirection.Invalid; - var eligibleDirectionCount = 0; - for (var j = 0; j < Atmospherics.Directions; j++) - { - var direction = (AtmosDirection) (1 << j); - if (!tile._adjacentBits.IsFlagSet(direction)) continue; - var tile2 = tile._adjacentTiles[j]; - - // skip anything that isn't part of our current processing block. - if (tile2._tileAtmosInfo.FastDone || tile2._tileAtmosInfo.LastQueueCycle != queueCycle) - continue; - - eligibleDirections |= direction; - eligibleDirectionCount++; - } - - if (eligibleDirectionCount <= 0) - continue; // Oof we've painted ourselves into a corner. Bad luck. Next part will handle this. - - var molesToMove = tile._tileAtmosInfo.MoleDelta / eligibleDirectionCount; - for (var j = 0; j < Atmospherics.Directions; j++) - { - var direction = (AtmosDirection) (1 << j); - if (!eligibleDirections.IsFlagSet(direction)) continue; - - tile.AdjustEqMovement(direction, molesToMove); - tile._tileAtmosInfo.MoleDelta -= molesToMove; - tile._adjacentTiles[j]._tileAtmosInfo.MoleDelta += molesToMove; - } - } - - giverTilesLength = 0; - takerTilesLength = 0; - - for (var i = 0; i < tileCount; i++) - { - var tile = tiles[i]; - if (tile._tileAtmosInfo.MoleDelta > 0) - { - giverTiles[giverTilesLength++] = tile; - } - else - { - takerTiles[takerTilesLength++] = tile; - } - } - } - - // This is the part that can become O(n^2). - if (giverTilesLength < takerTilesLength) - { - // as an optimization, we choose one of two methods based on which list is smaller. We really want to avoid O(n^2) if we can. - var queue = ArrayPool.Shared.Rent(tileCount); - for (var j = 0; j < giverTilesLength; j++) - { - var giver = giverTiles[j]; - giver._tileAtmosInfo.CurrentTransferDirection = AtmosDirection.Invalid; - giver._tileAtmosInfo.CurrentTransferAmount = 0; - var queueCycleSlow = ++_gridAtmosphereComponent.EqualizationQueueCycleControl; - var queueLength = 0; - queue[queueLength++] = giver; - giver._tileAtmosInfo.LastSlowQueueCycle = queueCycleSlow; - for (var i = 0; i < queueLength; i++) - { - if (giver._tileAtmosInfo.MoleDelta <= 0) - break; // We're done here now. Let's not do more work than needed. - - var tile = queue[i]; - for (var k = 0; k < Atmospherics.Directions; k++) - { - var direction = (AtmosDirection) (1 << k); - if (!tile._adjacentBits.IsFlagSet(direction)) continue; - var tile2 = tile._adjacentTiles[k]; - if (giver._tileAtmosInfo.MoleDelta <= 0) break; // We're done here now. Let's not do more work than needed. - if (tile2 == null || tile2._tileAtmosInfo.LastQueueCycle != queueCycle) continue; - if (tile2._tileAtmosInfo.LastSlowQueueCycle == queueCycleSlow) continue; - - queue[queueLength++] = tile2; - tile2._tileAtmosInfo.LastSlowQueueCycle = queueCycleSlow; - tile2._tileAtmosInfo.CurrentTransferDirection = direction.GetOpposite(); - tile2._tileAtmosInfo.CurrentTransferAmount = 0; - if (tile2._tileAtmosInfo.MoleDelta < 0) - { - // This tile needs gas. Let's give it to 'em. - if (-tile2._tileAtmosInfo.MoleDelta > giver._tileAtmosInfo.MoleDelta) - { - // We don't have enough gas! - tile2._tileAtmosInfo.CurrentTransferAmount -= giver._tileAtmosInfo.MoleDelta; - tile2._tileAtmosInfo.MoleDelta += giver._tileAtmosInfo.MoleDelta; - giver._tileAtmosInfo.MoleDelta = 0; - } - else - { - // We have enough gas. - tile2._tileAtmosInfo.CurrentTransferAmount += tile2._tileAtmosInfo.MoleDelta; - giver._tileAtmosInfo.MoleDelta += tile2._tileAtmosInfo.MoleDelta; - tile2._tileAtmosInfo.MoleDelta = 0; - } - } - } - } - - // Putting this loop here helps make it O(n^2) over O(n^3) - for (var i = queueLength - 1; i >= 0; i--) - { - var tile = queue[i]; - if (tile._tileAtmosInfo.CurrentTransferAmount != 0 && tile._tileAtmosInfo.CurrentTransferDirection != AtmosDirection.Invalid) - { - tile.AdjustEqMovement(tile._tileAtmosInfo.CurrentTransferDirection, tile._tileAtmosInfo.CurrentTransferAmount); - tile._adjacentTiles[tile._tileAtmosInfo.CurrentTransferDirection.ToIndex()] - ._tileAtmosInfo.CurrentTransferAmount += tile._tileAtmosInfo.CurrentTransferAmount; - tile._tileAtmosInfo.CurrentTransferAmount = 0; - } - } - } - - ArrayPool.Shared.Return(queue); - } - else - { - var queue = ArrayPool.Shared.Rent(tileCount); - for (var j = 0; j < takerTilesLength; j++) - { - var taker = takerTiles[j]; - taker._tileAtmosInfo.CurrentTransferDirection = AtmosDirection.Invalid; - taker._tileAtmosInfo.CurrentTransferAmount = 0; - var queueCycleSlow = ++_gridAtmosphereComponent.EqualizationQueueCycleControl; - var queueLength = 0; - queue[queueLength++] = taker; - taker._tileAtmosInfo.LastSlowQueueCycle = queueCycleSlow; - for (var i = 0; i < queueLength; i++) - { - if (taker._tileAtmosInfo.MoleDelta >= 0) - break; // We're done here now. Let's not do more work than needed. - - var tile = queue[i]; - for (var k = 0; k < Atmospherics.Directions; k++) - { - var direction = (AtmosDirection) (1 << k); - if (!tile._adjacentBits.IsFlagSet(direction)) continue; - var tile2 = tile._adjacentTiles[k]; - - if (taker._tileAtmosInfo.MoleDelta >= 0) break; // We're done here now. Let's not do more work than needed. - if (tile2 == null || tile2._tileAtmosInfo.LastQueueCycle != queueCycle) continue; - if (tile2._tileAtmosInfo.LastSlowQueueCycle == queueCycleSlow) continue; - queue[queueLength++] = tile2; - tile2._tileAtmosInfo.LastSlowQueueCycle = queueCycleSlow; - tile2._tileAtmosInfo.CurrentTransferDirection = direction.GetOpposite(); - tile2._tileAtmosInfo.CurrentTransferAmount = 0; - - if (tile2._tileAtmosInfo.MoleDelta > 0) - { - // This tile has gas we can suck, so let's - if (tile2._tileAtmosInfo.MoleDelta > -taker._tileAtmosInfo.MoleDelta) - { - // They have enough gas - tile2._tileAtmosInfo.CurrentTransferAmount -= taker._tileAtmosInfo.MoleDelta; - tile2._tileAtmosInfo.MoleDelta += taker._tileAtmosInfo.MoleDelta; - taker._tileAtmosInfo.MoleDelta = 0; - } - else - { - // They don't have enough gas! - tile2._tileAtmosInfo.CurrentTransferAmount += tile2._tileAtmosInfo.MoleDelta; - taker._tileAtmosInfo.MoleDelta += tile2._tileAtmosInfo.MoleDelta; - tile2._tileAtmosInfo.MoleDelta = 0; - } - } - } - } - - for (var i = queueLength - 1; i >= 0; i--) - { - var tile = queue[i]; - if (tile._tileAtmosInfo.CurrentTransferAmount == 0 || tile._tileAtmosInfo.CurrentTransferDirection == AtmosDirection.Invalid) - continue; - - tile.AdjustEqMovement(tile._tileAtmosInfo.CurrentTransferDirection, tile._tileAtmosInfo.CurrentTransferAmount); - - tile._adjacentTiles[tile._tileAtmosInfo.CurrentTransferDirection.ToIndex()] - ._tileAtmosInfo.CurrentTransferAmount += tile._tileAtmosInfo.CurrentTransferAmount; - tile._tileAtmosInfo.CurrentTransferAmount = 0; - } - } - - ArrayPool.Shared.Return(queue); - } - - for (var i = 0; i < tileCount; i++) - { - var tile = tiles[i]; - tile.FinalizeEq(atmosphereSystem); - } - - for (var i = 0; i < tileCount; i++) - { - var tile = tiles[i]; - for (var j = 0; j < Atmospherics.Directions; j++) - { - var direction = (AtmosDirection) (1 << j); - if (!tile._adjacentBits.IsFlagSet(direction)) continue; - var tile2 = tile._adjacentTiles[j]; - if (tile2?.Air?.Compare(Air) == GasMixture.GasCompareResult.NoExchange) continue; - _gridAtmosphereComponent.AddActiveTile(tile2); - break; - } - } - - ArrayPool.Shared.Return(tiles); - ArrayPool.Shared.Return(giverTiles); - ArrayPool.Shared.Return(takerTiles); - } - - private void FinalizeEq(AtmosphereSystem atmosphereSystem) - { - Span transferDirections = stackalloc float[Atmospherics.Directions]; - var hasTransferDirs = false; - for (var i = 0; i < Atmospherics.Directions; i++) - { - var amount = _tileAtmosInfo[i]; - if (amount == 0) continue; - transferDirections[i] = amount; - _tileAtmosInfo[i] = 0; // Set them to 0 to prevent infinite recursion. - hasTransferDirs = true; - } - - if (!hasTransferDirs) return; - - for(var i = 0; i < Atmospherics.Directions; i++) - { - var direction = (AtmosDirection) (1 << i); - if (!_adjacentBits.IsFlagSet(direction)) continue; - var amount = transferDirections[i]; - var tile = _adjacentTiles[i]; - if (tile?.Air == null) continue; - if (amount > 0) - { - if (Air.TotalMoles < amount) - FinalizeEqNeighbors(atmosphereSystem, transferDirections); - - tile._tileAtmosInfo[direction.GetOpposite()] = 0; - atmosphereSystem.Merge(tile.Air, Air.Remove(amount)); - UpdateVisuals(); - tile.UpdateVisuals(); - ConsiderPressureDifference(tile, amount); - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void FinalizeEqNeighbors(AtmosphereSystem atmosphereSystem, ReadOnlySpan transferDirs) - { - for (var i = 0; i < Atmospherics.Directions; i++) - { - var direction = (AtmosDirection) (1 << i); - var amount = transferDirs[i]; - if(amount < 0 && _adjacentBits.IsFlagSet(direction)) - _adjacentTiles[i].FinalizeEq(atmosphereSystem); // A bit of recursion if needed. - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ConsiderPressureDifference(TileAtmosphere other, float difference) - { - _gridAtmosphereComponent.AddHighPressureDelta(this); - if (difference > PressureDifference) - { - PressureDifference = difference; - _pressureDirection = ((Vector2i)(GridIndices - other.GridIndices)).GetDir().ToAtmosDirection(); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AdjustEqMovement(AtmosDirection direction, float amount) - { - _tileAtmosInfo[direction] += amount; - _adjacentTiles[direction.ToIndex()]._tileAtmosInfo[direction.GetOpposite()] -= amount; - } - - public void ProcessCell(AtmosphereSystem atmosphereSystem, int fireCount, bool spaceWind = true) - { - // Can't process a tile without air - if (Air == null) - { - _gridAtmosphereComponent.RemoveActiveTile(this); - return; - } - - if (_archivedCycle < fireCount) - Archive(fireCount); - - _currentCycle = fireCount; - var adjacentTileLength = 0; - - for (var i = 0; i < Atmospherics.Directions; i++) - { - var direction = (AtmosDirection) (1 << i); - if(_adjacentBits.IsFlagSet(direction)) - adjacentTileLength++; - } - - for(var i = 0; i < Atmospherics.Directions; i++) - { - var direction = (AtmosDirection) (1 << i); - if (!_adjacentBits.IsFlagSet(direction)) continue; - var enemyTile = _adjacentTiles[i]; - - // If the tile is null or has no air, we don't do anything for it. - if(enemyTile?.Air == null) continue; - if (fireCount <= enemyTile._currentCycle) continue; - enemyTile.Archive(fireCount); - - var shouldShareAir = false; - - if (ExcitedGroup != null && enemyTile.ExcitedGroup != null) - { - if (ExcitedGroup != enemyTile.ExcitedGroup) - { - ExcitedGroup.MergeGroups(enemyTile.ExcitedGroup); - } - - shouldShareAir = true; - } else if (Air.Compare(enemyTile.Air) != GasMixture.GasCompareResult.NoExchange) - { - if (!enemyTile.Excited) - { - _gridAtmosphereComponent.AddActiveTile(enemyTile); - } - - var excitedGroup = ExcitedGroup; - excitedGroup ??= enemyTile.ExcitedGroup; - - if (excitedGroup == null) - { - excitedGroup = new ExcitedGroup(); - excitedGroup.Initialize(_gridAtmosphereComponent); - } - - if (ExcitedGroup == null) - excitedGroup.AddTile(this); - - if(enemyTile.ExcitedGroup == null) - excitedGroup.AddTile(enemyTile); - - shouldShareAir = true; - } - - if (shouldShareAir) - { - var difference = atmosphereSystem.Share(Air, enemyTile.Air, adjacentTileLength); - - if (spaceWind) - { - if (difference > 0) - { - ConsiderPressureDifference(enemyTile, difference); - } - else - { - enemyTile.ConsiderPressureDifference(this, -difference); - } - } - - LastShareCheck(); - } - } - - if(Air != null) - _gridAtmosphereComponent.AtmosphereSystem.React(Air, this); - UpdateVisuals(); - - var remove = true; - - if(Air.Temperature > Atmospherics.MinimumTemperatureStartSuperConduction) - if (ConsiderSuperconductivity(true)) - remove = false; - - if(ExcitedGroup == null && remove) - _gridAtmosphereComponent.RemoveActiveTile(this); - } - - public void ProcessHotspot() - { - if (!Hotspot.Valid) - { - _gridAtmosphereComponent.RemoveHotspotTile(this); - return; - } - - if (!Excited) - { - _gridAtmosphereComponent.AddActiveTile(this); - } - - if (!Hotspot.SkippedFirstProcess) - { - Hotspot.SkippedFirstProcess = true; - return; - } - - ExcitedGroup?.ResetCooldowns(); - - if ((Hotspot.Temperature < Atmospherics.FireMinimumTemperatureToExist) || (Hotspot.Volume <= 1f) - || Air == null || Air.GetMoles(Gas.Oxygen) < 0.5f || (Air.GetMoles(Gas.Plasma) < 0.5f && Air.GetMoles(Gas.Tritium) < 0.5f)) - { - Hotspot = new Hotspot(); - UpdateVisuals(); - return; - } - - PerformHotspotExposure(); - - if (Hotspot.Bypassing) - { - Hotspot.State = 3; - _gridAtmosphereComponent.BurnTile(GridIndices); - - if (Air.Temperature > Atmospherics.FireMinimumTemperatureToSpread) - { - var radiatedTemperature = Air.Temperature * Atmospherics.FireSpreadRadiosityScale; - foreach (var tile in _adjacentTiles) - { - if(!tile.Hotspot.Valid) - tile.HotspotExpose(radiatedTemperature, Atmospherics.CellVolume/4); - } - } - } - else - { - Hotspot.State = (byte) (Hotspot.Volume > Atmospherics.CellVolume * 0.4f ? 2 : 1); - } - - if (Hotspot.Temperature > MaxFireTemperatureSustained) - MaxFireTemperatureSustained = Hotspot.Temperature; - - // TODO ATMOS Maybe destroy location here? - } - - public float MaxFireTemperatureSustained { get; private set; } - - private void PerformHotspotExposure() - { - if (Air == null || !Hotspot.Valid) return; - - Hotspot.Bypassing = Hotspot.SkippedFirstProcess && Hotspot.Volume > Air.Volume*0.95f; - - if (Hotspot.Bypassing) - { - Hotspot.Volume = Air.ReactionResults[GasReaction.Fire] * Atmospherics.FireGrowthRate; - Hotspot.Temperature = Air.Temperature; - } - else - { - var affected = Air.RemoveRatio(Hotspot.Volume / Air.Volume); - affected.Temperature = Hotspot.Temperature; - _gridAtmosphereComponent.AtmosphereSystem.React(affected, this); - Hotspot.Temperature = affected.Temperature; - Hotspot.Volume = affected.ReactionResults[GasReaction.Fire] * Atmospherics.FireGrowthRate; - AssumeAir(affected); - } - - var tileRef = GridIndices.GetTileRef(GridIndex); - - foreach (var entity in tileRef.GetEntitiesInTileFast(_gridTileLookupSystem)) - { - foreach (var fireAct in entity.GetAllComponents()) - { - - fireAct.FireAct(Hotspot.Temperature, Hotspot.Volume); - } - } - } - - public void HotspotExpose(float exposedTemperature, float exposedVolume, bool soh = false) - { - if (Air == null) - return; - - var oxygen = Air.GetMoles(Gas.Oxygen); - - if (oxygen < 0.5f) - return; - - var plasma = Air.GetMoles(Gas.Plasma); - var tritium = Air.GetMoles(Gas.Tritium); - - if (Hotspot.Valid) - { - if (soh) - { - if (plasma > 0.5f || tritium > 0.5f) - { - if (Hotspot.Temperature < exposedTemperature) - Hotspot.Temperature = exposedTemperature; - if (Hotspot.Volume < exposedVolume) - Hotspot.Volume = exposedVolume; - } - } - - return; - } - - if ((exposedTemperature > Atmospherics.PlasmaMinimumBurnTemperature) && (plasma > 0.5f || tritium > 0.5f)) - { - Hotspot = new Hotspot - { - Volume = exposedVolume * 25f, - Temperature = exposedTemperature, - SkippedFirstProcess = _currentCycle > _gridAtmosphereComponent.UpdateCounter - }; - - Hotspot.Start(); - - _gridAtmosphereComponent.AddActiveTile(this); - _gridAtmosphereComponent.AddHotspotTile(this); - } - } - - private bool ConsiderSuperconductivity() - { - if (ThermalConductivity == 0f) - return false; - - _gridAtmosphereComponent.AddSuperconductivityTile(this); - return true; - } - - private bool ConsiderSuperconductivity(bool starting) - { - if (Air.Temperature < (starting - ? Atmospherics.MinimumTemperatureStartSuperConduction - : Atmospherics.MinimumTemperatureForSuperconduction)) - return false; - - return !(_gridAtmosphereComponent.AtmosphereSystem.GetHeatCapacity(Air) < Atmospherics.MCellWithRatio) && ConsiderSuperconductivity(); - } - - public void Superconduct(AtmosphereSystem atmosphereSystem) - { - var directions = ConductivityDirections(); - - for(var i = 0; i < Atmospherics.Directions; i++) - { - var direction = (AtmosDirection) (1 << i); - if (!directions.IsFlagSet(direction)) continue; - - var adjacent = _adjacentTiles[direction.ToIndex()]; - - // TODO ATMOS handle adjacent being null. - if (adjacent == null || adjacent.ThermalConductivity == 0f) - continue; - - if(adjacent._archivedCycle < _gridAtmosphereComponent.UpdateCounter) - adjacent.Archive(_gridAtmosphereComponent.UpdateCounter); - - adjacent.NeighborConductWithSource(atmosphereSystem, this); - - adjacent.ConsiderSuperconductivity(); - } - - RadiateToSpace(); - - FinishSuperconduction(atmosphereSystem); - } - - private void FinishSuperconduction(AtmosphereSystem atmosphereSystem) - { - // Conduct with air on my tile if I have it - if (!BlocksAllAir) - { - Temperature = atmosphereSystem.TemperatureShare(Air, ThermalConductivity, Temperature, HeatCapacity); - } - - FinishSuperconduction(BlocksAllAir ? Temperature : Air.Temperature); - } - - private void FinishSuperconduction(float temperature) - { - // Make sure it's still hot enough to continue conducting. - if (temperature < Atmospherics.MinimumTemperatureForSuperconduction) - { - _gridAtmosphereComponent.RemoveSuperconductivityTile(this); - } - } - - private void NeighborConductWithSource(AtmosphereSystem atmosphereSystem, TileAtmosphere other) - { - if (BlocksAllAir) - { - if (!other.BlocksAllAir) - { - other.TemperatureShareOpenToSolid(atmosphereSystem, this); - } - else - { - other.TemperatureShareMutualSolid(this, ThermalConductivity); - } - - TemperatureExpose(null, Temperature, _gridAtmosphereComponent.GetVolumeForCells(1)); - return; - } - - if (!other.BlocksAllAir) - { - atmosphereSystem.TemperatureShare(other.Air, Air, Atmospherics.WindowHeatTransferCoefficient); - } - else - { - TemperatureShareOpenToSolid(atmosphereSystem, other); - } - - _gridAtmosphereComponent.AddActiveTile(this); - } - - private void TemperatureShareOpenToSolid(AtmosphereSystem atmosphereSystem, TileAtmosphere other) - { - other.Temperature = atmosphereSystem.TemperatureShare(Air, other.ThermalConductivity, other.Temperature, other.HeatCapacity); - } - - private void TemperatureShareMutualSolid(TileAtmosphere other, float conductionCoefficient) - { - var deltaTemperature = (_temperatureArchived - other._temperatureArchived); - if (MathF.Abs(deltaTemperature) > Atmospherics.MinimumTemperatureDeltaToConsider - && HeatCapacity != 0f && other.HeatCapacity != 0f) - { - var heat = conductionCoefficient * deltaTemperature * - (HeatCapacity * other.HeatCapacity / (HeatCapacity + other.HeatCapacity)); - - Temperature -= heat / HeatCapacity; - other.Temperature += heat / other.HeatCapacity; - } - } - - public void RadiateToSpace() - { - // Considering 0ºC as the break even point for radiation in and out. - if (Temperature > Atmospherics.T0C) - { - // Hardcoded space temperature. - var deltaTemperature = (_temperatureArchived - Atmospherics.TCMB); - if ((HeatCapacity > 0) && (MathF.Abs(deltaTemperature) > Atmospherics.MinimumTemperatureDeltaToConsider)) - { - var heat = ThermalConductivity * deltaTemperature * (HeatCapacity * - Atmospherics.HeatCapacityVacuum / (HeatCapacity + Atmospherics.HeatCapacityVacuum)); - - Temperature -= heat; - } - } - } - - public AtmosDirection ConductivityDirections() - { - if(BlocksAllAir) - { - if(_archivedCycle < _gridAtmosphereComponent.UpdateCounter) - Archive(_gridAtmosphereComponent.UpdateCounter); - return AtmosDirection.All; - } - - // TODO ATMOS check if this is correct - return AtmosDirection.All; - } - - public void ExplosivelyDepressurize(int cycleNum) - { - if (Air == null) return; - - const int limit = Atmospherics.ZumosHardTileLimit; - - var totalGasesRemoved = 0f; - var queueCycle = ++_gridAtmosphereComponent.EqualizationQueueCycleControl; - var tiles = ArrayPool.Shared.Rent(limit); - var spaceTiles = ArrayPool.Shared.Rent(limit); - - var tileCount = 0; - var spaceTileCount = 0; - - tiles[tileCount++] = this; - - _tileAtmosInfo = new TileAtmosInfo {LastQueueCycle = queueCycle}; - - for (var i = 0; i < tileCount; i++) - { - var tile = tiles[i]; - tile._tileAtmosInfo.LastCycle = cycleNum; - tile._tileAtmosInfo.CurrentTransferDirection = AtmosDirection.Invalid; - if (tile.Air.Immutable) - { - spaceTiles[spaceTileCount++] = tile; - tile.PressureSpecificTarget = tile; - } - else - { - for (var j = 0; j < Atmospherics.Directions; j++) - { - var direction = (AtmosDirection) (1 << j); - if (!tile._adjacentBits.IsFlagSet(direction)) continue; - var tile2 = tile._adjacentTiles[j]; - if (tile2.Air == null) continue; - if (tile2._tileAtmosInfo.LastQueueCycle == queueCycle) continue; - - tile.ConsiderFirelocks(tile2); - - // The firelocks might have closed on us. - if (!tile._adjacentBits.IsFlagSet(direction)) continue; - tile2._tileAtmosInfo = new TileAtmosInfo {LastQueueCycle = queueCycle}; - tiles[tileCount++] = tile2; - } - } - - if (tileCount >= limit || spaceTileCount >= limit) - break; - } - - var queueCycleSlow = ++_gridAtmosphereComponent.EqualizationQueueCycleControl; - var progressionOrder = ArrayPool.Shared.Rent(limit * 2); - var progressionCount = 0; - - for (var i = 0; i < spaceTileCount; i++) - { - var tile = spaceTiles[i]; - progressionOrder[progressionCount++] = tile; - tile._tileAtmosInfo.LastSlowQueueCycle = queueCycleSlow; - tile._tileAtmosInfo.CurrentTransferDirection = AtmosDirection.Invalid; - } - - for (var i = 0; i < progressionCount; i++) - { - var tile = progressionOrder[i]; - for (var j = 0; j < Atmospherics.Directions; j++) - { - var direction = (AtmosDirection) (1 << j); - // TODO ATMOS This is a terrible hack that accounts for the mess that are space TileAtmospheres. - if (!tile._adjacentBits.IsFlagSet(direction) && !tile.Air.Immutable) continue; - var tile2 = tile._adjacentTiles[j]; - if (tile2?._tileAtmosInfo.LastQueueCycle != queueCycle) continue; - if (tile2._tileAtmosInfo.LastSlowQueueCycle == queueCycleSlow) continue; - if(tile2.Air?.Immutable ?? false) continue; - tile2._tileAtmosInfo.CurrentTransferDirection = direction.GetOpposite(); - tile2._tileAtmosInfo.CurrentTransferAmount = 0; - tile2.PressureSpecificTarget = tile.PressureSpecificTarget; - tile2._tileAtmosInfo.LastSlowQueueCycle = queueCycleSlow; - progressionOrder[progressionCount++] = tile2; - } - } - - for (var i = progressionCount - 1; i >= 0; i--) - { - var tile = progressionOrder[i]; - if (tile._tileAtmosInfo.CurrentTransferDirection == AtmosDirection.Invalid) continue; - _gridAtmosphereComponent.AddHighPressureDelta(tile); - _gridAtmosphereComponent.AddActiveTile(tile); - var tile2 = tile._adjacentTiles[tile._tileAtmosInfo.CurrentTransferDirection.ToIndex()]; - if (tile2?.Air == null) continue; - var sum = tile2.Air.TotalMoles; - totalGasesRemoved += sum; - tile._tileAtmosInfo.CurrentTransferAmount += sum; - tile2._tileAtmosInfo.CurrentTransferAmount += tile._tileAtmosInfo.CurrentTransferAmount; - tile.PressureDifference = tile._tileAtmosInfo.CurrentTransferAmount; - tile._pressureDirection = tile._tileAtmosInfo.CurrentTransferDirection; - - if (tile2._tileAtmosInfo.CurrentTransferDirection == AtmosDirection.Invalid) - { - tile2.PressureDifference = tile2._tileAtmosInfo.CurrentTransferAmount; - tile2._pressureDirection = tile._tileAtmosInfo.CurrentTransferDirection; - } - - tile.Air.Clear(); - tile.UpdateVisuals(); - tile.HandleDecompressionFloorRip(sum); - } - - ArrayPool.Shared.Return(tiles); - ArrayPool.Shared.Return(spaceTiles); - ArrayPool.Shared.Return(progressionOrder); - } - - private void HandleDecompressionFloorRip(float sum) - { - var chance = MathHelper.Clamp(sum / 500, 0.005f, 0.5f); - if (sum > 20 && _robustRandom.Prob(chance)) - _gridAtmosphereComponent.PryTile(GridIndices); - } - - private void ConsiderFirelocks(TileAtmosphere other) - { - var reconsiderAdjacent = false; - - foreach (var entity in GridIndices.GetEntitiesInTileFast(GridIndex, _gridAtmosphereComponent.GridTileLookupSystem)) - { - if (!entity.TryGetComponent(out FirelockComponent firelock)) continue; - reconsiderAdjacent |= firelock.EmergencyPressureStop(); - } - - foreach (var entity in other.GridIndices.GetEntitiesInTileFast(other.GridIndex, _gridAtmosphereComponent.GridTileLookupSystem)) - { - if (!entity.TryGetComponent(out FirelockComponent firelock)) continue; - reconsiderAdjacent |= firelock.EmergencyPressureStop(); - } - - if (reconsiderAdjacent) - { - UpdateAdjacent(); - other.UpdateAdjacent(); - } - } - - public bool AssumeAir(GasMixture giver) - { - if (Air == null) return false; - - EntitySystem.Get().Merge(Air, giver); - - UpdateVisuals(); - - if (!Excited) - { - _gridAtmosphereComponent.AddActiveTile(this); - } - return true; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void UpdateVisuals() { @@ -1155,53 +115,28 @@ namespace Content.Server.Atmos var isSpace = _gridAtmosphereComponent.IsSpace(GridIndices); var adjacent = _gridAtmosphereComponent.GetTile(otherIndices, !isSpace); - _adjacentTiles[direction.ToIndex()] = adjacent; + AdjacentTiles[direction.ToIndex()] = adjacent; adjacent?.UpdateAdjacent(direction.GetOpposite()); if (adjacent != null && !BlockedAirflow.IsFlagSet(direction) && !_gridAtmosphereComponent.IsAirBlocked(adjacent.GridIndices, direction.GetOpposite())) { - _adjacentBits |= direction; + AdjacentBits |= direction; } } } - public void UpdateAdjacent(AtmosDirection direction) + private void UpdateAdjacent(AtmosDirection direction) { - _adjacentTiles[direction.ToIndex()] = _gridAtmosphereComponent.GetTile(GridIndices.Offset(direction.ToDirection())); + AdjacentTiles[direction.ToIndex()] = _gridAtmosphereComponent.GetTile(GridIndices.Offset(direction.ToDirection())); if (!BlockedAirflow.IsFlagSet(direction) && !_gridAtmosphereComponent.IsAirBlocked(GridIndices.Offset(direction.ToDirection()), direction.GetOpposite())) { - _adjacentBits |= direction; + AdjacentBits |= direction; } else { - _adjacentBits &= ~direction; + AdjacentBits &= ~direction; } } - - /// - /// Calls on this tile atmosphere's position. - /// - public void Invalidate() - { - _gridAtmosphereComponent.Invalidate(GridIndices); - } - - private void LastShareCheck() - { - var lastShare = Air.LastShare; - if (lastShare > Atmospherics.MinimumAirToSuspend) - { - ExcitedGroup.ResetCooldowns(); - } else if (lastShare > Atmospherics.MinimumMolesDeltaToMove) - { - ExcitedGroup.DismantleCooldown = 0; - } - } - - public void TemperatureExpose(GasMixture air, float temperature, float volume) - { - // TODO ATMOS do this - } } } diff --git a/Content.Shared/Atmos/Atmospherics.cs b/Content.Shared/Atmos/Atmospherics.cs index 2bb88f02a4..b2736ebf68 100644 --- a/Content.Shared/Atmos/Atmospherics.cs +++ b/Content.Shared/Atmos/Atmospherics.cs @@ -141,14 +141,14 @@ namespace Content.Shared.Atmos #endregion /// - /// Hard limit for tile equalization. + /// Hard limit for zone-based tile equalization. /// - public const int ZumosHardTileLimit = 2000; + public const int MonstermosHardTileLimit = 2000; /// /// Limit for zone-based tile equalization. /// - public const int ZumosTileLimit = 200; + public const int MonstermosTileLimit = 200; /// /// Total number of gases. Increase this if you want to add more! diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 6ba43c71e8..d1fc2f7856 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -243,12 +243,25 @@ namespace Content.Shared.CCVar public static readonly CVarDef SpaceWind = CVarDef.Create("atmos.space_wind", true, CVar.SERVERONLY); + /// + /// The sound that plays when space wind occurs. + /// + public static readonly CVarDef SpaceWindSound = + CVarDef.Create("atmos.space_wind_sound", "/Audio/Effects/space_wind.ogg", CVar.SERVERONLY); + /// /// Whether monstermos tile equalization is enabled. /// public static readonly CVarDef MonstermosEqualization = CVarDef.Create("atmos.monstermos_equalization", true, CVar.SERVERONLY); + /// + /// Whether monstermos explosive depressurization is enabled. + /// Needs to be enabled to work. + /// + public static readonly CVarDef MonstermosDepressurization = + CVarDef.Create("atmos.monstermos_depressurization", true, CVar.SERVERONLY); + /// /// Whether atmos superconduction is enabled. ///