diff --git a/Content.Client/Atmos/GasTileOverlay.cs b/Content.Client/Atmos/GasTileOverlay.cs new file mode 100644 index 0000000000..ec0cb330fa --- /dev/null +++ b/Content.Client/Atmos/GasTileOverlay.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Content.Client.GameObjects.EntitySystems; +using Content.Client.Utility; +using JetBrains.Annotations; +using Robust.Client.Graphics; +using Robust.Client.Graphics.ClientEye; +using Robust.Client.Graphics.Drawing; +using Robust.Client.Graphics.Overlays; +using Robust.Client.Interfaces.Graphics; +using Robust.Client.Interfaces.Graphics.ClientEye; +using Robust.Client.Interfaces.ResourceManagement; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.Map; +using Robust.Shared.Interfaces.Resources; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Client.Atmos +{ + public class GasTileOverlay : Overlay + { + private readonly GasTileOverlaySystem _gasTileOverlaySystem; + + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IEyeManager _eyeManager = default!; + [Dependency] private readonly IClyde _clyde = default!; + + public override OverlaySpace Space => OverlaySpace.WorldSpace; + + public GasTileOverlay() : base(nameof(GasTileOverlay)) + { + IoCManager.InjectDependencies(this); + + _gasTileOverlaySystem = EntitySystem.Get(); + } + + protected override void Draw(DrawingHandleBase handle, OverlaySpace overlay) + { + var drawHandle = (DrawingHandleWorld) handle; + + var mapId = _eyeManager.CurrentMap; + var eye = _eyeManager.CurrentEye; + + var worldBounds = Box2.CenteredAround(eye.Position.Position, + _clyde.ScreenSize / (float) EyeManager.PixelsPerMeter * eye.Zoom); + + foreach (var mapGrid in _mapManager.FindGridsIntersecting(mapId, worldBounds)) + { + foreach (var tile in mapGrid.GetTilesIntersecting(worldBounds)) + { + foreach (var (texture, color) in _gasTileOverlaySystem.GetOverlays(mapGrid.Index, tile.GridIndices)) + { + drawHandle.DrawTexture(texture, mapGrid.LocalToWorld(new Vector2(tile.X, tile.Y)), color); + } + } + } + } + } +} diff --git a/Content.Client/Construction/ConstructionPlacementHijack.cs b/Content.Client/Construction/ConstructionPlacementHijack.cs index 57f5503dbc..bd4431f751 100644 --- a/Content.Client/Construction/ConstructionPlacementHijack.cs +++ b/Content.Client/Construction/ConstructionPlacementHijack.cs @@ -20,12 +20,12 @@ namespace Content.Client.Construction } /// - public override bool HijackPlacementRequest(GridCoordinates coords) + public override bool HijackPlacementRequest(GridCoordinates coordinates) { if (_prototype != null) { var dir = Manager.Direction; - _constructionSystem.SpawnGhost(_prototype, coords, dir); + _constructionSystem.SpawnGhost(_prototype, coordinates, dir); } return true; } diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs index af240a8cd5..837e79a79d 100644 --- a/Content.Client/EntryPoint.cs +++ b/Content.Client/EntryPoint.cs @@ -73,6 +73,7 @@ namespace Content.Client prototypes.RegisterIgnore("material"); prototypes.RegisterIgnore("reaction"); //Chemical reactions only needed by server. Reactions checks are server-side. + prototypes.RegisterIgnore("gasReaction"); prototypes.RegisterIgnore("barSign"); ClientContentIoC.Register(); diff --git a/Content.Client/GameObjects/Components/Atmos/CanSeeGasesComponent.cs b/Content.Client/GameObjects/Components/Atmos/CanSeeGasesComponent.cs new file mode 100644 index 0000000000..d95748f72b --- /dev/null +++ b/Content.Client/GameObjects/Components/Atmos/CanSeeGasesComponent.cs @@ -0,0 +1,35 @@ +using Content.Client.Atmos; +using Robust.Client.GameObjects; +using Robust.Client.Interfaces.Graphics.Overlays; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.IoC; + +namespace Content.Client.GameObjects.Components.Atmos +{ + [RegisterComponent] + public class CanSeeGasesComponent : Component + { + [Dependency] private readonly IOverlayManager _overlayManager = default!; + + public override string Name => "CanSeeGases"; + + public override void HandleMessage(ComponentMessage message, IComponent component) + { + base.HandleMessage(message, component); + + switch (message) + { + case PlayerAttachedMsg _: + if(!_overlayManager.HasOverlay(nameof(GasTileOverlay))) + _overlayManager.AddOverlay(new GasTileOverlay()); + break; + + case PlayerDetachedMsg _: + if(!_overlayManager.HasOverlay(nameof(GasTileOverlay))) + _overlayManager.RemoveOverlay(nameof(GasTileOverlay)); + break; + } + } + } +} diff --git a/Content.Client/GameObjects/EntitySystems/GasTileOverlaySystem.cs b/Content.Client/GameObjects/EntitySystems/GasTileOverlaySystem.cs new file mode 100644 index 0000000000..1e2f0220af --- /dev/null +++ b/Content.Client/GameObjects/EntitySystems/GasTileOverlaySystem.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Content.Client.Atmos; +using Content.Client.Utility; +using Content.Shared.Atmos; +using Content.Shared.GameObjects.EntitySystems; +using JetBrains.Annotations; +using Robust.Client.Graphics; +using Robust.Client.Interfaces.Graphics.Overlays; +using Robust.Client.Interfaces.ResourceManagement; +using Robust.Client.ResourceManagement; +using Robust.Client.Utility; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Client.GameObjects.EntitySystems +{ + [UsedImplicitly] + public sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem + { + [Dependency] private readonly IResourceCache _resourceCache = default!; + + private readonly Dictionary _fireCache = new Dictionary(); + + // Gas overlays + private readonly float[] _timer = new float[Atmospherics.TotalNumberOfGases]; + private readonly float[][] _frameDelays = new float[Atmospherics.TotalNumberOfGases][]; + private readonly int[] _frameCounter = new int[Atmospherics.TotalNumberOfGases]; + private readonly Texture[][] _frames = new Texture[Atmospherics.TotalNumberOfGases][]; + + // Fire overlays + private const int FireStates = 3; + private const string FireRsiPath = "/Textures/Effects/fire.rsi"; + + private readonly float[] _fireTimer = new float[FireStates]; + private readonly float[][] _fireFrameDelays = new float[FireStates][]; + private readonly int[] _fireFrameCounter = new int[FireStates]; + private readonly Texture[][] _fireFrames = new Texture[FireStates][]; + + private Dictionary> _overlay = new Dictionary>(); + + public override void Initialize() + { + base.Initialize(); + + SubscribeNetworkEvent(new EntityEventHandler(OnTileOverlayMessage)); + + for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++) + { + var gas = Atmospherics.GetGas(i); + switch (gas.GasOverlay) + { + case SpriteSpecifier.Rsi animated: + var rsi = _resourceCache.GetResource(animated.RsiPath).RSI; + var stateId = animated.RsiState; + + if(!rsi.TryGetState(stateId, out var state)) continue; + + _frames[i] = state.GetFrames(RSI.State.Direction.South); + _frameDelays[i] = state.GetDelays(); + _frameCounter[i] = 0; + break; + case SpriteSpecifier.Texture texture: + _frames[i] = new[] {texture.Frame0()}; + _frameDelays[i] = Array.Empty(); + break; + case null: + _frames[i] = Array.Empty(); + _frameDelays[i] = Array.Empty(); + break; + } + } + + var fire = _resourceCache.GetResource(FireRsiPath).RSI; + + for (var i = 0; i < FireStates; i++) + { + if (!fire.TryGetState((i+1).ToString(), out var state)) + throw new ArgumentOutOfRangeException($"Fire RSI doesn't have state \"{i}\"!"); + + _fireFrames[i] = state.GetFrames(RSI.State.Direction.South); + _fireFrameDelays[i] = state.GetDelays(); + _fireFrameCounter[i] = 0; + } + } + + public (Texture, Color color)[] GetOverlays(GridId gridIndex, MapIndices indices) + { + if (!_overlay.TryGetValue(gridIndex, out var tiles) || !tiles.TryGetValue(indices, out var overlays)) + return Array.Empty<(Texture, Color)>(); + + var fire = overlays.FireState != 0; + var length = overlays.Gas.Length + (fire ? 1 : 0); + + var list = new (Texture, Color)[length]; + + for (var i = 0; i < overlays.Gas.Length; i++) + { + var gasData = overlays.Gas[i]; + var frames = _frames[gasData.Index]; + list[i] = (frames[_frameCounter[gasData.Index]], Color.White.WithAlpha(gasData.Opacity)); + } + + if (fire) + { + var state = overlays.FireState - 1; + var frames = _fireFrames[state]; + // TODO ATMOS Set color depending on temperature + list[length - 1] = (frames[_fireFrameCounter[state]], Color.White); + } + + return list; + } + + private void OnTileOverlayMessage(GasTileOverlayMessage ev) + { + if(ev.ClearAllOtherOverlays) + _overlay.Clear(); + + foreach (var data in ev.OverlayData) + { + if (!_overlay.TryGetValue(data.GridIndex, out var gridOverlays)) + { + gridOverlays = new Dictionary(); + _overlay.Add(data.GridIndex, gridOverlays); + } + + gridOverlays[data.GridIndices] = data.Data; + } + } + + public override void FrameUpdate(float frameTime) + { + base.FrameUpdate(frameTime); + + for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++) + { + var delays = _frameDelays[i]; + if (delays.Length == 0) continue; + + var frameCount = _frameCounter[i]; + _timer[i] += frameTime; + if (!(_timer[i] >= delays[frameCount])) continue; + _timer[i] = 0f; + _frameCounter[i] = (frameCount + 1) % _frames[i].Length; + } + + for (var i = 0; i < FireStates; i++) + { + var delays = _fireFrameDelays[i]; + if (delays.Length == 0) continue; + + var frameCount = _fireFrameCounter[i]; + _fireTimer[i] += frameTime; + if (!(_fireTimer[i] >= delays[frameCount])) continue; + _fireTimer[i] = 0f; + _fireFrameCounter[i] = (frameCount + 1) % _fireFrames[i].Length; + } + } + } +} diff --git a/Content.Client/IgnoredComponents.cs b/Content.Client/IgnoredComponents.cs index ed9b52f05f..8bf1b6e330 100644 --- a/Content.Client/IgnoredComponents.cs +++ b/Content.Client/IgnoredComponents.cs @@ -149,6 +149,8 @@ "Conveyor", "ConveyorSwitch", "Flippable", + "Airtight", + "MovedByPressure", }; } } diff --git a/Content.Server/Atmos/AtmosCommands.cs b/Content.Server/Atmos/AtmosCommands.cs new file mode 100644 index 0000000000..45f50bbe24 --- /dev/null +++ b/Content.Server/Atmos/AtmosCommands.cs @@ -0,0 +1,317 @@ +#nullable enable +using System; +using Content.Server.GameObjects.Components.Atmos; +using Content.Shared.Atmos; +using Robust.Server.Interfaces.Console; +using Robust.Server.Interfaces.Player; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Map; +using Robust.Shared.IoC; +using Robust.Shared.Map; + +namespace Content.Server.Atmos +{ + public class AddAtmos : IClientCommand + { + public string Command => "addatmos"; + public string Description => "Adds atmos support to a grid."; + public string Help => "addatmos "; + public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args) + { + if (args.Length < 1) return; + if(!int.TryParse(args[0], out var id)) return; + + var gridId = new GridId(id); + + var mapMan = IoCManager.Resolve(); + + if (!gridId.IsValid() || !mapMan.TryGetGrid(gridId, out var gridComp)) + { + shell.SendText(player, "Invalid grid ID."); + return; + } + + var entMan = IoCManager.Resolve(); + + if (!entMan.TryGetEntity(gridComp.GridEntityId, out var grid)) + { + shell.SendText(player, "Failed to get grid entity."); + return; + } + + if (grid.HasComponent()) + { + shell.SendText(player, "Grid already has an atmosphere."); + return; + } + + grid.AddComponent(); + + shell.SendText(player, $"Added atmosphere to grid {id}."); + } + } + + public class ListGases : IClientCommand + { + public string Command => "listgases"; + public string Description => "Prints a list of gases and their indices."; + public string Help => "listgases"; + public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args) + { + foreach (var gasPrototype in Atmospherics.Gases) + { + shell.SendText(player, $"{gasPrototype.Name} ID: {gasPrototype.ID}"); + } + } + } + + public class AddGas : IClientCommand + { + public string Command => "addgas"; + public string Description => "Adds gas at a certain position."; + public string Help => "addgas "; + public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args) + { + var gasId = -1; + var gas = (Gas) (-1); + if (args.Length < 5) return; + if(!int.TryParse(args[0], out var x) + || !int.TryParse(args[1], out var y) + || !int.TryParse(args[2], out var id) + || !(int.TryParse(args[3], out gasId) || Enum.TryParse(args[3], out gas)) + || !float.TryParse(args[4], out var moles)) return; + + var gridId = new GridId(id); + + var mapMan = IoCManager.Resolve(); + + if (!gridId.IsValid() || !mapMan.TryGetGrid(gridId, out var gridComp)) + { + shell.SendText(player, "Invalid grid ID."); + return; + } + + var entMan = IoCManager.Resolve(); + + if (!entMan.TryGetEntity(gridComp.GridEntityId, out var grid)) + { + shell.SendText(player, "Failed to get grid entity."); + return; + } + + if (!grid.HasComponent()) + { + shell.SendText(player, "Grid doesn't have an atmosphere."); + return; + } + + var gam = grid.GetComponent(); + var indices = new MapIndices(x, y); + var tile = gam.GetTile(indices); + + if (tile == null) + { + shell.SendText(player, "Invalid coordinates."); + return; + } + + if (tile.Air == null) + { + shell.SendText(player, "Can't add gas to that tile."); + return; + } + + if (gasId != -1) + { + tile.Air.AdjustMoles(gasId, moles); + gam.Invalidate(indices); + return; + } + + tile.Air.AdjustMoles(gas, moles); + gam.Invalidate(indices); + } + } + + public class FillGas : IClientCommand + { + public string Command => "fillgas"; + public string Description => "Adds gas to all tiles in a grid."; + public string Help => "fillgas "; + public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args) + { + var gasId = -1; + var gas = (Gas) (-1); + if (args.Length < 3) return; + if(!int.TryParse(args[0], out var id) + || !(int.TryParse(args[1], out gasId) || Enum.TryParse(args[1], out gas)) + || !float.TryParse(args[2], out var moles)) return; + + var gridId = new GridId(id); + + var mapMan = IoCManager.Resolve(); + + if (!gridId.IsValid() || !mapMan.TryGetGrid(gridId, out var gridComp)) + { + shell.SendText(player, "Invalid grid ID."); + return; + } + + var entMan = IoCManager.Resolve(); + + if (!entMan.TryGetEntity(gridComp.GridEntityId, out var grid)) + { + shell.SendText(player, "Failed to get grid entity."); + return; + } + + if (!grid.HasComponent()) + { + shell.SendText(player, "Grid doesn't have an atmosphere."); + return; + } + + var gam = grid.GetComponent(); + + foreach (var tile in gam) + { + if (gasId != -1) + { + tile.Air?.AdjustMoles(gasId, moles); + gam.Invalidate(tile.GridIndices); + continue; + } + + tile.Air?.AdjustMoles(gas, moles); + gam.Invalidate(tile.GridIndices); + } + } + } + + public class RemoveGas : IClientCommand + { + public string Command => "removegas"; + public string Description => "Removes an amount of gases."; + public string Help => "removegas \nIf is true, amount will be treated as the ratio of gas to be removed."; + public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args) + { + if (args.Length < 5) return; + if(!int.TryParse(args[0], out var x) + || !int.TryParse(args[1], out var y) + || !int.TryParse(args[2], out var id) + || !float.TryParse(args[3], out var amount) + || !bool.TryParse(args[4], out var ratio)) return; + + var gridId = new GridId(id); + + var mapMan = IoCManager.Resolve(); + + if (!gridId.IsValid() || !mapMan.TryGetGrid(gridId, out var gridComp)) + { + shell.SendText(player, "Invalid grid ID."); + return; + } + + var entMan = IoCManager.Resolve(); + + if (!entMan.TryGetEntity(gridComp.GridEntityId, out var grid)) + { + shell.SendText(player, "Failed to get grid entity."); + return; + } + + if (!grid.HasComponent()) + { + shell.SendText(player, "Grid doesn't have an atmosphere."); + return; + } + + var gam = grid.GetComponent(); + var indices = new MapIndices(x, y); + var tile = gam.GetTile(indices); + + if (tile == null) + { + shell.SendText(player, "Invalid coordinates."); + return; + } + + if (tile.Air == null) + { + shell.SendText(player, "Can't remove gas from that tile."); + return; + } + + if (ratio) + tile.Air.RemoveRatio(amount); + else + tile.Air.Remove(amount); + + gam.Invalidate(indices); + } + } + + public class SetTemperature : IClientCommand + { + public string Command => "settemp"; + public string Description => "Sets a tile's temperature."; + public string Help => "Usage: settemp "; + public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args) + { + if (args.Length < 4) return; + if(!int.TryParse(args[0], out var x) + || !int.TryParse(args[1], out var y) + || !int.TryParse(args[2], out var id) + || !float.TryParse(args[3], out var temperature)) return; + + var gridId = new GridId(id); + + var mapMan = IoCManager.Resolve(); + + if (temperature < Atmospherics.TCMB) + { + shell.SendText(player, "Invalid temperature."); + return; + } + + if (!gridId.IsValid() || !mapMan.TryGetGrid(gridId, out var gridComp)) + { + shell.SendText(player, "Invalid grid ID."); + return; + } + + var entMan = IoCManager.Resolve(); + + if (!entMan.TryGetEntity(gridComp.GridEntityId, out var grid)) + { + shell.SendText(player, "Failed to get grid entity."); + return; + } + + if (!grid.HasComponent()) + { + shell.SendText(player, "Grid doesn't have an atmosphere."); + return; + } + + var gam = grid.GetComponent(); + var indices = new MapIndices(x, y); + var tile = gam.GetTile(indices); + + if (tile == null) + { + shell.SendText(player, "Invalid coordinates."); + return; + } + + if (tile.Air == null) + { + shell.SendText(player, "Can't change that tile's temperature."); + return; + } + + tile.Air.Temperature = temperature; + gam.Invalidate(indices); + } + } +} diff --git a/Content.Server/Atmos/EntityNetworkUtils.cs b/Content.Server/Atmos/EntityNetworkUtils.cs new file mode 100644 index 0000000000..f5d09e947b --- /dev/null +++ b/Content.Server/Atmos/EntityNetworkUtils.cs @@ -0,0 +1,31 @@ +using System; +using Robust.Shared.Map; +using Robust.Shared.Maths; + +namespace Content.Server.Atmos +{ + public static class EntityNetworkUtils + { + public static Vector2i CardinalToIntVec(this Direction dir) + { + switch (dir) + { + case Direction.North: + return new Vector2i(0, 1); + case Direction.East: + return new Vector2i(1, 0); + case Direction.South: + return new Vector2i(0, -1); + case Direction.West: + return new Vector2i(-1, 0); + default: + throw new ArgumentException($"Direction dir {dir} is not a cardinal direction", nameof(dir)); + } + } + + public static MapIndices Offset(this MapIndices pos, Direction dir) + { + return pos + (MapIndices) dir.CardinalToIntVec(); + } + } +} diff --git a/Content.Server/Atmos/ExcitedGroup.cs b/Content.Server/Atmos/ExcitedGroup.cs new file mode 100644 index 0000000000..7935be28b5 --- /dev/null +++ b/Content.Server/Atmos/ExcitedGroup.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using Content.Server.GameObjects.Components.Atmos; +using Content.Shared.Atmos; +using Robust.Shared.ViewVariables; + +namespace Content.Server.Atmos +{ + public class ExcitedGroup : IDisposable + { + [ViewVariables] + private bool _disposed = false; + + [ViewVariables] + private readonly HashSet _tile = new HashSet(); + + [ViewVariables] + private GridAtmosphereComponent _gridAtmosphereComponent; + + [ViewVariables] + public int DismantleCooldown { get; set; } + + [ViewVariables] + public int BreakdownCooldown { get; set; } + + public void AddTile(TileAtmosphere tile) + { + _tile.Add(tile); + tile.ExcitedGroup = this; + ResetCooldowns(); + } + + public void MergeGroups(ExcitedGroup other) + { + var ourSize = _tile.Count; + var otherSize = other._tile.Count; + + if (ourSize > otherSize) + { + foreach (var tile in other._tile) + { + tile.ExcitedGroup = this; + _tile.Add(tile); + } + other._tile.Clear(); + other.Dispose(); + ResetCooldowns(); + } + else + { + foreach (var tile in _tile) + { + tile.ExcitedGroup = other; + other._tile.Add(tile); + } + _tile.Clear(); + Dispose(); + other.ResetCooldowns(); + } + } + + ~ExcitedGroup() + { + Dispose(); + } + + public void Initialize(GridAtmosphereComponent gridAtmosphereComponent) + { + _gridAtmosphereComponent = gridAtmosphereComponent; + _gridAtmosphereComponent.AddExcitedGroup(this); + } + + public void ResetCooldowns() + { + BreakdownCooldown = 0; + DismantleCooldown = 0; + } + + public void SelfBreakdown(bool spaceIsAllConsuming = false) + { + var combined = new GasMixture(Atmospherics.CellVolume); + + var tileSize = _tile.Count; + + if (_disposed) return; + + if (tileSize == 0) + { + Dispose(); + return; + } + + foreach (var tile in _tile) + { + if (tile?.Air == null) continue; + combined.Merge(tile.Air); + if (!spaceIsAllConsuming || !tile.Air.Immutable) continue; + combined.Clear(); + break; + } + + combined.Multiply(1 / (float)tileSize); + + foreach (var tile in _tile) + { + if (tile?.Air == null) continue; + tile.Air.CopyFromMutable(combined); + tile.UpdateVisuals(); + } + + BreakdownCooldown = 0; + } + + public void Dismantle(bool unexcite = true) + { + foreach (var tile in _tile) + { + if (tile == null) continue; + tile.ExcitedGroup = null; + if (!unexcite) continue; + tile.Excited = false; + _gridAtmosphereComponent.RemoveActiveTile(tile); + } + + _tile.Clear(); + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + _gridAtmosphereComponent.RemoveExcitedGroup(this); + + Dismantle(); + + _gridAtmosphereComponent = null; + } + } +} diff --git a/Content.Server/Atmos/GasMixture.cs b/Content.Server/Atmos/GasMixture.cs new file mode 100644 index 0000000000..341d3f7a66 --- /dev/null +++ b/Content.Server/Atmos/GasMixture.cs @@ -0,0 +1,543 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Content.Server.Atmos.Reactions; +using Content.Server.Interfaces; +using Content.Shared.Atmos; +using Robust.Shared.Interfaces.Serialization; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; +using Math = CannyFastMath.Math; +using MathF = CannyFastMath.MathF; + +namespace Content.Server.Atmos +{ + /// + /// A general-purpose, variable volume gas mixture. + /// + [Serializable] + public class GasMixture : IExposeData, IEquatable, ICloneable + { + [ViewVariables] + private float[] _moles = new float[Atmospherics.TotalNumberOfGases]; + + [ViewVariables] + private float[] _molesArchived = new float[Atmospherics.TotalNumberOfGases]; + private float _temperature = Atmospherics.TCMB; + public IReadOnlyList Gases => _moles; + + [ViewVariables] + public bool Immutable { get; private set; } + + [ViewVariables] + public float LastShare { get; private set; } = 0; + + [ViewVariables] + public float HeatCapacity + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + var capacity = 0f; + + for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++) + { + capacity += Atmospherics.GetGas(i).SpecificHeat * _moles[i]; + } + + return MathF.Max(capacity, Atmospherics.MinimumHeatCapacity); + } + } + + [ViewVariables] + public float HeatCapacityArchived + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + var capacity = 0f; + + for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++) + { + capacity += Atmospherics.GetGas(i).SpecificHeat * _molesArchived[i]; + } + + return MathF.Max(capacity, Atmospherics.MinimumHeatCapacity); + } + } + + [ViewVariables] + public float TotalMoles + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + var moles = 0f; + + foreach (var gas in _moles) + { + moles += gas; + } + + return moles; + } + } + + [ViewVariables] + public float Pressure + { + get + { + if (Volume <= 0) return 0f; + return TotalMoles * Atmospherics.R * Temperature / Volume; + } + } + + [ViewVariables] + public float Temperature + { + get => _temperature; + set + { + if(value < Atmospherics.TCMB) + throw new Exception($"Tried to set gas temperature below CMB! Value: {value}"); + + if (Immutable) return; + _temperature = value; + } + } + + public float ReactionResultFire { get; set; } + + [ViewVariables] + public float ThermalEnergy => Temperature * HeatCapacity; + + [ViewVariables] + public float TemperatureArchived { get; private set; } + + [ViewVariables] + public float Volume { get; set; } + + public GasMixture() + { + } + + public GasMixture(float volume) + { + if (volume < 0) + volume = 0; + Volume = volume; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void MarkImmutable() + { + Immutable = true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Archive() + { + _moles.AsSpan().CopyTo(_molesArchived.AsSpan()); + TemperatureArchived = Temperature; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Merge(GasMixture giver) + { + if (Immutable || giver == null) return; + + if (MathF.Abs(Temperature - giver.Temperature) > Atmospherics.MinimumTemperatureDeltaToConsider) + { + var combinedHeatCapacity = HeatCapacity + giver.HeatCapacity; + if (combinedHeatCapacity > 0f) + { + Temperature = + (giver.Temperature * giver.HeatCapacity + Temperature * HeatCapacity) / combinedHeatCapacity; + } + } + + for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++) + { + _moles[i] += giver._moles[i]; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float GetMoles(int gasId) + { + return _moles[gasId]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float GetMoles(Gas gas) + { + return GetMoles((int)gas); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetMoles(int gasId, float quantity) + { + if (!Immutable) + _moles[gasId] = quantity; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetMoles(Gas gas, float quantity) + { + SetMoles((int)gas, quantity); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AdjustMoles(int gasId, float quantity) + { + if (!Immutable) + _moles[gasId] += quantity; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AdjustMoles(Gas gas, float moles) + { + AdjustMoles((int)gas, moles); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GasMixture Remove(float amount) + { + return RemoveRatio(amount / TotalMoles); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GasMixture RemoveRatio(float ratio) + { + if(ratio <= 0) + return new GasMixture(Volume); + + if (ratio > 1) + ratio = 1; + + var removed = new GasMixture {Volume = Volume, Temperature = Temperature}; + + for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++) + { + var moles = _moles[i]; + if (moles < Atmospherics.GasMinMoles) + removed._moles[i] = 0f; + else + { + var removedMoles = moles * ratio; + removed._moles[i] = removedMoles; + if (!Immutable) + _moles[i] -= removedMoles; + } + } + + return removed; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CopyFromMutable(GasMixture sample) + { + if (Immutable) return; + sample._moles.AsSpan().CopyTo(_moles.AsSpan()); + Temperature = sample.Temperature; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Share(GasMixture sharer, int atmosAdjacentTurfs) + { + var temperatureDelta = TemperatureArchived - sharer.TemperatureArchived; + var absTemperatureDelta = Math.Abs(temperatureDelta); + var oldHeatCapacity = 0f; + var oldSharerHeatCapacity = 0f; + + if (absTemperatureDelta > Atmospherics.MinimumTemperatureDeltaToConsider) + { + oldHeatCapacity = HeatCapacity; + oldSharerHeatCapacity = sharer.HeatCapacity; + } + + var heatCapacityToSharer = 0f; + var heatCapacitySharerToThis = 0f; + var movedMoles = 0f; + var absMovedMoles = 0f; + + for(var i = 0; i < Atmospherics.TotalNumberOfGases; i++) + { + var thisValue = _moles[i]; + var sharerValue = sharer._moles[i]; + var delta = (thisValue - sharerValue) / (atmosAdjacentTurfs + 1); + if (!(MathF.Abs(delta) >= Atmospherics.GasMinMoles)) continue; + if (absTemperatureDelta > Atmospherics.MinimumTemperatureDeltaToConsider) + { + var gasHeatCapacity = delta * Atmospherics.GetGas(i).SpecificHeat; + if (delta > 0) + { + heatCapacityToSharer += gasHeatCapacity; + } + else + { + heatCapacitySharerToThis -= gasHeatCapacity; + } + } + + if (!Immutable) _moles[i] -= delta; + if (!sharer.Immutable) sharer._moles[i] += delta; + movedMoles += delta; + absMovedMoles += MathF.Abs(delta); + } + + LastShare = absMovedMoles; + + if (absTemperatureDelta > Atmospherics.MinimumTemperatureDeltaToConsider) + { + var newHeatCapacity = oldHeatCapacity + heatCapacitySharerToThis - heatCapacityToSharer; + var newSharerHeatCapacity = oldSharerHeatCapacity + heatCapacityToSharer - heatCapacitySharerToThis; + + // Transfer of thermal energy (via changed heat capacity) between self and sharer. + if (!Immutable && newHeatCapacity > Atmospherics.MinimumHeatCapacity) + { + Temperature = ((oldHeatCapacity * Temperature) - (heatCapacityToSharer * TemperatureArchived) + (heatCapacitySharerToThis * sharer.TemperatureArchived)) / newHeatCapacity; + } + + if (!sharer.Immutable && newSharerHeatCapacity > Atmospherics.MinimumHeatCapacity) + { + sharer.Temperature = ((oldSharerHeatCapacity * sharer.Temperature) - (heatCapacitySharerToThis * sharer.TemperatureArchived) + (heatCapacityToSharer*TemperatureArchived)) / newSharerHeatCapacity; + } + + // Thermal energy of the system (self and sharer) is unchanged. + + if (MathF.Abs(oldSharerHeatCapacity) > Atmospherics.MinimumHeatCapacity) + { + if (MathF.Abs(newSharerHeatCapacity / oldSharerHeatCapacity - 1) < 0.1) + { + TemperatureShare(sharer, Atmospherics.OpenHeatTransferCoefficient); + } + } + } + + if (!(temperatureDelta > Atmospherics.MinimumTemperatureToMove) && + !(MathF.Abs(movedMoles) > Atmospherics.MinimumMolesDeltaToMove)) return 0f; + var moles = TotalMoles; + var theirMoles = sharer.TotalMoles; + + return (TemperatureArchived * (moles + movedMoles)) - (sharer.TemperatureArchived * (theirMoles - movedMoles)) * Atmospherics.R / Volume; + + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void TemperatureShare(GasMixture sharer, float conductionCoefficient) + { + var temperatureDelta = TemperatureArchived - sharer.TemperatureArchived; + if (MathF.Abs(temperatureDelta) > Atmospherics.MinimumTemperatureDeltaToConsider) + { + var heatCapacity = HeatCapacityArchived; + var sharerHeatCapacity = sharer.HeatCapacityArchived; + + if (sharerHeatCapacity > Atmospherics.MinimumHeatCapacity && heatCapacity > Atmospherics.MinimumHeatCapacity) + { + var heat = conductionCoefficient * temperatureDelta * (heatCapacity * sharerHeatCapacity / (heatCapacity + sharerHeatCapacity)); + + if (!Immutable) + Temperature = MathF.Abs(MathF.Max(Temperature - heat / heatCapacity, Atmospherics.TCMB)); + + if (!sharer.Immutable) + sharer.Temperature = MathF.Abs(MathF.Max(sharer.Temperature + heat / sharerHeatCapacity, Atmospherics.TCMB)); + } + } + } + + public enum GasCompareResult + { + NoExchange = -2, + TemperatureExchange = -1, + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GasCompareResult Compare(GasMixture sample) + { + var moles = 0f; + + for(var i = 0; i < Atmospherics.TotalNumberOfGases; i++) + { + var gasMoles = _moles[i]; + var delta = MathF.Abs(gasMoles - sample._moles[i]); + if (delta > Atmospherics.MinimumMolesDeltaToMove && (delta > gasMoles * Atmospherics.MinimumAirRatioToMove)) + return (GasCompareResult)i; // We can move gases! + moles += gasMoles; + } + + if (moles > Atmospherics.MinimumMolesDeltaToMove) + { + var tempDelta = MathF.Abs(Temperature - sample.Temperature); + if (tempDelta > Atmospherics.MinimumTemperatureDeltaToSuspend) + return GasCompareResult.TemperatureExchange; // There can be temperature exchange. + } + + // No exchange at all! + return GasCompareResult.NoExchange; + } + + /// + /// Pump gas from this mixture to the output mixture. + /// Amount depends on target pressure. + /// + /// The mixture to pump the gas to + /// The target pressure to reach + /// Whether we could pump air to the output or not + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool PumpGasTo(GasMixture outputAir, float targetPressure) + { + var outputStartingPressure = outputAir.Pressure; + var pressureDelta = targetPressure - outputStartingPressure; + + if (pressureDelta < 0.01) + // No need to pump gas, we've reached the target. + return false; + + if (!(TotalMoles > 0) || !(Temperature > 0)) return false; + + // We calculate the necessary moles to transfer with the ideal gas law. + var transferMoles = pressureDelta * outputAir.Volume / (Temperature * Atmospherics.R); + + // And now we transfer the gas. + var removed = Remove(transferMoles); + outputAir.Merge(removed); + return true; + } + + /// + /// Releases gas from this mixture to the output mixture. + /// It can't transfer air to a mixture with higher pressure. + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ReleaseGasTo(GasMixture outputAir, float targetPressure) + { + var outputStartingPressure = outputAir.Pressure; + var inputStartingPressure = Pressure; + + if (outputStartingPressure >= MathF.Min(targetPressure, inputStartingPressure - 10)) + // No need to pump gas if the target is already reached or input pressure is too low. + // Need at least 10 kPa difference to overcome friction in the mechanism. + return false; + + if (!(TotalMoles > 0) || !(Temperature > 0)) return false; + + // We calculate the necessary moles to transfer with the ideal gas law. + var pressureDelta = MathF.Min(targetPressure - outputStartingPressure, (inputStartingPressure - outputStartingPressure) / 2f); + var transferMoles = pressureDelta * outputAir.Volume / (Temperature * Atmospherics.R); + + // And now we transfer the gas. + var removed = Remove(transferMoles); + outputAir.Merge(removed); + + return true; + + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReactionResult React(IGasMixtureHolder holder) + { + var reaction = ReactionResult.NoReaction; + var temperature = Temperature; + var energy = ThermalEnergy; + + // TODO ATMOS Take reaction priority into account! + foreach (var prototype in IoCManager.Resolve().EnumeratePrototypes()) + { + if (energy < prototype.MinimumEnergyRequirement || + temperature < prototype.MinimumTemperatureRequirement) + continue; + + for (var i = 0; i < prototype.MinimumRequirements.Length; i++) + { + if(i > Atmospherics.TotalNumberOfGases) + throw new IndexOutOfRangeException("Reaction Gas Minimum Requirements Array Prototype exceeds total number of gases!"); + + var req = prototype.MinimumRequirements[i]; + if (GetMoles(i) < req) + continue; + + reaction = prototype.React(this, holder); + if(reaction.HasFlag(ReactionResult.NoReaction)) + break; + } + } + + return reaction; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear() + { + if (Immutable) return; + Array.Clear(_moles, 0, Atmospherics.TotalNumberOfGases); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Multiply(float multiplier) + { + if (Immutable) return; + for(var i = 0; i < Atmospherics.TotalNumberOfGases; i++) + { + _moles[i] *= multiplier; + } + } + + public void ExposeData(ObjectSerializer serializer) + { + serializer.DataField(this, x => Immutable, "immutable", false); + serializer.DataField(this, x => Volume, "volume", 0f); + serializer.DataField(this, x => LastShare, "lastShare", 0f); + serializer.DataField(this, x => TemperatureArchived, "temperatureArchived", 0f); + serializer.DataField(ref _moles, "moles", new float[Atmospherics.TotalNumberOfGases]); + serializer.DataField(ref _molesArchived, "molesArchived", new float[Atmospherics.TotalNumberOfGases]); + serializer.DataField(ref _temperature, "temperature", Atmospherics.TCMB); + } + + public override bool Equals(object? obj) + { + if (obj is GasMixture mix) + return Equals(mix); + return false; + } + + public bool Equals(GasMixture? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(_moles, other._moles) + && Equals(_molesArchived, other._molesArchived) + && _temperature.Equals(other._temperature) + && Immutable == other.Immutable + && LastShare.Equals(other.LastShare) + && TemperatureArchived.Equals(other.TemperatureArchived) + && Volume.Equals(other.Volume); + } + + public override int GetHashCode() + { + return HashCode.Combine(_moles, _molesArchived, _temperature, Immutable, LastShare, TemperatureArchived, Volume); + } + + public object Clone() + { + return new GasMixture() + { + _moles = (float[])_moles.Clone(), + _molesArchived = (float[])_molesArchived.Clone(), + _temperature = _temperature, + Immutable = Immutable, + LastShare = LastShare, + TemperatureArchived = TemperatureArchived, + Volume = Volume, + }; + } + } +} diff --git a/Content.Server/Atmos/HighPressureMovementController.cs b/Content.Server/Atmos/HighPressureMovementController.cs new file mode 100644 index 0000000000..b7644151d4 --- /dev/null +++ b/Content.Server/Atmos/HighPressureMovementController.cs @@ -0,0 +1,81 @@ +#nullable enable +using Content.Server.GameObjects.Components.Atmos; +using Robust.Shared.GameObjects.Components; +using Robust.Shared.Interfaces.Physics; +using Robust.Shared.Interfaces.Random; +using Robust.Shared.IoC; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Physics; +using Robust.Shared.Random; +using Logger = Robust.Shared.Log.Logger; +using MathF = CannyFastMath.MathF; + +namespace Content.Server.Atmos +{ + public class HighPressureMovementController : VirtualController + { + [Dependency] private IRobustRandom _robustRandom = default!; + [Dependency] private IPhysicsManager _physicsManager = default!; + public override ICollidableComponent? ControlledComponent { protected get; set; } + + private const float MoveForcePushRatio = 1f; + private const float MoveForceForcePushRatio = 1f; + private const float ProbabilityOffset = 25f; + private const float ProbabilityBasePercent = 10f; + private const float ThrowForce = 100f; + + public void ExperiencePressureDifference(int cycle, float pressureDifference, Direction direction, + float pressureResistanceProbDelta, GridCoordinates throwTarget) + { + if (ControlledComponent == null) + return; + + // TODO ATMOS stuns? + + var transform = ControlledComponent.Owner.Transform; + var pressureComponent = ControlledComponent.Owner.GetComponent(); + var maxForce = MathF.Sqrt(pressureDifference) * 2.25f; + var moveProb = 100f; + + if (pressureComponent.PressureResistance > 0) + moveProb = MathF.Abs((pressureDifference / pressureComponent.PressureResistance * ProbabilityBasePercent) - + ProbabilityOffset); + + if (moveProb > ProbabilityOffset && _robustRandom.Prob(MathF.Min(moveProb / 100f, 1f)) + && !float.IsPositiveInfinity(pressureComponent.MoveResist) + && (!ControlledComponent.Anchored + && (maxForce >= (pressureComponent.MoveResist * MoveForcePushRatio))) + || (ControlledComponent.Anchored && (maxForce >= (pressureComponent.MoveResist * MoveForceForcePushRatio)))) + { + + + if (maxForce > ThrowForce && throwTarget != GridCoordinates.InvalidGrid) + { + var moveForce = MathF.Min(maxForce * MathF.Clamp(moveProb, 0, 100) / 100f, 50f); + var pos = throwTarget.Position - transform.GridPosition.Position; + LinearVelocity = pos * moveForce; + } + else + { + var moveForce = MathF.Min(maxForce * MathF.Clamp(moveProb, 0, 100) / 100f, 25f); + LinearVelocity = direction.ToVec() * moveForce; + } + + pressureComponent.LastHighPressureMovementAirCycle = cycle; + } + } + + public override void UpdateAfterProcessing() + { + base.UpdateAfterProcessing(); + + if (ControlledComponent != null && !_physicsManager.IsWeightless(ControlledComponent.Owner.Transform.GridPosition)) + { + LinearVelocity *= 0.85f; + if (LinearVelocity.Length < 1f) + Stop(); + } + } + } +} diff --git a/Content.Server/Atmos/Hotspot.cs b/Content.Server/Atmos/Hotspot.cs new file mode 100644 index 0000000000..2178a6bea8 --- /dev/null +++ b/Content.Server/Atmos/Hotspot.cs @@ -0,0 +1,34 @@ +using Robust.Shared.ViewVariables; + +namespace Content.Server.Atmos +{ + public struct Hotspot + { + [ViewVariables] + public bool Valid; + + [ViewVariables] + public bool SkippedFirstProcess; + + [ViewVariables] + public bool Bypassing; + + [ViewVariables] + public float Temperature; + + [ViewVariables] + public float Volume; + + /// + /// State for the fire sprite. + /// + [ViewVariables] + public int State; + + public void Start() + { + Valid = true; + State = 1; + } + } +} diff --git a/Content.Server/Atmos/IGridAtmosphereComponent.cs b/Content.Server/Atmos/IGridAtmosphereComponent.cs new file mode 100644 index 0000000000..5fdd25b2e3 --- /dev/null +++ b/Content.Server/Atmos/IGridAtmosphereComponent.cs @@ -0,0 +1,133 @@ +using System.Collections.Generic; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Map; + +namespace Content.Server.Atmos +{ + public interface IGridAtmosphereComponent : IComponent, IEnumerable + { + /// + /// Number of times has been called. + /// + int UpdateCounter { get; } + + /// + /// How many tiles have high pressure delta. + /// + int HighPressureDeltaCount { get; } + + /// + /// Control variable for equalization. + /// + long EqualizationQueueCycleControl { get; set; } + + /// + /// Attemps to pry a tile. + /// + /// + void PryTile(MapIndices indices); + + /// + /// Burns a tile. + /// + /// + void BurnTile(MapIndices gridIndices); + + /// + /// Invalidates a coordinate to be revalidated again. + /// Use this after changing a tile's gas contents, or when the tile becomes space, etc. + /// + /// + void Invalidate(MapIndices indices); + + /// + /// Adds an active tile so it becomes processed every update until it becomes inactive. + /// Also makes the tile excited. + /// + /// + void AddActiveTile(TileAtmosphere tile); + + /// + /// Removes an active tile and disposes of its . + /// Use with caution. + /// + /// + void RemoveActiveTile(TileAtmosphere tile); + + /// + /// Marks a tile as having a hotspot so it can be processed. + /// + /// + void AddHotspotTile(TileAtmosphere tile); + + /// + /// Removes a tile from the hotspot processing list. + /// + /// + void RemoveHotspotTile(TileAtmosphere tile); + + /// + /// Marks a tile has having high pressure differences that need to be equalized. + /// + /// + void AddHighPressureDelta(TileAtmosphere tile); + + /// + /// Returns whether the tile in question is marked as having high pressure differences or not. + /// + /// + /// + bool HasHighPressureDelta(TileAtmosphere tile); + + /// + /// Adds a excited group to be processed. + /// + /// + void AddExcitedGroup(ExcitedGroup excitedGroup); + + /// + /// Removes an excited group. + /// + /// + void RemoveExcitedGroup(ExcitedGroup excitedGroup); + + /// + /// Returns a tile. + /// + /// + /// + TileAtmosphere GetTile(MapIndices indices); + + /// + /// Returns a tile. + /// + /// + /// + TileAtmosphere GetTile(GridCoordinates coordinates); + + /// + /// Returns if the tile in question is air-blocked. + /// This could be due to a wall, an airlock, etc. + /// Also see AirtightComponent. + /// + /// + /// + bool IsAirBlocked(MapIndices indices); + + /// + /// Returns if the tile in question is space. + /// + /// + /// + bool IsSpace(MapIndices indices); + + /// + /// Returns the volume in liters for a number of cells/tiles. + /// + /// + /// + float GetVolumeForCells(int cellCount); + + void Update(float frameTime); + } +} diff --git a/Content.Server/Atmos/Reactions/GasReactionPrototype.cs b/Content.Server/Atmos/Reactions/GasReactionPrototype.cs new file mode 100644 index 0000000000..6f1023750e --- /dev/null +++ b/Content.Server/Atmos/Reactions/GasReactionPrototype.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using Content.Server.Interfaces; +using Content.Shared.Atmos; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using YamlDotNet.RepresentationModel; + +namespace Content.Server.Atmos.Reactions +{ + [Flags] + public enum ReactionResult : byte + { + NoReaction = 0, + Reacting = 1, + + } + + [Prototype("gasReaction")] + public class GasReactionPrototype : IPrototype, IIndexedPrototype + { + public string ID { get; private set; } + + /// + /// Minimum gas amount requirements. + /// + public float[] MinimumRequirements { get; private set; } + + /// + /// Minimum temperature requirement. + /// + public float MinimumTemperatureRequirement { get; private set; } + + /// + /// Minimum energy requirement. + /// + public float MinimumEnergyRequirement { get; private set; } + + /// + /// Lower numbers are checked/react later than higher numbers. + /// If two reactions have the same priority, they may happen in either order. + /// + public int Priority { get; private set; } + + /// + /// A list of effects this will produce. + /// + private List _effects; + + public void LoadFrom(YamlMappingNode mapping) + { + var serializer = YamlObjectSerializer.NewReader(mapping); + + serializer.DataField(this, x => ID, "id", string.Empty); + serializer.DataField(this, x => Priority, "priority", 100); + serializer.DataField(this, x => MinimumRequirements, "minimumRequirements", new float[Atmospherics.TotalNumberOfGases]); + serializer.DataField(this, x => MinimumTemperatureRequirement, "minimumTemperature", Atmospherics.TCMB); + serializer.DataField(this, x => MinimumEnergyRequirement, "minimumEnergy", 0f); + serializer.DataField(ref _effects, "effects", new List()); + } + + public ReactionResult React(GasMixture mixture, IGasMixtureHolder holder) + { + var result = ReactionResult.NoReaction; + + foreach (var effect in _effects) + { + result |= effect.React(mixture, holder); + } + + return result; + } + } +} diff --git a/Content.Server/Atmos/Reactions/PhoronFireReaction.cs b/Content.Server/Atmos/Reactions/PhoronFireReaction.cs new file mode 100644 index 0000000000..0d7fa36083 --- /dev/null +++ b/Content.Server/Atmos/Reactions/PhoronFireReaction.cs @@ -0,0 +1,90 @@ +#nullable enable +using CannyFastMath; +using Content.Server.Interfaces; +using Content.Shared.Atmos; +using JetBrains.Annotations; +using Robust.Shared.Serialization; + +namespace Content.Server.Atmos.Reactions +{ + [UsedImplicitly] + public class PhoronFireReaction : IGasReactionEffect + { + public ReactionResult React(GasMixture mixture, IGasMixtureHolder? holder) + { + var energyReleased = 0f; + var oldHeatCapacity = mixture.HeatCapacity; + var temperature = mixture.Temperature; + var location = holder as TileAtmosphere; + + // More phoron released at higher temperatures + var temperatureScale = 0f; + var superSaturation = false; + + if (temperature > Atmospherics.PhoronUpperTemperature) + temperatureScale = 1f; + else + temperatureScale = (temperature - Atmospherics.PhoronMinimumBurnTemperature) / + (Atmospherics.PhoronUpperTemperature - Atmospherics.PhoronMinimumBurnTemperature); + + if (temperatureScale > 0f) + { + var phoronBurnRate = 0f; + var oxygenBurnRate = Atmospherics.OxygenBurnRateBase - temperatureScale; + + if (mixture.GetMoles(Gas.Oxygen) / mixture.GetMoles(Gas.Phoron) > + Atmospherics.SuperSaturationThreshold) + superSaturation = true; + + if (mixture.GetMoles(Gas.Oxygen) > + mixture.GetMoles(Gas.Phoron) * Atmospherics.PhoronOxygenFullburn) + phoronBurnRate = (mixture.GetMoles(Gas.Phoron) * temperatureScale) / + Atmospherics.PhoronBurnRateDelta; + else + phoronBurnRate = (temperatureScale * (mixture.GetMoles(Gas.Oxygen) / Atmospherics.PhoronOxygenFullburn)) / Atmospherics.PhoronBurnRateDelta; + + if (phoronBurnRate > Atmospherics.MinimumHeatCapacity) + { + phoronBurnRate = MathF.Min(MathF.Min(phoronBurnRate, mixture.GetMoles(Gas.Phoron)), mixture.GetMoles(Gas.Oxygen)/oxygenBurnRate); + mixture.SetMoles(Gas.Phoron, mixture.GetMoles(Gas.Phoron) - phoronBurnRate); + mixture.SetMoles(Gas.Oxygen, mixture.GetMoles(Gas.Oxygen) - (phoronBurnRate * oxygenBurnRate)); + + if(superSaturation) + mixture.AdjustMoles(Gas.Tritium, phoronBurnRate); + else + mixture.AdjustMoles(Gas.CarbonDioxide, phoronBurnRate); + + energyReleased += Atmospherics.FirePhoronEnergyReleased * (phoronBurnRate); + + mixture.ReactionResultFire += (phoronBurnRate) * (1 + oxygenBurnRate); + } + } + + if (energyReleased > 0) + { + var newHeatCapacity = mixture.HeatCapacity; + if (newHeatCapacity > Atmospherics.MinimumHeatCapacity) + mixture.Temperature = ((temperature * oldHeatCapacity + energyReleased) / newHeatCapacity); + } + + if (location != null) + { + temperature = mixture.Temperature; + if (temperature > Atmospherics.FireMinimumTemperatureToExist) + { + location.HotspotExpose(temperature, Atmospherics.CellVolume); + + // TODO ATMOS Expose temperature all items on cell + + location.TemperatureExpose(mixture, temperature, Atmospherics.CellVolume); + } + } + + return mixture.ReactionResultFire != 0 ? ReactionResult.Reacting : ReactionResult.NoReaction; + } + + public void ExposeData(ObjectSerializer serializer) + { + } + } +} diff --git a/Content.Server/Atmos/TileAtmosInfo.cs b/Content.Server/Atmos/TileAtmosInfo.cs new file mode 100644 index 0000000000..b8bdb70a68 --- /dev/null +++ b/Content.Server/Atmos/TileAtmosInfo.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Robust.Shared.Maths; +using Robust.Shared.ViewVariables; + +namespace Content.Server.Atmos +{ + public struct TileAtmosInfo + { + [ViewVariables] + public int LastCycle; + + [ViewVariables] + public long LastQueueCycle; + + [ViewVariables] + public long LastSlowQueueCycle; + + [ViewVariables] + public float MoleDelta; + + [ViewVariables] + public float TransferDirectionEast; + + [ViewVariables] + public float TransferDirectionWest; + + [ViewVariables] + public float TransferDirectionNorth; + + [ViewVariables] + public float TransferDirectionSouth; + + public float this[Direction direction] + { + get => + direction switch + { + Direction.East => TransferDirectionEast, + Direction.West => TransferDirectionWest, + Direction.North => TransferDirectionNorth, + Direction.South => TransferDirectionSouth, + _ => throw new ArgumentOutOfRangeException(nameof(direction)) + }; + + set + { + switch (direction) + { + case Direction.East: + TransferDirectionEast = value; + break; + case Direction.West: + TransferDirectionWest = value; + break; + case Direction.North: + TransferDirectionNorth = value; + break; + case Direction.South: + TransferDirectionSouth = value; + break; + default: + throw new ArgumentOutOfRangeException(nameof(direction)); + } + } + } + + [ViewVariables] + public float CurrentTransferAmount; + + public Direction CurrentTransferDirection; + + [ViewVariables] + public bool FastDone; + } +} diff --git a/Content.Server/Atmos/TileAtmosphere.cs b/Content.Server/Atmos/TileAtmosphere.cs new file mode 100644 index 0000000000..24d264d2b5 --- /dev/null +++ b/Content.Server/Atmos/TileAtmosphere.cs @@ -0,0 +1,891 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Content.Server.GameObjects.Components.Atmos; +using Content.Server.GameObjects.EntitySystems; +using Content.Server.Interfaces; +using Content.Shared.Atmos; +using Content.Shared.Audio; +using Robust.Server.GameObjects.EntitySystems; +using Robust.Shared.GameObjects.Components; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Map; +using Robust.Shared.Interfaces.Random; +using Robust.Shared.IoC; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Random; +using Robust.Shared.ViewVariables; +using Logger = Robust.Shared.Log.Logger; +using MathF = CannyFastMath.MathF; + +namespace Content.Server.Atmos +{ + public class TileAtmosphere : IGasMixtureHolder + { + [Robust.Shared.IoC.Dependency] private IRobustRandom _robustRandom = default!; + [Robust.Shared.IoC.Dependency] private IEntityManager _entityManager = default!; + [Robust.Shared.IoC.Dependency] private IMapManager _mapManager = default!; + + private int _archivedCycle = 0; + private int _currentCycle = 0; + + // 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 = 0; + + [ViewVariables] + public TileAtmosphere PressureSpecificTarget { get; set; } = null; + + [ViewVariables] + public float PressureDifference { get; set; } = 0; + + [ViewVariables] + public bool Excited { get; set; } = false; + + [ViewVariables] + private GridAtmosphereComponent _gridAtmosphereComponent; + + [ViewVariables] + private readonly Dictionary _adjacentTiles = new Dictionary(); + + [ViewVariables] + private TileAtmosInfo _tileAtmosInfo; + + [ViewVariables] + public Hotspot Hotspot; + + private Direction _pressureDirection; + + [ViewVariables] + public GridId GridIndex { get; } + + [ViewVariables] + public MapIndices GridIndices { get; } + + [ViewVariables] + public ExcitedGroup ExcitedGroup { get; set; } + + [ViewVariables] + public GasMixture Air { get; set; } + + public TileAtmosphere(GridAtmosphereComponent atmosphereComponent, GridId gridIndex, MapIndices gridIndices, GasMixture mixture = null) + { + IoCManager.InjectDependencies(this); + _gridAtmosphereComponent = atmosphereComponent; + GridIndex = gridIndex; + GridIndices = gridIndices; + Air = mixture; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Archive(int fireCount) + { + _archivedCycle = fireCount; + Air?.Archive(); + } + + 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 phoron = Air.GetMoles(Gas.Phoron); + var tritium = Air.GetMoles(Gas.Tritium); + + if (Hotspot.Valid) + { + if (soh) + { + if (phoron > 0.5f || tritium > 0.5f) + { + if (Hotspot.Temperature < exposedTemperature) + Hotspot.Temperature = exposedTemperature; + if (Hotspot.Volume < exposedVolume) + Hotspot.Volume = exposedVolume; + } + } + + return; + } + + if ((exposedTemperature > Atmospherics.PhoronMinimumBurnTemperature) && (phoron > 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); + } + } + + //[MethodImpl(MethodImplOptions.AggressiveInlining)] + public void HighPressureMovements() + { + // TODO ATMOS finish this + + if(PressureDifference > 15) + { + if(_soundCooldown == 0) + EntitySystem.Get().PlayAtCoords("/Audio/Effects/space_wind.ogg", + GridIndices.ToGridCoordinates(_mapManager, GridIndex), AudioHelpers.WithVariation(0.125f).WithVolume(MathF.Clamp(PressureDifference / 10, 10, 100))); + } + + + foreach (var entity in _entityManager.GetEntitiesIntersecting(_mapManager.GetGrid(GridIndex).ParentMapId, Box2.UnitCentered.Translated(GridIndices))) + { + if (!entity.TryGetComponent(out ICollidableComponent physics) + || !entity.TryGetComponent(out MovedByPressureComponent pressure)) + continue; + + var pressureMovements = physics.EnsureController(); + if (pressure.LastHighPressureMovementAirCycle < _gridAtmosphereComponent.UpdateCounter) + { + pressureMovements.ExperiencePressureDifference(_gridAtmosphereComponent.UpdateCounter, PressureDifference, _pressureDirection, 0, PressureSpecificTarget?.GridIndices.ToGridCoordinates(_mapManager, GridIndex) ?? GridCoordinates.InvalidGrid); + } + + } + + if (PressureDifference > 100) + { + // Do space wind graphics here! + } + + _soundCooldown++; + if (_soundCooldown > 75) + _soundCooldown = 0; + } + + //[MethodImpl(MethodImplOptions.AggressiveInlining)] + public void EqualizePressureInZone(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 + foreach (var (direction, other) in _adjacentTiles) + { + 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 = new TileAtmosphere[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; + } + + foreach (var (_, adj) in exploring._adjacentTiles) + { + if (adj?.Air == null) continue; + if(adj._tileAtmosInfo.LastQueueCycle == queueCycle) continue; + adj._tileAtmosInfo = new TileAtmosInfo(); + + adj._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 / (tiles.Length); + var giverTiles = new List(); + var takerTiles = new List(); + + 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.Add(tile); + } + else + { + takerTiles.Add(tile); + } + } + + var logN = MathF.Log2(tiles.Length); + + // Optimization - try to spread gases using an O(nlogn) algorithm that has a chance of not working first to avoid O(n^2) + if (giverTiles.Count > logN && takerTiles.Count > logN) + { + // Even if it fails, it will speed up the next part. + Array.Sort(tiles, (a, b) + => a._tileAtmosInfo.MoleDelta.CompareTo(b._tileAtmosInfo.MoleDelta)); + + foreach (var tile in tiles) + { + tile._tileAtmosInfo.FastDone = true; + if (!(tile._tileAtmosInfo.MoleDelta > 0)) continue; + Direction eligibleAdjBits = 0; + var amtEligibleAdj = 0; + foreach (var direction in Cardinal) + { + if (!tile._adjacentTiles.TryGetValue(direction, out var tile2)) continue; + + // skip anything that isn't part of our current processing block. Original one didn't do this unfortunately, which probably cause some massive lag. + if (tile2._tileAtmosInfo.FastDone || tile2._tileAtmosInfo.LastQueueCycle != queueCycle) + continue; + + eligibleAdjBits |= direction; + amtEligibleAdj++; + } + + if (amtEligibleAdj <= 0) continue; // Oof we've painted ourselves into a corner. Bad luck. Next part will handle this. + var molesToMove = tile._tileAtmosInfo.MoleDelta / amtEligibleAdj; + foreach (var direction in Cardinal) + { + if((eligibleAdjBits & direction) == 0 || !tile._adjacentTiles.TryGetValue(direction, out var tile2)) continue; + tile.AdjustEqMovement(direction, molesToMove); + tile._tileAtmosInfo.MoleDelta -= molesToMove; + tile2._tileAtmosInfo.MoleDelta += molesToMove; + } + } + + giverTiles.Clear(); + takerTiles.Clear(); + + foreach (var tile in tiles) + { + if (tile._tileAtmosInfo.MoleDelta > 0) + { + giverTiles.Add(tile); + } + else + { + takerTiles.Add(tile); + } + } + + // This is the part that can become O(n^2). + if (giverTiles.Count < takerTiles.Count) + { + // 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 = new List(takerTiles.Count); + foreach (var giver in giverTiles) + { + giver._tileAtmosInfo.CurrentTransferDirection = (Direction)(-1); + giver._tileAtmosInfo.CurrentTransferAmount = 0; + var queueCycleSlow = ++_gridAtmosphereComponent.EqualizationQueueCycleControl; + queue.Clear(); + queue.Add(giver); + giver._tileAtmosInfo.LastSlowQueueCycle = queueCycleSlow; + var queueCount = queue.Count; + for (var i = 0; i < queueCount; i++) + { + if (giver._tileAtmosInfo.MoleDelta <= 0) + break; // We're done here now. Let's not do more work than needed. + + var tile = queue[i]; + foreach (var direction in Cardinal) + { + if(!tile._adjacentTiles.TryGetValue(direction, out var tile2)) continue; + if (giver._tileAtmosInfo.MoleDelta <= 0) + break; // We're done here now. Let's not do more work than needed. + + if (tile2?._tileAtmosInfo == null || tile2._tileAtmosInfo.LastQueueCycle != queueCycle) + continue; + + if (tile2._tileAtmosInfo.LastSlowQueueCycle == queueCycleSlow) continue; + queue.Add(tile2); + queueCount++; + 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 = queue.Count - 1; i >= 0; i--) + { + var tile = queue[i]; + if (tile._tileAtmosInfo.CurrentTransferAmount != 0 && + tile._tileAtmosInfo.CurrentTransferDirection != (Direction)(-1)) + { + tile.AdjustEqMovement(tile._tileAtmosInfo.CurrentTransferDirection, tile._tileAtmosInfo.CurrentTransferAmount); + if(tile._adjacentTiles.TryGetValue(tile._tileAtmosInfo.CurrentTransferDirection, out var adjacent)) + adjacent._tileAtmosInfo.CurrentTransferAmount += tile._tileAtmosInfo.CurrentTransferAmount; + tile._tileAtmosInfo.CurrentTransferAmount = 0; + } + } + } + } + else + { + var queue = new List(giverTiles.Count); + foreach (var taker in takerTiles) + { + taker._tileAtmosInfo.CurrentTransferDirection = Direction.Invalid; + taker._tileAtmosInfo.CurrentTransferAmount = 0; + var queueCycleSlow = ++_gridAtmosphereComponent.EqualizationQueueCycleControl; + queue.Clear(); + queue.Add(taker); + taker._tileAtmosInfo.LastSlowQueueCycle = queueCycleSlow; + var queueCount = queue.Count; + for (int i = 0; i < queueCount; i++) + { + if (taker._tileAtmosInfo.MoleDelta >= 0) + break; // We're done here now. Let's not do more work than needed. + + var tile = queue[i]; + foreach (var direction in Cardinal) + { + if(!tile._adjacentTiles.ContainsKey(direction)) continue; + var tile2 = tile._adjacentTiles[direction]; + + if (taker._tileAtmosInfo.MoleDelta >= 0) + break; // We're done here now. Let's not do more work than needed. + + if (tile2?._tileAtmosInfo == null || tile2._tileAtmosInfo.LastQueueCycle != queueCycle) continue; + if (tile2._tileAtmosInfo.LastSlowQueueCycle == queueCycleSlow) continue; + queue.Add(tile2); + queueCount++; + 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 = queue.Count - 1; i >= 0; i--) + { + var tile = queue[i]; + if (tile._tileAtmosInfo.CurrentTransferAmount == 0 || + tile._tileAtmosInfo.CurrentTransferDirection == Direction.Invalid) continue; + tile.AdjustEqMovement(tile._tileAtmosInfo.CurrentTransferDirection, tile._tileAtmosInfo.CurrentTransferAmount); + + if(tile._adjacentTiles.TryGetValue(tile._tileAtmosInfo.CurrentTransferDirection, out var adjacent)) + adjacent._tileAtmosInfo.CurrentTransferAmount += tile._tileAtmosInfo.CurrentTransferAmount; + tile._tileAtmosInfo.CurrentTransferAmount = 0; + } + } + } + + foreach (var tile in tiles) + { + tile.FinalizeEq(); + } + + foreach (var tile in tiles) + { + foreach (var direction in Cardinal) + { + if (!tile._adjacentTiles.TryGetValue(direction, out var tile2)) continue; + if (tile2?.Air.Compare(Air) == GasMixture.GasCompareResult.NoExchange) continue; + _gridAtmosphereComponent.AddActiveTile(tile2); + break; + } + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FinalizeEq() + { + var transferDirections = new Dictionary(); + var hasTransferDirs = false; + foreach (var direction in Cardinal) + { + var amount = _tileAtmosInfo[direction]; + transferDirections[direction] = amount; + if (amount == 0) continue; + _tileAtmosInfo[direction] = 0; + hasTransferDirs = true; + } + + if (!hasTransferDirs) return; + + foreach (var direction in Cardinal) + { + var amount = transferDirections[direction]; + if (!_adjacentTiles.TryGetValue(direction, out var tile) || tile.Air == null) continue; + if (amount > 0) + { + // Prevent infinite recursion. + tile._tileAtmosInfo[direction.GetOpposite()] = 0; + + if (Air.TotalMoles < amount) + FinalizeEqNeighbors(); + + tile.Air.Merge(Air.Remove(amount)); + UpdateVisuals(); + tile.UpdateVisuals(); + ConsiderPressureDifference(tile, amount); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FinalizeEqNeighbors() + { + foreach (var direction in Cardinal) + { + var amount = _tileAtmosInfo[direction]; + if(amount < 0 && _adjacentTiles.TryGetValue(direction, out var adjacent)) + adjacent.FinalizeEq(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ConsiderPressureDifference(TileAtmosphere tile, float difference) + { + _gridAtmosphereComponent.AddHighPressureDelta(this); + if (difference > PressureDifference) + { + PressureDifference = difference; + _pressureDirection = ((Vector2i) (tile.GridIndices - GridIndices)).GetDir(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AdjustEqMovement(Direction direction, float molesToMove) + { + _tileAtmosInfo[direction] += molesToMove; + if(direction != (Direction)(-1) && _adjacentTiles.TryGetValue(direction, out var adj)) + _adjacentTiles[direction]._tileAtmosInfo[direction.GetOpposite()] -= molesToMove; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ProcessCell(int fireCount) + { + // Can't process a tile without air + if (Air == null) + { + _gridAtmosphereComponent.RemoveActiveTile(this); + return; + } + + if (_archivedCycle < fireCount) + Archive(fireCount); + + _currentCycle = fireCount; + var adjacentTileLength = 0; + foreach (var (_, enemyTile) in _adjacentTiles) + { + // If the tile is null or has no air, we don't do anything + if(enemyTile?.Air == null) continue; + adjacentTileLength++; + 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 = Air.Share(enemyTile.Air, adjacentTileLength); + + // Space wind! + if (difference > 0) + { + ConsiderPressureDifference(enemyTile, difference); + } + else + { + enemyTile.ConsiderPressureDifference(this, -difference); + } + + LastShareCheck(); + } + } + + React(); + UpdateVisuals(); + + if((!(Air.Temperature > Atmospherics.MinimumTemperatureStartSuperConduction && ConsiderSuperconductivity(true))) && ExcitedGroup == null) + _gridAtmosphereComponent.RemoveActiveTile(this); + } + + public void ProcessHotspot() + { + if (!Hotspot.Valid) + { + _gridAtmosphereComponent.RemoveHotspotTile(this); + return; + } + + if (!Hotspot.SkippedFirstProcess) + { + Hotspot.SkippedFirstProcess = true; + return; + } + + ExcitedGroup?.ResetCooldowns(); + + if ((Hotspot.Temperature < Atmospherics.FireMinimumTemperatureToExist) || (Hotspot.Volume <= 1f) + || Air == null || Air.Gases[(int)Gas.Oxygen] < 0.5f || Air.Gases[(int)Gas.Phoron] < 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 = 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 > Atmospherics.CellVolume*0.95); + + if (Hotspot.Bypassing) + { + Hotspot.Volume = Air.ReactionResultFire * Atmospherics.FireGrowthRate; + Hotspot.Temperature = Air.Temperature; + } + else + { + var affected = Air.RemoveRatio(Hotspot.Volume / Air.Volume); + if (affected != null) + { + affected.Temperature = Hotspot.Temperature; + affected.React(this); + Hotspot.Temperature = affected.Temperature; + Hotspot.Volume = affected.ReactionResultFire * Atmospherics.FireGrowthRate; + AssumeAir(affected); + } + } + + // TODO ATMOS Let all entities in this tile know about the fire? + } + + private bool ConsiderSuperconductivity(bool starting) + { + // TODO ATMOS + return false; + } + + //[MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ExplosivelyDepressurize(int cycleNum) + { + if (Air == null) return; + var totalGasesRemoved = 0f; + var queueCycle = ++_gridAtmosphereComponent.EqualizationQueueCycleControl; + var tiles = new List(); + var spaceTiles = new List(); + tiles.Add(this); + _tileAtmosInfo = new TileAtmosInfo + { + LastQueueCycle = queueCycle, + CurrentTransferDirection = Direction.Invalid + }; + var tileCount = 1; + for (var i = 0; i < tileCount; i++) + { + var tile = tiles[i]; + tile._tileAtmosInfo.LastCycle = cycleNum; + tile._tileAtmosInfo.CurrentTransferDirection = Direction.Invalid; + if (tile.Air.Immutable) + { + spaceTiles.Add(tile); + tile.PressureSpecificTarget = tile; + } + else + { + if (i > Atmospherics.ZumosHardTileLimit) continue; + foreach (var direction in Cardinal) + { + if (!_adjacentTiles.TryGetValue(direction, out var tile2)) continue; + if (tile2?.Air == null) continue; + if (tile2._tileAtmosInfo.LastQueueCycle == queueCycle) continue; + tile.ConsiderFirelocks(tile2); + if (tile._adjacentTiles[direction]?.Air != null) + { + tile2._tileAtmosInfo = new TileAtmosInfo {LastQueueCycle = queueCycle}; + tiles.Add(tile2); + tileCount++; + } + } + } + } + + var queueCycleSlow = ++_gridAtmosphereComponent.EqualizationQueueCycleControl; + var progressionOrder = new List(); + foreach (var tile in spaceTiles) + { + progressionOrder.Add(tile); + tile._tileAtmosInfo.LastSlowQueueCycle = queueCycleSlow; + tile._tileAtmosInfo.CurrentTransferDirection = Direction.Invalid; + } + + var progressionCount = progressionOrder.Count; + for (int i = 0; i < progressionCount; i++) + { + var tile = progressionOrder[i]; + foreach (var direction in Cardinal) + { + if (!_adjacentTiles.TryGetValue(direction, out var tile2)) continue; + if (tile2?._tileAtmosInfo.LastQueueCycle != queueCycle) continue; + if (tile2._tileAtmosInfo.LastSlowQueueCycle == queueCycleSlow) continue; + if(tile2.Air.Immutable) continue; + tile2._tileAtmosInfo.CurrentTransferDirection = direction.GetOpposite(); + tile2._tileAtmosInfo.CurrentTransferAmount = 0; + tile2.PressureSpecificTarget = tile.PressureSpecificTarget; + tile2._tileAtmosInfo.LastSlowQueueCycle = queueCycleSlow; + progressionOrder.Add(tile2); + progressionCount++; + } + } + + for (int i = 0; i < progressionCount; i++) + { + var tile = progressionOrder[i]; + if (tile._tileAtmosInfo.CurrentTransferDirection == Direction.Invalid) continue; + var hpdLength = _gridAtmosphereComponent.HighPressureDeltaCount; + var inHdp = _gridAtmosphereComponent.HasHighPressureDelta(tile); + if(!inHdp) + _gridAtmosphereComponent.AddHighPressureDelta(tile); + if (!tile._adjacentTiles.TryGetValue(tile._tileAtmosInfo.CurrentTransferDirection, out var tile2) || 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 == Direction.Invalid) + { + tile2.PressureDifference = tile2._tileAtmosInfo.CurrentTransferAmount; + tile2._pressureDirection = tile._tileAtmosInfo.CurrentTransferDirection; + } + tile.Air.Clear(); + tile.UpdateVisuals(); + tile.HandleDecompressionFloorRip(sum); + } + } + + private void HandleDecompressionFloorRip(float sum) + { + if (sum > 20 && _robustRandom.Prob(MathF.Clamp(sum / 100, 0.005f, 0.5f))) + _gridAtmosphereComponent.PryTile(GridIndices); + } + + private void ConsiderFirelocks(TileAtmosphere other) + { + // TODO ATMOS firelocks! + //throw new NotImplementedException(); + } + + + private void React() + { + // TODO ATMOS I think this is enough? gotta make sure... + Air?.React(this); + } + + public bool AssumeAir(GasMixture giver) + { + if (giver == null || Air == null) return false; + + Air.Merge(giver); + + UpdateVisuals(); + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void UpdateVisuals() + { + if (Air == null) return; + + _gasTileOverlaySystem ??= EntitySystem.Get(); + _gasTileOverlaySystem.Invalidate(GridIndex, GridIndices); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void UpdateAdjacent() + { + foreach (var direction in Cardinal) + { + if(!_gridAtmosphereComponent.IsAirBlocked(GridIndices.Offset(direction))) + _adjacentTiles[direction] = _gridAtmosphereComponent.GetTile(GridIndices.Offset(direction)); + } + } + + public void UpdateAdjacent(Direction direction) + { + _adjacentTiles[direction] = _gridAtmosphereComponent.GetTile(GridIndices.Offset(direction)); + } + + private void LastShareCheck() + { + var lastShare = Air.LastShare; + if (lastShare > Atmospherics.MinimumAirToSuspend) + { + ExcitedGroup.ResetCooldowns(); + } else if (lastShare > Atmospherics.MinimumMolesDeltaToMove) + { + ExcitedGroup.DismantleCooldown = 0; + } + } + + private static readonly Direction[] Cardinal = + new Direction[] + { + Direction.North, Direction.East, Direction.South, Direction.West + }; + + private static GasTileOverlaySystem _gasTileOverlaySystem; + + public void TemperatureExpose(GasMixture mixture, float temperature, float cellVolume) + { + // TODO ATMOS do this + } + } +} diff --git a/Content.Server/Content.Server.csproj b/Content.Server/Content.Server.csproj index 0c9be50c54..35caeeabfe 100644 --- a/Content.Server/Content.Server.csproj +++ b/Content.Server/Content.Server.csproj @@ -18,6 +18,7 @@ + diff --git a/Content.Server/GameObjects/Components/Atmos/AirtightComponent.cs b/Content.Server/GameObjects/Components/Atmos/AirtightComponent.cs new file mode 100644 index 0000000000..d78b271739 --- /dev/null +++ b/Content.Server/GameObjects/Components/Atmos/AirtightComponent.cs @@ -0,0 +1,98 @@ +using System; +using Content.Server.Atmos; +using Content.Server.GameObjects.EntitySystems; +using Robust.Server.Interfaces.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Components.Transform; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.IoC; +using Robust.Shared.Map; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameObjects.Components.Atmos +{ + [RegisterComponent] + public class AirtightComponent : Component, IMapInit + { + private SnapGridComponent _snapGrid; + private (GridId, MapIndices) _lastPosition; + + public override string Name => "Airtight"; + + private bool _airBlocked = true; + + [ViewVariables(VVAccess.ReadWrite)] + public bool AirBlocked + { + get => _airBlocked; + set + { + _airBlocked = value; + EntitySystem.Get().GetGridAtmosphere(Owner.Transform.GridID)?.Invalidate(_snapGrid.Position); + } + } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataField(ref _airBlocked, "airBlocked", true); + } + + public override void Initialize() + { + base.Initialize(); + + // Using the SnapGrid is critical for the performance of the room builder, and thus if + // it is absent the component will not be airtight. An exception is much easier to track + // down than the object magically not being airtight, so throw one if the SnapGrid component + // is missing. + if (!Owner.TryGetComponent(out _snapGrid)) + throw new Exception("Airtight entities must have a SnapGrid component"); + + UpdatePosition(); + } + + public override void OnRemove() + { + base.OnRemove(); + + _airBlocked = false; + + UpdatePosition(); + } + + public void MapInit() + { + _snapGrid.OnPositionChanged += OnTransformMove; + _lastPosition = (Owner.Transform.GridID, _snapGrid.Position); + UpdatePosition(); + } + + protected override void Shutdown() + { + base.Shutdown(); + + _airBlocked = false; + + _snapGrid.OnPositionChanged -= OnTransformMove; + UpdatePosition(); + } + + private void OnTransformMove() + { + UpdatePosition(_lastPosition.Item1, _lastPosition.Item2); + UpdatePosition(); + _lastPosition = (Owner.Transform.GridID, _snapGrid.Position); + } + + private void UpdatePosition() => UpdatePosition(Owner.Transform.GridID, _snapGrid.Position); + + private void UpdatePosition(GridId gridId, MapIndices pos) + { + EntitySystem.Get().GetGridAtmosphere(gridId)?.Invalidate(pos); + } + + } +} diff --git a/Content.Server/GameObjects/Components/Atmos/GasAnalyzerComponent.cs b/Content.Server/GameObjects/Components/Atmos/GasAnalyzerComponent.cs new file mode 100644 index 0000000000..8d3970797a --- /dev/null +++ b/Content.Server/GameObjects/Components/Atmos/GasAnalyzerComponent.cs @@ -0,0 +1,40 @@ +#nullable enable +using Content.Server.GameObjects.EntitySystems; +using Content.Shared.Atmos; +using Content.Shared.GameObjects.EntitySystems; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Map; +using Robust.Shared.Utility; + +namespace Content.Server.GameObjects.Components.Atmos +{ + [RegisterComponent] + public class GasAnalyzerComponent : Component, IExamine + { + public override string Name => "GasAnalyzer"; + public void Examine(FormattedMessage message, bool inDetailsRange) + { + if (!inDetailsRange) return; + + var gam = EntitySystem.Get().GetGridAtmosphere(Owner.Transform.GridID); + + var tile = gam?.GetTile(Owner.Transform.GridPosition).Air; + + if (tile == null) return; + + message.AddText($"Pressure: {tile.Pressure}\n"); + message.AddText($"Temperature: {tile.Temperature}\n"); + + for (int i = 0; i < Atmospherics.TotalNumberOfGases; i++) + { + var gas = Atmospherics.GetGas(i); + + if (tile.Gases[i] <= Atmospherics.GasMinMoles) continue; + + message.AddText(gas.Name); + message.AddText($"\n Moles: {tile.Gases[i]}\n"); + } + } + } +} diff --git a/Content.Server/GameObjects/Components/Atmos/GasCanisterComponent.cs b/Content.Server/GameObjects/Components/Atmos/GasCanisterComponent.cs new file mode 100644 index 0000000000..a2afda77de --- /dev/null +++ b/Content.Server/GameObjects/Components/Atmos/GasCanisterComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameObjects; + +namespace Content.Server.GameObjects.Components.Atmos +{ + [RegisterComponent] + public class GasCanisterComponent : Component + { + public override string Name => "GasCanister"; + } +} diff --git a/Content.Server/GameObjects/Components/Atmos/GasMixtureComponent.cs b/Content.Server/GameObjects/Components/Atmos/GasMixtureComponent.cs new file mode 100644 index 0000000000..68e59e0869 --- /dev/null +++ b/Content.Server/GameObjects/Components/Atmos/GasMixtureComponent.cs @@ -0,0 +1,19 @@ +using Content.Server.Atmos; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization; + +namespace Content.Server.GameObjects.Components.Atmos +{ + [RegisterComponent] + public class GasMixtureComponent : Component + { + public override string Name => "GasMixture"; + public GasMixture GasMixture { get; set; } = new GasMixture(); + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + serializer.DataField(this, x => GasMixture.Volume, "volume", 0f); + } + } +} diff --git a/Content.Server/GameObjects/Components/Atmos/GasTankComponent.cs b/Content.Server/GameObjects/Components/Atmos/GasTankComponent.cs new file mode 100644 index 0000000000..47f96463e0 --- /dev/null +++ b/Content.Server/GameObjects/Components/Atmos/GasTankComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameObjects; + +namespace Content.Server.GameObjects.Components.Atmos +{ + [RegisterComponent] + public class GasTankComponent : Component + { + public override string Name => "GasTank"; + } +} diff --git a/Content.Server/GameObjects/Components/Atmos/GridAtmosphereComponent.cs b/Content.Server/GameObjects/Components/Atmos/GridAtmosphereComponent.cs new file mode 100644 index 0000000000..cee6f2f737 --- /dev/null +++ b/Content.Server/GameObjects/Components/Atmos/GridAtmosphereComponent.cs @@ -0,0 +1,523 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using Content.Server.Atmos; +using Content.Shared.Atmos; +using Content.Shared.Maps; +using Robust.Server.Interfaces.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Components.Map; +using Robust.Shared.GameObjects.Components.Transform; +using Robust.Shared.Interfaces.Map; +using Robust.Shared.Interfaces.Timing; +using Robust.Shared.IoC; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Serialization; +using Robust.Shared.Timing; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameObjects.Components.Atmos +{ + /// + /// This is our SSAir equivalent. + /// + [RegisterComponent, Serializable] + public class GridAtmosphereComponent : Component, IGridAtmosphereComponent + { + [Robust.Shared.IoC.Dependency] private IGameTiming _gameTiming = default!; + [Robust.Shared.IoC.Dependency] private IMapManager _mapManager = default!; + + /// + /// Check current execution time every n instances processed. + /// + private const int LagCheckIterations = 15; + + /// + /// Max milliseconds allowed for atmos updates. + /// + private const float LagCheckMaxMilliseconds = 5f; + + /// + /// How much time before atmos updates are ran. + /// + private const float AtmosTime = 1/26f; + + public override string Name => "GridAtmosphere"; + + private float _timer = 0f; + private Stopwatch _stopwatch = new Stopwatch(); + public int UpdateCounter { get; private set; } = 0; + private IMapGrid _grid; + + [ViewVariables] + private readonly HashSet _excitedGroups = new HashSet(1000); + + [ViewVariables] + private readonly Dictionary _tiles = new Dictionary(1000); + + [ViewVariables] + private readonly HashSet _activeTiles = new HashSet(1000); + + [ViewVariables] + private readonly HashSet _hotspotTiles = new HashSet(1000); + + [ViewVariables] + private readonly HashSet _invalidatedCoords = new HashSet(1000); + + [ViewVariables] + private HashSet _highPressureDelta = new HashSet(1000); + + [ViewVariables] + private ProcessState _state = ProcessState.TileEqualize; + + private enum ProcessState + { + TileEqualize, + ActiveTiles, + ExcitedGroups, + HighPressureDelta, + Hotspots, + } + + /// + public void PryTile(MapIndices indices) + { + if (IsSpace(indices) || IsAirBlocked(indices)) return; + + var tile = _grid.GetTileRef(indices).Tile; + + var tileDefinitionManager = IoCManager.Resolve(); + var tileDef = (ContentTileDefinition)tileDefinitionManager[tile.TypeId]; + + var underplating = tileDefinitionManager["underplating"]; + _grid.SetTile(indices, new Tile(underplating.TileId)); + + //Actually spawn the relevant tile item at the right position and give it some offset to the corner. + var tileItem = IoCManager.Resolve().SpawnEntity(tileDef.ItemDropPrototypeName, new GridCoordinates(indices.X, indices.Y, _grid)); + tileItem.Transform.WorldPosition += (0.2f, 0.2f); + } + + public override void Initialize() + { + base.Initialize(); + + _grid = Owner.GetComponent().Grid; + + RepopulateTiles(); + } + + public override void OnAdd() + { + base.OnAdd(); + + _grid = Owner.GetComponent().Grid; + + RepopulateTiles(); + } + + public void RepopulateTiles() + { + _tiles.Clear(); + + foreach (var tile in _grid.GetAllTiles()) + { + if(!_tiles.ContainsKey(tile.GridIndices)) + _tiles.Add(tile.GridIndices, new TileAtmosphere(this, tile.GridIndex, tile.GridIndices, new GasMixture(GetVolumeForCells(1)){Temperature = Atmospherics.T20C})); + } + + foreach (var (_, tile) in _tiles.ToArray()) + { + tile.UpdateAdjacent(); + tile.UpdateVisuals(); + } + } + + /// + public void Invalidate(MapIndices indices) + { + _invalidatedCoords.Add(indices); + } + + private void Revalidate() + { + foreach (var indices in _invalidatedCoords.ToArray()) + { + var tile = GetTile(indices); + AddActiveTile(tile); + + if (tile == null) + { + tile = new TileAtmosphere(this, _grid.Index, indices, new GasMixture(GetVolumeForCells(1)){Temperature = Atmospherics.T20C}); + _tiles.Add(indices, tile); + } + + if (IsSpace(indices)) + { + tile.Air = new GasMixture(GetVolumeForCells(1)); + tile.Air.MarkImmutable(); + } else if (IsAirBlocked(indices)) + { + tile.Air = null; + } + else + { + tile.Air ??= new GasMixture(GetVolumeForCells(1)); + } + + tile.UpdateAdjacent(); + tile.UpdateVisuals(); + + foreach (var direction in Cardinal()) + { + var otherIndices = indices.Offset(direction); + var otherTile = GetTile(otherIndices); + AddActiveTile(otherTile); + otherTile?.UpdateAdjacent(direction.GetOpposite()); + } + } + + _invalidatedCoords.Clear(); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddActiveTile(TileAtmosphere tile) + { + if (tile?.GridIndex != _grid.Index || tile?.Air == null) return; + tile.Excited = true; + _activeTiles.Add(tile); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RemoveActiveTile(TileAtmosphere tile) + { + if (tile == null) return; + _activeTiles.Remove(tile); + tile.Excited = false; + tile.ExcitedGroup?.Dispose(); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddHotspotTile(TileAtmosphere tile) + { + if (tile?.GridIndex != _grid.Index || tile?.Air == null) return; + _hotspotTiles.Add(tile); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RemoveHotspotTile(TileAtmosphere tile) + { + if (tile == null) return; + _hotspotTiles.Remove(tile); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddHighPressureDelta(TileAtmosphere tile) + { + if (tile?.GridIndex != _grid.Index) return; + _highPressureDelta.Add(tile); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool HasHighPressureDelta(TileAtmosphere tile) + { + return _highPressureDelta.Contains(tile); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddExcitedGroup(ExcitedGroup excitedGroup) + { + _excitedGroups.Add(excitedGroup); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RemoveExcitedGroup(ExcitedGroup excitedGroup) + { + _excitedGroups.Remove(excitedGroup); + } + + /// + public TileAtmosphere GetTile(GridCoordinates coordinates) + { + return GetTile(coordinates.ToMapIndices(_mapManager)); + } + + /// + public TileAtmosphere GetTile(MapIndices indices) + { + if (_tiles.TryGetValue(indices, out var tile)) return tile; + + // We don't have that tile! + if (IsSpace(indices)) + { + var space = new TileAtmosphere(this, _grid.Index, indices, new GasMixture(int.MaxValue){Temperature = Atmospherics.TCMB}); + space.Air.MarkImmutable(); + return space; + } + + return null; + } + + /// + public bool IsAirBlocked(MapIndices indices) + { + var ac = GetObstructingComponent(indices); + return ac != null && ac.AirBlocked; + } + + /// + public bool IsSpace(MapIndices indices) + { + // TODO ATMOS use ContentTileDefinition to define in YAML whether or not a tile is considered space + return _grid.GetTileRef(indices).Tile.IsEmpty; + } + + public Dictionary GetAdjacentTiles(MapIndices indices) + { + var sides = new Dictionary(); + foreach (var dir in Cardinal()) + { + var side = indices.Offset(dir); + sides[dir] = GetTile(side); + } + + return sides; + } + + /// + public int HighPressureDeltaCount => _highPressureDelta.Count; + + public long EqualizationQueueCycleControl { get; set; } + + /// + public float GetVolumeForCells(int cellCount) + { + return _grid.TileSize * cellCount * Atmospherics.CellVolume; + } + + /// + public void Update(float frameTime) + { + _timer += frameTime; + + if (_invalidatedCoords.Count != 0) + Revalidate(); + + if (_timer < AtmosTime) + return; + + // We subtract it so it takes lost time into account. + _timer -= AtmosTime; + + switch (_state) + { + case ProcessState.TileEqualize: + if(ProcessTileEqualize()) + _state = ProcessState.ActiveTiles; + return; + case ProcessState.ActiveTiles: + if(ProcessActiveTiles()) + _state = ProcessState.ExcitedGroups; + return; + case ProcessState.ExcitedGroups: + if(ProcessExcitedGroups()) + _state = ProcessState.HighPressureDelta; + return; + case ProcessState.HighPressureDelta: + if(ProcessHighPressureDelta()) + _state = ProcessState.Hotspots; + break; + case ProcessState.Hotspots: + if(ProcessHotspots()) + _state = ProcessState.TileEqualize; + break; + } + + UpdateCounter++; + } + + public bool ProcessTileEqualize() + { + _stopwatch.Restart(); + + var number = 0; + foreach (var tile in _activeTiles.ToArray()) + { + tile.EqualizePressureInZone(UpdateCounter); + + if (number++ < LagCheckIterations) continue; + number = 0; + // Process the rest next time. + if (_stopwatch.Elapsed.TotalMilliseconds >= LagCheckMaxMilliseconds) + return false; + } + + return true; + } + + public bool ProcessActiveTiles() + { + _stopwatch.Restart(); + + var number = 0; + foreach (var tile in _activeTiles.ToArray()) + { + tile.ProcessCell(UpdateCounter); + + if (number++ < LagCheckIterations) continue; + number = 0; + // Process the rest next time. + if (_stopwatch.Elapsed.TotalMilliseconds >= LagCheckMaxMilliseconds) + return false; + } + + return true; + } + + public bool ProcessExcitedGroups() + { + _stopwatch.Restart(); + + var number = 0; + foreach (var excitedGroup in _excitedGroups.ToArray()) + { + excitedGroup.BreakdownCooldown++; + excitedGroup.DismantleCooldown++; + + if(excitedGroup.BreakdownCooldown > Atmospherics.ExcitedGroupBreakdownCycles) + excitedGroup.SelfBreakdown(); + + else if(excitedGroup.DismantleCooldown > Atmospherics.ExcitedGroupsDismantleCycles) + excitedGroup.Dismantle(); + + if (number++ < LagCheckIterations) continue; + number = 0; + // Process the rest next time. + if (_stopwatch.Elapsed.TotalMilliseconds >= LagCheckMaxMilliseconds) + return false; + } + + return true; + } + + public bool ProcessHighPressureDelta() + { + _stopwatch.Restart(); + + var number = 0; + foreach (var tile in _highPressureDelta.ToArray()) + { + tile.HighPressureMovements(); + tile.PressureDifference = 0f; + tile.PressureSpecificTarget = null; + _highPressureDelta.Remove(tile); + + if (number++ < LagCheckIterations) continue; + number = 0; + // Process the rest next time. + if (_stopwatch.Elapsed.TotalMilliseconds >= LagCheckMaxMilliseconds) + return false; + } + + return true; + } + + private bool ProcessHotspots() + { + _stopwatch.Restart(); + + var number = 0; + foreach (var hotspot in _hotspotTiles.ToArray()) + { + hotspot.ProcessHotspot(); + + if (number++ < LagCheckIterations) continue; + number = 0; + // Process the rest next time. + if (_stopwatch.Elapsed.TotalMilliseconds >= LagCheckMaxMilliseconds) + return false; + } + + return true; + } + + private AirtightComponent GetObstructingComponent(MapIndices indices) + { + foreach (var v in _grid.GetSnapGridCell(indices, SnapGridOffset.Center)) + { + if (v.Owner.TryGetComponent(out var ac)) + return ac; + } + + return null; + } + + private static IEnumerable Cardinal() => + new[] + { + Direction.North, Direction.East, Direction.South, Direction.West + }; + + public void Dispose() + { + + } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + if (serializer.Reading) + { + var gridId = Owner.GetComponent().Grid.Index; + + if (!serializer.TryReadDataField("uniqueMixes", out List uniqueMixes) || + !serializer.TryReadDataField("tiles", out Dictionary tiles)) + return; + foreach (var (indices, mix) in tiles) + { + _tiles.Add(indices, new TileAtmosphere(this, gridId, indices, (GasMixture)uniqueMixes[mix].Clone())); + Invalidate(indices); + } + } else if (serializer.Writing) + { + var uniqueMixes = new List(); + var tiles = new Dictionary(); + foreach (var (indices, tile) in _tiles) + { + if (tile.Air == null) continue; + uniqueMixes.Add(tile.Air); + tiles[indices] = uniqueMixes.Count - 1; + } + + serializer.DataField(ref uniqueMixes, "uniqueMixes", new List()); + serializer.DataField(ref tiles, "tiles", new Dictionary()); + } + } + + public IEnumerator GetEnumerator() + { + return _tiles.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + public void BurnTile(MapIndices gridIndices) + { + // TODO ATMOS + } + } +} diff --git a/Content.Server/GameObjects/Components/Atmos/MovedByPressureComponent.cs b/Content.Server/GameObjects/Components/Atmos/MovedByPressureComponent.cs new file mode 100644 index 0000000000..9e1e62f455 --- /dev/null +++ b/Content.Server/GameObjects/Components/Atmos/MovedByPressureComponent.cs @@ -0,0 +1,22 @@ +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization; + +namespace Content.Server.GameObjects.Components.Atmos +{ + [RegisterComponent] + public class MovedByPressureComponent : Component + { + public override string Name => "MovedByPressure"; + + public float PressureResistance { get; set; } = 1f; + public float MoveResist { get; set; } = 100f; + public int LastHighPressureMovementAirCycle { get; set; } = 0; + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + serializer.DataField(this, x => PressureResistance, "pressureResistance", 1f); + serializer.DataField(this, x => MoveResist, "moveResist", 100f); + } + } +} diff --git a/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs b/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs index dd232f2097..c00b3d0499 100644 --- a/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs +++ b/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using Content.Server.GameObjects.Components.Access; +using Content.Server.GameObjects.Components.Atmos; using Content.Server.GameObjects.Components.GUI; using Content.Server.GameObjects.Components.Mobs; using Content.Server.Interfaces.GameObjects; @@ -41,6 +42,7 @@ namespace Content.Server.GameObjects protected const float AutoCloseDelay = 5; protected float CloseSpeed = AutoCloseDelay; + private AirtightComponent airtightComponent; private ICollidableComponent _collidableComponent; private AppearanceComponent _appearance; private CancellationTokenSource _cancellationTokenSource; @@ -69,6 +71,7 @@ namespace Content.Server.GameObjects { base.Initialize(); + airtightComponent = Owner.GetComponent(); _collidableComponent = Owner.GetComponent(); _appearance = Owner.GetComponent(); _cancellationTokenSource = new CancellationTokenSource(); @@ -176,6 +179,7 @@ namespace Content.Server.GameObjects Timer.Spawn(OpenTimeOne, async () => { + airtightComponent.AirBlocked = false; _collidableComponent.Hard = false; await Timer.Delay(OpenTimeTwo, _cancellationTokenSource.Token); @@ -272,6 +276,7 @@ namespace Content.Server.GameObjects CheckCrush(); } + airtightComponent.AirBlocked = true; _collidableComponent.Hard = true; await Timer.Delay(CloseTimeTwo, _cancellationTokenSource.Token); diff --git a/Content.Server/GameObjects/EntitySystems/AtmosphereSystem.cs b/Content.Server/GameObjects/EntitySystems/AtmosphereSystem.cs new file mode 100644 index 0000000000..80197df799 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/AtmosphereSystem.cs @@ -0,0 +1,68 @@ +#nullable enable +using System; +using System.Collections.Generic; +using Content.Server.GameObjects.Components.Atmos; +using JetBrains.Annotations; +using Robust.Server.Interfaces.Timing; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Components.Map; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Map; +using Robust.Shared.Map; + +namespace Content.Server.GameObjects.EntitySystems +{ + [UsedImplicitly] + public class AtmosphereSystem : EntitySystem + { +#pragma warning disable 649 + [Robust.Shared.IoC.Dependency] private readonly IMapManager _mapManager = default!; + [Robust.Shared.IoC.Dependency] private readonly IEntityManager _entityManager = default!; + [Robust.Shared.IoC.Dependency] private readonly IPauseManager _pauseManager = default!; +#pragma warning restore 649 + + public override void Initialize() + { + base.Initialize(); + + _mapManager.TileChanged += OnTileChanged; + EntityQuery = new MultipleTypeEntityQuery(new List(){typeof(GridAtmosphereComponent)}); + } + + public GridAtmosphereComponent? GetGridAtmosphere(GridId gridId) + { + var grid = _mapManager.GetGrid(gridId); + var gridEnt = _entityManager.GetEntity(grid.GridEntityId); + return gridEnt.TryGetComponent(out GridAtmosphereComponent atmos) ? atmos : null; + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + foreach (var gridEnt in RelevantEntities) + { + var grid = gridEnt.GetComponent(); + if (_pauseManager.IsGridPaused(grid.GridIndex)) + continue; + + gridEnt.GetComponent().Update(frameTime); + } + } + + private void OnTileChanged(object? sender, TileChangedEventArgs eventArgs) + { + // When a tile changes, we want to update it only if it's gone from + // space -> not space or vice versa. So if the old tile is the + // same as the new tile in terms of space-ness, ignore the change + + if (eventArgs.NewTile.Tile.IsEmpty == eventArgs.OldTile.IsEmpty) + { + return; + } + + GetGridAtmosphere(eventArgs.NewTile.GridIndex)?.Invalidate(eventArgs.NewTile.GridIndices); + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/GasTileOverlaySystem.cs b/Content.Server/GameObjects/EntitySystems/GasTileOverlaySystem.cs new file mode 100644 index 0000000000..6fb9879bd7 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/GasTileOverlaySystem.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using Content.Server.Atmos; +using Content.Server.GameObjects.Components.Atmos; +using Content.Shared.Atmos; +using Content.Shared.GameObjects.EntitySystems; +using JetBrains.Annotations; +using Robust.Server.Interfaces.Player; +using Robust.Server.Player; +using Robust.Shared.Enums; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Map; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.IoC; +using Robust.Shared.Map; +using Robust.Shared.Utility; + +namespace Content.Server.GameObjects.EntitySystems +{ + [UsedImplicitly] + public sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem + { + private int _tickTimer = 0; + private HashSet _queue = new HashSet(); + private Dictionary> _invalid = new Dictionary>(); + + private Dictionary> _overlay = + new Dictionary>(); + + [Robust.Shared.IoC.Dependency] private IPlayerManager _playerManager = default!; + + public override void Initialize() + { + base.Initialize(); + + _playerManager.PlayerStatusChanged += OnPlayerStatusChanged; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Invalidate(GridId gridIndex, MapIndices indices) + { + if (!_invalid.TryGetValue(gridIndex, out var set) || set == null) + { + set = new HashSet(); + _invalid.Add(gridIndex, set); + } + + set.Add(indices); + } + + public void SetTileOverlay(GridId gridIndex, MapIndices indices, GasData[] gasData, int fireState = 0, float fireTemperature = 0f) + { + if(!_overlay.TryGetValue(gridIndex, out var _)) + _overlay[gridIndex] = new Dictionary(); + + _overlay[gridIndex][indices] = new GasOverlayData(fireState, fireTemperature, gasData); + _queue.Add(GetData(gridIndex, indices)); + } + + private void OnPlayerStatusChanged(object sender, SessionStatusEventArgs e) + { + if (e.NewStatus != SessionStatus.InGame) return; + + RaiseNetworkEvent(new GasTileOverlayMessage(GetData(), true), e.Session.ConnectedClient); + } + + private GasTileOverlayData[] GetData() + { + var list = new List(); + + foreach (var (gridId, tiles) in _overlay) + { + foreach (var (indices, _) in tiles) + { + var data = GetData(gridId, indices); + if(data.Data.Gas.Length > 0) + list.Add(data); + } + } + + return list.ToArray(); + } + + private GasTileOverlayData GetData(GridId gridIndex, MapIndices indices) + { + return new GasTileOverlayData(gridIndex, indices, _overlay[gridIndex][indices]); + } + + private void Revalidate() + { + var mapMan = IoCManager.Resolve(); + var entityMan = IoCManager.Resolve(); + var list = new List(); + + foreach (var (gridId, indices) in _invalid) + { + if (!mapMan.GridExists(gridId)) + { + _invalid.Remove(gridId); + return; + } + var grid = entityMan.GetEntity(mapMan.GetGrid(gridId).GridEntityId); + if (!grid.TryGetComponent(out GridAtmosphereComponent gam)) continue; + + foreach (var index in indices) + { + var tile = gam.GetTile(index); + + if (tile?.Air == null) continue; + + list.Clear(); + + for(var i = 0; i < Atmospherics.TotalNumberOfGases; i++) + { + var gas = Atmospherics.GetGas(i); + var overlay = gas.GasOverlay; + if (overlay == null) continue; + var moles = tile.Air.Gases[i]; + if(moles == 0f || moles < gas.GasMolesVisible) continue; + list.Add(new GasData(i, MathF.Max(MathF.Min(1, moles / gas.GasMolesVisibleMax), 0f))); + } + + if (list.Count == 0) continue; + + SetTileOverlay(gridId, index, list.ToArray(), tile.Hotspot.State, tile.Hotspot.Temperature); + } + + indices.Clear(); + } + } + + public override void Update(float frameTime) + { + _tickTimer++; + + Revalidate(); + + if (_tickTimer < 10) return; + + _tickTimer = 0; + if(_queue.Count > 0) + RaiseNetworkEvent(new GasTileOverlayMessage(_queue.ToArray())); + _queue.Clear(); + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/InstrumentSystem.cs b/Content.Server/GameObjects/EntitySystems/InstrumentSystem.cs index 61e0b598a4..a64f1fec01 100644 --- a/Content.Server/GameObjects/EntitySystems/InstrumentSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/InstrumentSystem.cs @@ -2,7 +2,7 @@ using Content.Server.GameObjects.Components.Instruments; using Robust.Shared.GameObjects; using Robust.Shared.GameObjects.Systems; -namespace Content.Server.Interfaces.GameObjects.Components.Interaction +namespace Content.Server.GameObjects.EntitySystems { public class InstrumentSystem : EntitySystem { diff --git a/Content.Server/IgnoredComponents.cs b/Content.Server/IgnoredComponents.cs index 35725d7466..f40975a84c 100644 --- a/Content.Server/IgnoredComponents.cs +++ b/Content.Server/IgnoredComponents.cs @@ -19,6 +19,7 @@ "Marker", "EmergencyLight", "Clickable", + "CanSeeGases", }; } diff --git a/Content.Server/Interfaces/IGasMixtureHolder.cs b/Content.Server/Interfaces/IGasMixtureHolder.cs new file mode 100644 index 0000000000..12f42a214f --- /dev/null +++ b/Content.Server/Interfaces/IGasMixtureHolder.cs @@ -0,0 +1,11 @@ +using Content.Server.Atmos; + +namespace Content.Server.Interfaces +{ + public interface IGasMixtureHolder + { + public GasMixture Air { get; set; } + + bool AssumeAir(GasMixture giver); + } +} diff --git a/Content.Server/Interfaces/IGasReactionEffect.cs b/Content.Server/Interfaces/IGasReactionEffect.cs new file mode 100644 index 0000000000..27958cc3cc --- /dev/null +++ b/Content.Server/Interfaces/IGasReactionEffect.cs @@ -0,0 +1,13 @@ +#nullable enable +using Content.Server.Atmos; +using Content.Server.Atmos.Reactions; +using Robust.Shared.Interfaces.Serialization; +using Robust.Shared.Map; + +namespace Content.Server.Interfaces +{ + public interface IGasReactionEffect : IExposeData + { + ReactionResult React(GasMixture mixture, IGasMixtureHolder? holder); + } +} diff --git a/Content.Server/Observer/Ghost.cs b/Content.Server/Observer/Ghost.cs index ce2dda23c3..a2a2dda05f 100644 --- a/Content.Server/Observer/Ghost.cs +++ b/Content.Server/Observer/Ghost.cs @@ -1,12 +1,18 @@ -using Content.Server.GameObjects; +using Content.Server.GameObjects; using Content.Server.GameObjects.Components.Observer; +using Content.Server.GameObjects.EntitySystems; using Content.Server.Interfaces.GameTicking; using Content.Server.Players; +using Content.Shared.Atmos; using Content.Shared.GameObjects; using Robust.Server.Interfaces.Console; +using Robust.Server.Interfaces.Placement; using Robust.Server.Interfaces.Player; +using Robust.Shared.GameObjects.Systems; using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Map; using Robust.Shared.IoC; +using Robust.Shared.Map; namespace Content.Server.Observer { @@ -39,6 +45,8 @@ namespace Content.Server.Observer var position = player.AttachedEntity?.Transform.GridPosition ?? IoCManager.Resolve().GetObserverSpawnPoint(); + + if (canReturn && player.AttachedEntity.TryGetComponent(out SpeciesComponent species)) { switch (species.CurrentDamageState) diff --git a/Content.Server/ServerContentIoC.cs b/Content.Server/ServerContentIoC.cs index e61f5ba4a7..57d1ab0054 100644 --- a/Content.Server/ServerContentIoC.cs +++ b/Content.Server/ServerContentIoC.cs @@ -1,4 +1,4 @@ -using Content.Server.AI.Utility.Considerations; +using Content.Server.AI.Utility.Considerations; using Content.Server.AI.WorldState; using Content.Server.Cargo; using Content.Server.Chat; diff --git a/Content.Shared/Atmos/Atmospherics.cs b/Content.Shared/Atmos/Atmospherics.cs new file mode 100644 index 0000000000..74d1db086e --- /dev/null +++ b/Content.Shared/Atmos/Atmospherics.cs @@ -0,0 +1,174 @@ +using System.Collections.Generic; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Atmos +{ + /// + /// Class to store atmos constants. + /// + public static class Atmospherics + { + static Atmospherics() + { + var protoMan = IoCManager.Resolve(); + + GasPrototypes = new GasPrototype[TotalNumberOfGases]; + + for (var i = 0; i < TotalNumberOfGases; i++) + { + GasPrototypes[i] = protoMan.Index(i.ToString()); + } + } + + private static readonly GasPrototype[] GasPrototypes; + + public static GasPrototype GetGas(int gasId) => GasPrototypes[gasId]; + public static GasPrototype GetGas(Gas gasId) => GasPrototypes[(int) gasId]; + public static IEnumerable Gases => GasPrototypes; + + #region ATMOS + /// + /// The universal gas constant, in kPa*L/(K*mol) + /// + public const float R = 8.314462618f; + + /// + /// 1 ATM in kPA. + /// + public const float OneAtmosphere = 101.325f; + + /// + /// -270.3ºC in K. CMB stands for Cosmic Microwave Background. + /// + public const float TCMB = 2.7f; + + /// + /// 0ºC in K + /// + public const float T0C = 273.15f; + + /// + /// 20ºC in K + /// + public const float T20C = 293.15f; + + /// + /// Liters in a cell. + /// + public const float CellVolume = 2500f; + + /// + /// Moles in a 2.5 m^3 cell at 101.325 Pa and 20ºC + /// + public const float MolesCellStandard = (OneAtmosphere * CellVolume / (T20C * R)); + + #endregion + + /// + /// Visible moles multiplied by this factor to get moles at which gas is at max visibility. + /// + public const float FactorGasVisibleMax = 20f; + + /// + /// Minimum number of moles a gas can have. + /// + public const float GasMinMoles = 0.00000005f; + + public const float OpenHeatTransferCoefficient = 0.4f; + + /// + /// Ratio of air that must move to/from a tile to reset group processing + /// + public const float MinimumAirRatioToSuspend = 0.1f; + + /// + /// Minimum ratio of air that must move to/from a tile + /// + public const float MinimumAirRatioToMove = 0.001f; + + /// + /// Minimum amount of air that has to move before a group processing can be suspended + /// + public const float MinimumAirToSuspend = (MolesCellStandard * MinimumAirRatioToSuspend); + + public const float MinimumTemperatureToMove = (T20C + 100f); + + public const float MinimumMolesDeltaToMove = (MolesCellStandard * MinimumAirRatioToMove); + + /// + /// Minimum temperature difference before group processing is suspended + /// + public const float MinimumTemperatureDeltaToSuspend = 4.0f; + + /// + /// Minimum temperature difference before the gas temperatures are just set to be equal. + /// + public const float MinimumTemperatureDeltaToConsider = 0.5f; + + /// + /// Minimum temperature for starting superconduction. + /// + public const float MinimumTemperatureStartSuperConduction = (T20C + 200f); + + /// + /// Minimum heat capacity. + /// + public const float MinimumHeatCapacity = 0.0003f; + + #region Excited Groups + + /// + /// Number of full atmos updates ticks before an excited group breaks down (averages gas contents across turfs) + /// + public const int ExcitedGroupBreakdownCycles = 4; + + /// + /// Number of full atmos updates before an excited group dismantles and removes its turfs from active + /// + public const int ExcitedGroupsDismantleCycles = 16; + + #endregion + + /// + /// Hard limit for tile equalization. + /// + public const int ZumosHardTileLimit = 2000; + + /// + /// Limit for zone-based tile equalization. + /// + public const int ZumosTileLimit = 200; + + /// + /// Total number of gases. Increase this if you want to add more! + /// + public const int TotalNumberOfGases = 5; + + public const float FireMinimumTemperatureToExist = T0C + 100f; + public const float FireMinimumTemperatureToSpread = T0C + 150f; + public const float FireSpreadRadiosityScale = 0.85f; + public const float FirePhoronEnergyReleased = 3000000f; + public const float FireGrowthRate = 40000f; + + public const float SuperSaturationThreshold = 96f; + + public const float OxygenBurnRateBase = 1.4f; + public const float PhoronMinimumBurnTemperature = (100f+T0C); + public const float PhoronUpperTemperature = (1370f+T0C); + public const float PhoronOxygenFullburn = 10f; + public const float PhoronBurnRateDelta = 9f; + } + + /// + /// Gases to Ids. Keep these updated with the prototypes! + /// + public enum Gas + { + Oxygen = 0, + Nitrogen = 1, + CarbonDioxide = 2, + Phoron = 3, + Tritium = 4, + } +} diff --git a/Content.Shared/Atmos/GasPrototype.cs b/Content.Shared/Atmos/GasPrototype.cs new file mode 100644 index 0000000000..95aed54219 --- /dev/null +++ b/Content.Shared/Atmos/GasPrototype.cs @@ -0,0 +1,86 @@ +using Robust.Shared.Interfaces.Serialization; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using System; +using Robust.Shared.Utility; +using YamlDotNet.RepresentationModel; + +namespace Content.Shared.Atmos +{ + [Prototype("gas")] + public class GasPrototype : IPrototype, IIndexedPrototype + { + public string Name { get; private set; } + + // TODO: Control gas amount necessary for overlay to appear + // TODO: Add interfaces for gas behaviours e.g. breathing, burning + + public string ID { get; private set; } + + /// + /// Specific heat for gas. + /// + public float SpecificHeat { get; private set; } + + /// + /// Minimum amount of moles for this gas to be visible. + /// + public float GasMolesVisible { get; private set; } + + /// + /// Visibility for this gas will be max after this value. + /// + public float GasMolesVisibleMax => GasMolesVisible * Atmospherics.FactorGasVisibleMax; + + /// + /// If this reagent is in gas form, this is the path to the overlay that will be used to make the gas visible. + /// + public string GasOverlayTexture { get; private set; } + + /// + /// If this reagent is in gas form, this will be the path to the RSI sprite that will be used to make the gas visible. + /// + public string GasOverlayState { get; set; } + + /// + /// State for the gas RSI overlay. + /// + public string GasOverlaySprite { get; set; } + + /// + /// Sprite specifier for the gas overlay. + /// + public SpriteSpecifier GasOverlay + { + get + { + if(string.IsNullOrEmpty(GasOverlaySprite) && !string.IsNullOrEmpty(GasOverlayTexture)) + return new SpriteSpecifier.Texture(new ResourcePath(GasOverlayTexture)); + + if(!string.IsNullOrEmpty(GasOverlaySprite) && !string.IsNullOrEmpty(GasOverlayState)) + return new SpriteSpecifier.Rsi(new ResourcePath(GasOverlaySprite), GasOverlayState); + + return null; + } + } + + /// + /// Path to the tile overlay used when this gas appears visible. + /// + public string OverlayPath { get; private set; } + + public void LoadFrom(YamlMappingNode mapping) + { + var serializer = YamlObjectSerializer.NewReader(mapping); + + serializer.DataField(this, x => ID, "id", string.Empty); + serializer.DataField(this, x => Name, "name", string.Empty); + serializer.DataField(this, x => OverlayPath, "overlayPath", string.Empty); + serializer.DataField(this, x => SpecificHeat, "specificHeat", 0f); + serializer.DataField(this, x => GasMolesVisible, "gasMolesVisible", 0.25f); + serializer.DataField(this, x => GasOverlayTexture, "gasOverlayTexture", string.Empty); + serializer.DataField(this, x => GasOverlaySprite, "gasOverlaySprite", string.Empty); + serializer.DataField(this, x => GasOverlayState, "gasOverlayState", string.Empty); + } + } +} diff --git a/Content.Shared/Chemistry/ReagentPrototype.cs b/Content.Shared/Chemistry/ReagentPrototype.cs index 2840ca4855..1280c7781e 100644 --- a/Content.Shared/Chemistry/ReagentPrototype.cs +++ b/Content.Shared/Chemistry/ReagentPrototype.cs @@ -5,6 +5,7 @@ using Robust.Shared.IoC; using Robust.Shared.Maths; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; +using Robust.Shared.Utility; using YamlDotNet.RepresentationModel; namespace Content.Shared.Chemistry @@ -12,6 +13,8 @@ namespace Content.Shared.Chemistry [Prototype("reagent")] public class ReagentPrototype : IPrototype, IIndexedPrototype { + private const float CelsiusToKelvin = 273.15f; + #pragma warning disable 649 [Dependency] private readonly IModuleManager _moduleManager; #pragma warning restore 649 @@ -29,7 +32,6 @@ namespace Content.Shared.Chemistry public Color SubstanceColor => _substanceColor; //List of metabolism effects this reagent has, should really only be used server-side. public List Metabolism => _metabolism; - public string SpriteReplacementPath => _spritePath; public ReagentPrototype() diff --git a/Content.Shared/GameObjects/EntitySystems/SharedGasTileOverlaySystem.cs b/Content.Shared/GameObjects/EntitySystems/SharedGasTileOverlaySystem.cs new file mode 100644 index 0000000000..111f81f10d --- /dev/null +++ b/Content.Shared/GameObjects/EntitySystems/SharedGasTileOverlaySystem.cs @@ -0,0 +1,73 @@ +using System; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.EntitySystems +{ + public abstract class SharedGasTileOverlaySystem : EntitySystem + { + [Serializable, NetSerializable] + public struct GasData + { + public int Index { get; set; } + public float Opacity { get; set; } + + public GasData(int gasId, float opacity) + { + Index = gasId; + Opacity = opacity; + } + } + + [Serializable, NetSerializable] + public readonly struct GasOverlayData + { + public readonly int FireState; + public readonly float FireTemperature; + public readonly GasData[] Gas; + + public GasOverlayData(int fireState, float fireTemperature, GasData[] gas) + { + FireState = fireState; + FireTemperature = fireTemperature; + Gas = gas; + } + } + + [Serializable, NetSerializable] + public readonly struct GasTileOverlayData + { + public readonly GridId GridIndex; + public readonly MapIndices GridIndices; + public readonly GasOverlayData Data; + + public GasTileOverlayData(GridId gridIndex, MapIndices gridIndices, GasOverlayData data) + { + GridIndex = gridIndex; + GridIndices = gridIndices; + Data = data; + } + + public override int GetHashCode() + { + return GridIndex.GetHashCode() ^ GridIndices.GetHashCode() ^ Data.GetHashCode(); + } + } + + [Serializable, NetSerializable] + public class GasTileOverlayMessage : EntitySystemMessage + { + public GasTileOverlayData[] OverlayData { get; } + public bool ClearAllOtherOverlays { get; } + + public GasTileOverlayMessage(GasTileOverlayData[] overlayData, bool clearAllOtherOverlays = false) + { + OverlayData = overlayData; + ClearAllOtherOverlays = clearAllOtherOverlays; + } + } + } +} diff --git a/Resources/Audio/Effects/space_wind.ogg b/Resources/Audio/Effects/space_wind.ogg new file mode 100644 index 0000000000..3709cdb055 Binary files /dev/null and b/Resources/Audio/Effects/space_wind.ogg differ diff --git a/Resources/Groups/groups.yml b/Resources/Groups/groups.yml index bdfa5ec6c0..fc07a7092a 100644 --- a/Resources/Groups/groups.yml +++ b/Resources/Groups/groups.yml @@ -171,6 +171,12 @@ - anchor - unanchor - tubeconnections + - addatmos + - addgas + - fillgas + - listgases + - removegas + - settemp CanViewVar: true CanAdminPlace: true CanScript: true diff --git a/Resources/Prototypes/Atmospherics/gases.yml b/Resources/Prototypes/Atmospherics/gases.yml new file mode 100644 index 0000000000..b6ec483c1b --- /dev/null +++ b/Resources/Prototypes/Atmospherics/gases.yml @@ -0,0 +1,28 @@ +- type: gas + id: 0 + name: Oxygen + specificHeat: 20 + +- type: gas + id: 1 + name: Nitrogen + specificHeat: 30 + +- type: gas + id: 2 + name: Carbon Dioxide + specificHeat: 30 + +- type: gas + id: 3 + name: Phoron + specificHeat: 200 + gasOverlaySprite: /Textures/Effects/atmospherics.rsi + gasOverlayState: phoron + +- type: gas + id: 4 + name: Tritium + specificHeat: 10 + gasOverlaySprite: /Textures/Effects/atmospherics.rsi + gasOverlayState: tritium diff --git a/Resources/Prototypes/Atmospherics/reactions.yml b/Resources/Prototypes/Atmospherics/reactions.yml new file mode 100644 index 0000000000..9b28728a42 --- /dev/null +++ b/Resources/Prototypes/Atmospherics/reactions.yml @@ -0,0 +1,11 @@ +- type: gasReaction + id: PhoronFire + priority: -2 + minimumTemperature: 373.149 # Same as Atmospherics.FireMinimumTemperatureToExist + minimumRequirements: # In this case, same as minimum mole count. + - 0.01 # oxygen + - 0 # nitrogen + - 0 # carbon dioxide + - 0.01 # phoron + effects: + - !type:PhoronFireReaction {} diff --git a/Resources/Prototypes/Entities/Constructible/Doors/airlock_base.yml b/Resources/Prototypes/Entities/Constructible/Doors/airlock_base.yml index 1e509147e0..cb2ae3a10e 100644 --- a/Resources/Prototypes/Entities/Constructible/Doors/airlock_base.yml +++ b/Resources/Prototypes/Entities/Constructible/Doors/airlock_base.yml @@ -51,6 +51,8 @@ interfaces: - key: enum.WiresUiKey.Key type: WiresBoundUserInterface + - type: Airtight + adjacentAtmosphere: true - type: Occluder - type: SnapGrid offset: Center diff --git a/Resources/Prototypes/Entities/Constructible/Storage/Closets/closet.yml b/Resources/Prototypes/Entities/Constructible/Storage/Closets/closet.yml index bb2500721e..45ae98fcca 100644 --- a/Resources/Prototypes/Entities/Constructible/Storage/Closets/closet.yml +++ b/Resources/Prototypes/Entities/Constructible/Storage/Closets/closet.yml @@ -17,6 +17,7 @@ sprite: Constructible/Structures/closet.rsi state: generic_door - type: Clickable + - type: MovedByPressure - type: InteractionOutline - type: Collidable shapes: diff --git a/Resources/Prototypes/Entities/Constructible/Walls/walls.yml b/Resources/Prototypes/Entities/Constructible/Walls/walls.yml index fb97f7198f..2ba3947118 100644 --- a/Resources/Prototypes/Entities/Constructible/Walls/walls.yml +++ b/Resources/Prototypes/Entities/Constructible/Walls/walls.yml @@ -33,6 +33,8 @@ sizeY: 32 - type: SnapGrid offset: Center + - type: Airtight + - type: IconSmooth key: walls base: solid diff --git a/Resources/Prototypes/Entities/Constructible/Walls/windows.yml b/Resources/Prototypes/Entities/Constructible/Walls/windows.yml index 6b3be3d922..bcecaf60cf 100644 --- a/Resources/Prototypes/Entities/Constructible/Walls/windows.yml +++ b/Resources/Prototypes/Entities/Constructible/Walls/windows.yml @@ -31,6 +31,7 @@ thresholdvalue: 100 - type: SnapGrid offset: Center + - type: Airtight - type: Window base: window diff --git a/Resources/Prototypes/Entities/Mobs/Player/observer.yml b/Resources/Prototypes/Entities/Mobs/Player/observer.yml index dccd2699a6..eba5ba58d4 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/observer.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/observer.yml @@ -16,6 +16,7 @@ DoRangeCheck: false - type: IgnorePause - type: Ghost + - type: CanSeeGases - type: Sprite netsync: false drawdepth: Ghosts diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml index 2e2989ee36..eab5362bdd 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml @@ -11,6 +11,7 @@ - type: Flashable - type: Hands - type: MovementSpeedModifier + - type: MovedByPressure - type: Hunger - type: Thirst # Organs @@ -139,6 +140,7 @@ - type: Grammar proper: true - type: Pullable + - type: CanSeeGases - type: entity save: false diff --git a/Resources/Prototypes/Entities/item_base.yml b/Resources/Prototypes/Entities/item_base.yml index 014c2c445d..160dfad548 100644 --- a/Resources/Prototypes/Entities/item_base.yml +++ b/Resources/Prototypes/Entities/item_base.yml @@ -7,6 +7,7 @@ size: 5 - type: Clickable - type: InteractionOutline + - type: MovedByPressure - type: Collidable shapes: - !type:PhysShapeAabb diff --git a/Resources/Prototypes/Reagents/chemicals.yml b/Resources/Prototypes/Reagents/chemicals.yml index 0f6421018e..482f42721c 100644 --- a/Resources/Prototypes/Reagents/chemicals.yml +++ b/Resources/Prototypes/Reagents/chemicals.yml @@ -35,8 +35,8 @@ boilingPoint: 100.0 - type: reagent - id: chem.Plasma - name: plasma + id: chem.Phoron + name: phoron desc: Funky, space-magic pixie dust. You probably shouldn't eat this, but we both know you will anyways. color: "#500064" boilingPoint: -127.3 # Random values picked between the actual values for CO2 and O2 diff --git a/Resources/Prototypes/Reagents/elements.yml b/Resources/Prototypes/Reagents/elements.yml index 13fa9d1d5c..50900bea4b 100644 --- a/Resources/Prototypes/Reagents/elements.yml +++ b/Resources/Prototypes/Reagents/elements.yml @@ -13,6 +13,7 @@ color: "#808080" boilingPoint: -183.0 meltingPoint: -218.4 + gasOverlayTexture: /Textures/Effects/Gas/deleteme.png - type: reagent id: chem.S diff --git a/Resources/Textures/Effects/atmospherics.rsi/chem_gas_old.png b/Resources/Textures/Effects/atmospherics.rsi/chem_gas_old.png new file mode 100644 index 0000000000..11a6ef1a58 Binary files /dev/null and b/Resources/Textures/Effects/atmospherics.rsi/chem_gas_old.png differ diff --git a/Resources/Textures/Effects/atmospherics.rsi/freon.png b/Resources/Textures/Effects/atmospherics.rsi/freon.png new file mode 100644 index 0000000000..405f61a49a Binary files /dev/null and b/Resources/Textures/Effects/atmospherics.rsi/freon.png differ diff --git a/Resources/Textures/Effects/atmospherics.rsi/freon_old.png b/Resources/Textures/Effects/atmospherics.rsi/freon_old.png new file mode 100644 index 0000000000..597212a36b Binary files /dev/null and b/Resources/Textures/Effects/atmospherics.rsi/freon_old.png differ diff --git a/Resources/Textures/Effects/atmospherics.rsi/fusion_gas.png b/Resources/Textures/Effects/atmospherics.rsi/fusion_gas.png new file mode 100644 index 0000000000..11a6ef1a58 Binary files /dev/null and b/Resources/Textures/Effects/atmospherics.rsi/fusion_gas.png differ diff --git a/Resources/Textures/Effects/atmospherics.rsi/meta.json b/Resources/Textures/Effects/atmospherics.rsi/meta.json new file mode 100644 index 0000000000..a03db15269 --- /dev/null +++ b/Resources/Textures/Effects/atmospherics.rsi/meta.json @@ -0,0 +1 @@ +{"version": 1, "size": {"x": 32, "y": 32}, "license": "CC-BY-SA 3.0", "copyright": "Taken from https://github.com/tgstation/tgstation at 04e43d8c1d5097fdb697addd4395fb849dd341bd", "states": [{"name": "chem_gas_old", "directions": 1, "delays": [[0.1, 0.1, 0.1, 0.1, 0.1, 0.1]]}, {"name": "freon", "directions": 1, "delays": [[0.2, 0.2, 0.2, 0.3, 0.3, 0.2, 0.2, 0.2]]}, {"name": "freon_old", "directions": 1, "delays": [[0.1, 0.1, 0.1, 0.1, 0.1, 0.1]]}, {"name": "fusion_gas", "directions": 1, "delays": [[0.1, 0.1, 0.1, 0.1, 0.1, 0.1]]}, {"name": "miasma", "directions": 1, "delays": [[0.2, 0.2, 0.2, 0.3, 0.3, 0.2, 0.2, 0.2]]}, {"name": "miasma_old", "directions": 1, "delays": [[0.22999999999999998, 0.22999999999999998, 0.22999999999999998, 0.22999999999999998, 0.22999999999999998, 0.22999999999999998]]}, {"name": "nitrous_oxide", "directions": 1, "delays": [[0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]]}, {"name": "nitrous_oxide_old", "directions": 1, "delays": [[0.2, 0.2, 0.2]]}, {"name": "nitryl", "directions": 1, "delays": [[0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]]}, {"name": "nitryl_old", "directions": 1, "delays": [[0.1, 0.1, 0.1, 0.1]]}, {"name": "phoron", "directions": 1, "delays": [[0.1, 0.1, 0.1]]}, {"name": "phoron_old", "directions": 1, "delays": [[0.2, 0.2, 0.2]]}, {"name": "tritium", "directions": 1, "delays": [[0.1, 0.1, 0.1, 0.1, 0.1]]}, {"name": "tritium_old", "directions": 1, "delays": [[0.1, 0.1, 0.1, 0.1, 0.1]]}, {"name": "water_vapor", "directions": 1, "delays": [[0.2, 0.2, 0.2, 0.3, 0.3, 0.2, 0.2, 0.2]]}, {"name": "water_vapor_old", "directions": 1, "delays": [[0.2, 0.2, 0.2, 0.2, 0.2, 0.2]]}]} diff --git a/Resources/Textures/Effects/atmospherics.rsi/miasma.png b/Resources/Textures/Effects/atmospherics.rsi/miasma.png new file mode 100644 index 0000000000..4ad7f19298 Binary files /dev/null and b/Resources/Textures/Effects/atmospherics.rsi/miasma.png differ diff --git a/Resources/Textures/Effects/atmospherics.rsi/miasma_old.png b/Resources/Textures/Effects/atmospherics.rsi/miasma_old.png new file mode 100644 index 0000000000..bc2d922ac4 Binary files /dev/null and b/Resources/Textures/Effects/atmospherics.rsi/miasma_old.png differ diff --git a/Resources/Textures/Effects/atmospherics.rsi/nitrous_oxide.png b/Resources/Textures/Effects/atmospherics.rsi/nitrous_oxide.png new file mode 100644 index 0000000000..cd092091a8 Binary files /dev/null and b/Resources/Textures/Effects/atmospherics.rsi/nitrous_oxide.png differ diff --git a/Resources/Textures/Effects/atmospherics.rsi/nitrous_oxide_old.png b/Resources/Textures/Effects/atmospherics.rsi/nitrous_oxide_old.png new file mode 100644 index 0000000000..fd03927304 Binary files /dev/null and b/Resources/Textures/Effects/atmospherics.rsi/nitrous_oxide_old.png differ diff --git a/Resources/Textures/Effects/atmospherics.rsi/nitryl.png b/Resources/Textures/Effects/atmospherics.rsi/nitryl.png new file mode 100644 index 0000000000..4fa5e1d231 Binary files /dev/null and b/Resources/Textures/Effects/atmospherics.rsi/nitryl.png differ diff --git a/Resources/Textures/Effects/atmospherics.rsi/nitryl_old.png b/Resources/Textures/Effects/atmospherics.rsi/nitryl_old.png new file mode 100644 index 0000000000..454edb859c Binary files /dev/null and b/Resources/Textures/Effects/atmospherics.rsi/nitryl_old.png differ diff --git a/Resources/Textures/Effects/atmospherics.rsi/phoron.png b/Resources/Textures/Effects/atmospherics.rsi/phoron.png new file mode 100644 index 0000000000..f4c568e322 Binary files /dev/null and b/Resources/Textures/Effects/atmospherics.rsi/phoron.png differ diff --git a/Resources/Textures/Effects/atmospherics.rsi/phoron_old.png b/Resources/Textures/Effects/atmospherics.rsi/phoron_old.png new file mode 100644 index 0000000000..24fe050f29 Binary files /dev/null and b/Resources/Textures/Effects/atmospherics.rsi/phoron_old.png differ diff --git a/Resources/Textures/Effects/atmospherics.rsi/tritium.png b/Resources/Textures/Effects/atmospherics.rsi/tritium.png new file mode 100644 index 0000000000..bc70da3d9b Binary files /dev/null and b/Resources/Textures/Effects/atmospherics.rsi/tritium.png differ diff --git a/Resources/Textures/Effects/atmospherics.rsi/tritium_old.png b/Resources/Textures/Effects/atmospherics.rsi/tritium_old.png new file mode 100644 index 0000000000..d49a319ff9 Binary files /dev/null and b/Resources/Textures/Effects/atmospherics.rsi/tritium_old.png differ diff --git a/Resources/Textures/Effects/atmospherics.rsi/water_vapor.png b/Resources/Textures/Effects/atmospherics.rsi/water_vapor.png new file mode 100644 index 0000000000..17cde3e811 Binary files /dev/null and b/Resources/Textures/Effects/atmospherics.rsi/water_vapor.png differ diff --git a/Resources/Textures/Effects/atmospherics.rsi/water_vapor_old.png b/Resources/Textures/Effects/atmospherics.rsi/water_vapor_old.png new file mode 100644 index 0000000000..14ea9b6847 Binary files /dev/null and b/Resources/Textures/Effects/atmospherics.rsi/water_vapor_old.png differ diff --git a/Resources/Textures/Effects/fire.rsi/1.png b/Resources/Textures/Effects/fire.rsi/1.png new file mode 100644 index 0000000000..acdd06c95b Binary files /dev/null and b/Resources/Textures/Effects/fire.rsi/1.png differ diff --git a/Resources/Textures/Effects/fire.rsi/2.png b/Resources/Textures/Effects/fire.rsi/2.png new file mode 100644 index 0000000000..9564bdb611 Binary files /dev/null and b/Resources/Textures/Effects/fire.rsi/2.png differ diff --git a/Resources/Textures/Effects/fire.rsi/3.png b/Resources/Textures/Effects/fire.rsi/3.png new file mode 100644 index 0000000000..333bbf3cdd Binary files /dev/null and b/Resources/Textures/Effects/fire.rsi/3.png differ diff --git a/Resources/Textures/Effects/fire.rsi/fire.png b/Resources/Textures/Effects/fire.rsi/fire.png new file mode 100644 index 0000000000..78bb9be625 Binary files /dev/null and b/Resources/Textures/Effects/fire.rsi/fire.png differ diff --git a/Resources/Textures/Effects/fire.rsi/meta.json b/Resources/Textures/Effects/fire.rsi/meta.json new file mode 100644 index 0000000000..016582890e --- /dev/null +++ b/Resources/Textures/Effects/fire.rsi/meta.json @@ -0,0 +1 @@ +{"version": 1, "size": {"x": 32, "y": 32}, "license": "CC-BY-SA 3.0", "copyright": "Taken from https://github.com/tgstation/tgstation at 04e43d8c1d5097fdb697addd4395fb849dd341bd", "states": [{"name": "1", "directions": 4, "delays": [[0.1, 0.2, 0.1, 0.1, 0.1, 0.1], [0.1, 0.2, 0.1, 0.1, 0.1, 0.1], [0.1, 0.2, 0.1, 0.1, 0.1, 0.1], [0.1, 0.2, 0.1, 0.1, 0.1, 0.1]]}, {"name": "2", "directions": 4, "delays": [[0.1, 0.2, 0.1, 0.2, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.2, 0.1, 0.2, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.2, 0.1, 0.2, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.2, 0.1, 0.2, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]]}, {"name": "3", "directions": 4, "delays": [[0.1, 0.2, 0.1, 0.2, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.2, 0.1, 0.2, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.2, 0.1, 0.2, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.2, 0.1, 0.2, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]]}, {"name": "fire", "directions": 1, "delays": [[0.1, 0.1, 0.1]]}, {"name": "overcharged", "directions": 1, "delays": [[0.1, 0.1, 5.0, 0.1, 0.1, 0.1, 2.0, 0.1, 4.0]]}]} \ No newline at end of file diff --git a/Resources/Textures/Effects/fire.rsi/overcharged.png b/Resources/Textures/Effects/fire.rsi/overcharged.png new file mode 100644 index 0000000000..035a1c7297 Binary files /dev/null and b/Resources/Textures/Effects/fire.rsi/overcharged.png differ diff --git a/RobustToolbox b/RobustToolbox index e21cf79ee4..e5ae2182b0 160000 --- a/RobustToolbox +++ b/RobustToolbox @@ -1 +1 @@ -Subproject commit e21cf79ee498a9648ff6625bb04e1631c483c60c +Subproject commit e5ae2182b0c31cc065c60b1ebc540dca0c73adf5 diff --git a/SpaceStation14.sln.DotSettings b/SpaceStation14.sln.DotSettings index a356ba2137..0d0c37af86 100644 --- a/SpaceStation14.sln.DotSettings +++ b/SpaceStation14.sln.DotSettings @@ -43,11 +43,13 @@ True <data /> <data><IncludeFilters /><ExcludeFilters><Filter ModuleMask="*.UnitTesting" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /></ExcludeFilters></data> + True True True <data /> <data><IncludeFilters /><ExcludeFilters><Filter ModuleMask="Lidgren.Network" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /></ExcludeFilters></data> True + True True True True @@ -59,6 +61,7 @@ True True True + True True True True @@ -70,7 +73,8 @@ True True True + True True - True - True + True + True \ No newline at end of file