Adds atmos (#1389)

* Add initial atmospherics framework

* Make walls and airlocks airtight

* Add the basic atmosphere gas system

* Atmos: move CardinalToIntVec and Offset into extention methods in EntityNetworkUtils

* Optimize vending machine code a bit.

* Address feedback from Atmos PR, make code compile

Fix use of OnMove -> SnapGridComponent.OnPositionChanged.
Added pause checking to atmos simulations.

* Improvements to the existing ZAS prototype (#996)

* Rename Volume -> Quantity in GasProperty

This makes the name consistent with the rest of the code, given that
it's meant to be a mol value.

* Replace Gas enum with GasPrototype

Unused as of yet, but laying the groundwork for future changes.

* Update AtmosphereMap, improving maths

Adds a temporary default atmosphere, hardcoded in for testing. It will
be replaced eventually.

Fixed a maths mistake in the original code involving unit conversions.

Added the Take() method to IAtmosphere, for taking some volume of
mixture out of a gas mix. This will be pretty handy in the future, but
for now it's used by walls to delete air where they are built.

* Fix merging, splitting logic for zones

Removing a cell from a zone now correctly reduces its volume.
Adding a cell to a zone now correctly increases its volume.

* Improvements to atmos code, reorganising of types

Moved GasPrototype to shared, because it's going to be used by the
client later.

Split up the atmosphere code. Now zones are explicitly their own kind of
atmosphere, which should speed up some loops and also solve
inconsistencies in the volume calculation.
GasMixtures are another type of atmosphere, for more general use.

Try to fix the splitting/merging code, which was quite cryptic and not
clear at all. It *should* work now.

* Switch zones back to MapIndices

Turns out I'm an idiot who misunderstood the code. MapIndices can be
used as one-per-tile, which is what is needed for atmos. GridCoordinates
are many-per-tile, and were causing lots of problems.

* Add zone debugging overlay

This is the first example of zone information being sent to a client.

It adds the `togglezoneinfo` command, which overlays the tiles and gas
information for the zone currently occupied by the user on the screen.
This was helpful for debugging the GridCoordinates problem.

* Fix position of atmos zone debug text

* Make AirtightComponent only activate on MapInit

This should stop it splitting atmospheres in mapping.

* Doc comments improvements to AtmosphereMap

Fix some malformed comments, inherit some useful docs, document some
more functions.

* Add zone logic for changing tiles to/from space

Zones are now correctly created when all the tiles in a room are built,
and destroyed when one of the tiles is destroyed.

* update engine

* right

* Cleanup code

* Port GasMixture, further cleanup

* Fix windows not being airtight, some other stuff

* Work on atmos

* Difference between ZoneBlocked and AirBlocked

* Big GridAtmosphereManager cleanup, zones are broken now oops

* Remove zones, add excited group implementation

* Further cleanup

* Further work on atmos

* Work on gas overlay.

* PumpGasTo and ReleaseGasTo methods for GasMixture.

* Adds Tile Overlay System.

* More work on atmos

* Gasses spread, equalize and all that

* Fix a few crashes

* Gas can actually spread from room to room after opening airlocks

* Add explosive depressurization, tile prying on depressurization, gas spreading on wall break. Etc.

* More work

* Remove atmoschem, add "performant" gas overlays

* what the fuck git

* More work I guess?

* Fix stuff, create a few (empty) components for future work

* Fix temperature

* Fix tile air sharing

* Atmos optimizations

* Further atmos optimizations

* Even more optimizations!

* Update Content.Client/GameObjects/EntitySystems/GasTileOverlaySystem.cs

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>

* Update Content.Client/GameObjects/EntitySystems/GasTileOverlaySystem.cs

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>

* Update Content.Client/GameObjects/EntitySystems/GasTileOverlaySystem.cs

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>

* Address a few reviews

* Oops, forgot to remove this

* Update Content.Server/Atmos/AtmosphereMap.cs

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>

* Fix compile

* Improved client gas tile overlays

* Less allocations

* Even less allocations!

* some stuff dunno

* Big refactor time, oh yeah

* it truly is 1 AM

* bruh

* No idea why now it doesn't work.
Oh well! I'll fix this on thursday 😌

* Basic atmos serialization

* Fix bugs, add VV attributes

* Start adding stuff for gas reactions

* Add a few atmos commands

* Fill gas command

* Changes to gas opacity

* Fixes I guess

* Fixes, add space wind sound

* High pressure movement!

* Better high pressure movements.

* Fix direction, maybe?

* And now it works great

* Science!

* and this is why you don't trust people

* remove logging

* Turns out I'm fucking dumb

* Work on hotspots and reactions, add tritium gas

* IGridAtmosphereComponent interface! For future unsimulated grids.

* Makes atmos updates NoGC.

* C E A S E

* Add settemp atmos command

* Important reminder.

* Remove useless AtmosCooldown.

* More work on hotspots

* Overlays for hotspots. Fire works!

* Turns out I suck at coding

* Fire texture change

* Yeah let's make that an absolute value, hmm?

* Support for atmos after teleporting grid (more or less)

* fix attempt (doesn't actually fix it)

* Make static variable not static

* Remove magic numbers

* Stopwatch moment

* Slight cleanup.

* More cleanup.

* Atmos optimizations

* Fix build

* Remove useless ghost atmos shit

* Adds CanSeeGases component for gas overlay stuff

* Component and prototype ignores

* ExcitedGroups now dispose on being merged with others

* Some tweaking.

* Atmos now uses frame time for updates.

* Nullable boogaloo

* IExamine fix

* Fix build

* Fix restartround

* Atmos tweaking, use invalid direction

* Increase high pressure movement force

* Better sort

* Update submodule.

* NULLABILITY AAAAH

Special thanks to monster860 and all monstermos contributors!

Co-authored-by: Campbell Suter <znix@znix.xyz>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
Co-authored-by: ComicIronic <comicironic@gmail.com>
Co-authored-by: silicons <2003111+silicons@users.noreply.github.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
This commit is contained in:
Víctor Aguilera Puerto
2020-08-02 20:08:20 +02:00
committed by GitHub
parent 15f31b654e
commit 85df48a700
76 changed files with 4066 additions and 11 deletions

View File

@@ -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<GasTileOverlaySystem>();
}
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);
}
}
}
}
}
}

View File

@@ -20,12 +20,12 @@ namespace Content.Client.Construction
}
/// <inheritdoc />
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;
}

View File

@@ -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();

View File

@@ -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;
}
}
}
}

View File

@@ -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<float, Color> _fireCache = new Dictionary<float, Color>();
// 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<GridId, Dictionary<MapIndices, GasOverlayData>> _overlay = new Dictionary<GridId, Dictionary<MapIndices, GasOverlayData>>();
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent(new EntityEventHandler<GasTileOverlayMessage>(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<RSIResource>(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<float>();
break;
case null:
_frames[i] = Array.Empty<Texture>();
_frameDelays[i] = Array.Empty<float>();
break;
}
}
var fire = _resourceCache.GetResource<RSIResource>(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<MapIndices, GasOverlayData>();
_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;
}
}
}
}

View File

@@ -149,6 +149,8 @@
"Conveyor",
"ConveyorSwitch",
"Flippable",
"Airtight",
"MovedByPressure",
};
}
}

View File

@@ -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 <GridId>";
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<IMapManager>();
if (!gridId.IsValid() || !mapMan.TryGetGrid(gridId, out var gridComp))
{
shell.SendText(player, "Invalid grid ID.");
return;
}
var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetEntity(gridComp.GridEntityId, out var grid))
{
shell.SendText(player, "Failed to get grid entity.");
return;
}
if (grid.HasComponent<GridAtmosphereComponent>())
{
shell.SendText(player, "Grid already has an atmosphere.");
return;
}
grid.AddComponent<GridAtmosphereComponent>();
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 <X> <Y> <GridId> <Gas> <moles>";
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<IMapManager>();
if (!gridId.IsValid() || !mapMan.TryGetGrid(gridId, out var gridComp))
{
shell.SendText(player, "Invalid grid ID.");
return;
}
var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetEntity(gridComp.GridEntityId, out var grid))
{
shell.SendText(player, "Failed to get grid entity.");
return;
}
if (!grid.HasComponent<GridAtmosphereComponent>())
{
shell.SendText(player, "Grid doesn't have an atmosphere.");
return;
}
var gam = grid.GetComponent<GridAtmosphereComponent>();
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 <GridId> <Gas> <moles>";
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<IMapManager>();
if (!gridId.IsValid() || !mapMan.TryGetGrid(gridId, out var gridComp))
{
shell.SendText(player, "Invalid grid ID.");
return;
}
var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetEntity(gridComp.GridEntityId, out var grid))
{
shell.SendText(player, "Failed to get grid entity.");
return;
}
if (!grid.HasComponent<GridAtmosphereComponent>())
{
shell.SendText(player, "Grid doesn't have an atmosphere.");
return;
}
var gam = grid.GetComponent<GridAtmosphereComponent>();
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 <X> <Y> <GridId> <amount> <ratio>\nIf <ratio> 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<IMapManager>();
if (!gridId.IsValid() || !mapMan.TryGetGrid(gridId, out var gridComp))
{
shell.SendText(player, "Invalid grid ID.");
return;
}
var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetEntity(gridComp.GridEntityId, out var grid))
{
shell.SendText(player, "Failed to get grid entity.");
return;
}
if (!grid.HasComponent<GridAtmosphereComponent>())
{
shell.SendText(player, "Grid doesn't have an atmosphere.");
return;
}
var gam = grid.GetComponent<GridAtmosphereComponent>();
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 <X> <Y> <GridId> <moles>";
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<IMapManager>();
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<IEntityManager>();
if (!entMan.TryGetEntity(gridComp.GridEntityId, out var grid))
{
shell.SendText(player, "Failed to get grid entity.");
return;
}
if (!grid.HasComponent<GridAtmosphereComponent>())
{
shell.SendText(player, "Grid doesn't have an atmosphere.");
return;
}
var gam = grid.GetComponent<GridAtmosphereComponent>();
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);
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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<TileAtmosphere> _tile = new HashSet<TileAtmosphere>();
[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;
}
}
}

View File

@@ -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
{
/// <summary>
/// A general-purpose, variable volume gas mixture.
/// </summary>
[Serializable]
public class GasMixture : IExposeData, IEquatable<GasMixture>, ICloneable
{
[ViewVariables]
private float[] _moles = new float[Atmospherics.TotalNumberOfGases];
[ViewVariables]
private float[] _molesArchived = new float[Atmospherics.TotalNumberOfGases];
private float _temperature = Atmospherics.TCMB;
public IReadOnlyList<float> 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;
}
/// <summary>
/// Pump gas from this mixture to the output mixture.
/// Amount depends on target pressure.
/// </summary>
/// <param name="outputAir">The mixture to pump the gas to</param>
/// <param name="targetPressure">The target pressure to reach</param>
/// <returns>Whether we could pump air to the output or not</returns>
[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;
}
/// <summary>
/// Releases gas from this mixture to the output mixture.
/// It can't transfer air to a mixture with higher pressure.
/// </summary>
/// <param name="outputAir"></param>
/// <param name="targetPressure"></param>
/// <returns></returns>
[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<IPrototypeManager>().EnumeratePrototypes<GasReactionPrototype>())
{
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,
};
}
}
}

View File

@@ -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<MovedByPressureComponent>();
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();
}
}
}
}

View File

@@ -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;
/// <summary>
/// State for the fire sprite.
/// </summary>
[ViewVariables]
public int State;
public void Start()
{
Valid = true;
State = 1;
}
}
}

View File

@@ -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<TileAtmosphere>
{
/// <summary>
/// Number of times <see cref="Update"/> has been called.
/// </summary>
int UpdateCounter { get; }
/// <summary>
/// How many tiles have high pressure delta.
/// </summary>
int HighPressureDeltaCount { get; }
/// <summary>
/// Control variable for equalization.
/// </summary>
long EqualizationQueueCycleControl { get; set; }
/// <summary>
/// Attemps to pry a tile.
/// </summary>
/// <param name="indices"></param>
void PryTile(MapIndices indices);
/// <summary>
/// Burns a tile.
/// </summary>
/// <param name="gridIndices"></param>
void BurnTile(MapIndices gridIndices);
/// <summary>
/// Invalidates a coordinate to be revalidated again.
/// Use this after changing a tile's gas contents, or when the tile becomes space, etc.
/// </summary>
/// <param name="indices"></param>
void Invalidate(MapIndices indices);
/// <summary>
/// Adds an active tile so it becomes processed every update until it becomes inactive.
/// Also makes the tile excited.
/// </summary>
/// <param name="tile"></param>
void AddActiveTile(TileAtmosphere tile);
/// <summary>
/// Removes an active tile and disposes of its <seealso cref="ExcitedGroup"/>.
/// Use with caution.
/// </summary>
/// <param name="tile"></param>
void RemoveActiveTile(TileAtmosphere tile);
/// <summary>
/// Marks a tile as having a hotspot so it can be processed.
/// </summary>
/// <param name="tile"></param>
void AddHotspotTile(TileAtmosphere tile);
/// <summary>
/// Removes a tile from the hotspot processing list.
/// </summary>
/// <param name="tile"></param>
void RemoveHotspotTile(TileAtmosphere tile);
/// <summary>
/// Marks a tile has having high pressure differences that need to be equalized.
/// </summary>
/// <param name="tile"></param>
void AddHighPressureDelta(TileAtmosphere tile);
/// <summary>
/// Returns whether the tile in question is marked as having high pressure differences or not.
/// </summary>
/// <param name="tile"></param>
/// <returns></returns>
bool HasHighPressureDelta(TileAtmosphere tile);
/// <summary>
/// Adds a excited group to be processed.
/// </summary>
/// <param name="excitedGroup"></param>
void AddExcitedGroup(ExcitedGroup excitedGroup);
/// <summary>
/// Removes an excited group.
/// </summary>
/// <param name="excitedGroup"></param>
void RemoveExcitedGroup(ExcitedGroup excitedGroup);
/// <summary>
/// Returns a tile.
/// </summary>
/// <param name="indices"></param>
/// <returns></returns>
TileAtmosphere GetTile(MapIndices indices);
/// <summary>
/// Returns a tile.
/// </summary>
/// <param name="coordinates"></param>
/// <returns></returns>
TileAtmosphere GetTile(GridCoordinates coordinates);
/// <summary>
/// Returns if the tile in question is air-blocked.
/// This could be due to a wall, an airlock, etc.
/// Also see AirtightComponent.
/// </summary>
/// <param name="indices"></param>
/// <returns></returns>
bool IsAirBlocked(MapIndices indices);
/// <summary>
/// Returns if the tile in question is space.
/// </summary>
/// <param name="indices"></param>
/// <returns></returns>
bool IsSpace(MapIndices indices);
/// <summary>
/// Returns the volume in liters for a number of cells/tiles.
/// </summary>
/// <param name="cellCount"></param>
/// <returns></returns>
float GetVolumeForCells(int cellCount);
void Update(float frameTime);
}
}

View File

@@ -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; }
/// <summary>
/// Minimum gas amount requirements.
/// </summary>
public float[] MinimumRequirements { get; private set; }
/// <summary>
/// Minimum temperature requirement.
/// </summary>
public float MinimumTemperatureRequirement { get; private set; }
/// <summary>
/// Minimum energy requirement.
/// </summary>
public float MinimumEnergyRequirement { get; private set; }
/// <summary>
/// Lower numbers are checked/react later than higher numbers.
/// If two reactions have the same priority, they may happen in either order.
/// </summary>
public int Priority { get; private set; }
/// <summary>
/// A list of effects this will produce.
/// </summary>
private List<IGasReactionEffect> _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<IGasReactionEffect>());
}
public ReactionResult React(GasMixture mixture, IGasMixtureHolder holder)
{
var result = ReactionResult.NoReaction;
foreach (var effect in _effects)
{
result |= effect.React(mixture, holder);
}
return result;
}
}
}

View File

@@ -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)
{
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<Direction, TileAtmosphere> _adjacentTiles = new Dictionary<Direction, TileAtmosphere>();
[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<AudioSystem>().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<HighPressureMovementController>();
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<TileAtmosphere>();
var takerTiles = new List<TileAtmosphere>();
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<TileAtmosphere>(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<TileAtmosphere>(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<Direction, float>();
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<TileAtmosphere>();
var spaceTiles = new List<TileAtmosphere>();
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<TileAtmosphere>();
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>();
_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
}
}
}

View File

@@ -18,6 +18,7 @@
<ItemGroup>
<ProjectReference Include="..\Content.Server.Database\Content.Server.Database.csproj" />
<ProjectReference Include="..\RobustToolbox\Lidgren.Network\Lidgren.Network.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Client\Robust.Client.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Shared\Robust.Shared.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Server\Robust.Server.csproj" />

View File

@@ -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<AtmosphereSystem>().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<AtmosphereSystem>().GetGridAtmosphere(gridId)?.Invalidate(pos);
}
}
}

View File

@@ -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<AtmosphereSystem>().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");
}
}
}
}

View File

@@ -0,0 +1,10 @@
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.Components.Atmos
{
[RegisterComponent]
public class GasCanisterComponent : Component
{
public override string Name => "GasCanister";
}
}

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,10 @@
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.Components.Atmos
{
[RegisterComponent]
public class GasTankComponent : Component
{
public override string Name => "GasTank";
}
}

View File

@@ -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
{
/// <summary>
/// This is our SSAir equivalent.
/// </summary>
[RegisterComponent, Serializable]
public class GridAtmosphereComponent : Component, IGridAtmosphereComponent
{
[Robust.Shared.IoC.Dependency] private IGameTiming _gameTiming = default!;
[Robust.Shared.IoC.Dependency] private IMapManager _mapManager = default!;
/// <summary>
/// Check current execution time every n instances processed.
/// </summary>
private const int LagCheckIterations = 15;
/// <summary>
/// Max milliseconds allowed for atmos updates.
/// </summary>
private const float LagCheckMaxMilliseconds = 5f;
/// <summary>
/// How much time before atmos updates are ran.
/// </summary>
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<ExcitedGroup> _excitedGroups = new HashSet<ExcitedGroup>(1000);
[ViewVariables]
private readonly Dictionary<MapIndices, TileAtmosphere> _tiles = new Dictionary<MapIndices, TileAtmosphere>(1000);
[ViewVariables]
private readonly HashSet<TileAtmosphere> _activeTiles = new HashSet<TileAtmosphere>(1000);
[ViewVariables]
private readonly HashSet<TileAtmosphere> _hotspotTiles = new HashSet<TileAtmosphere>(1000);
[ViewVariables]
private readonly HashSet<MapIndices> _invalidatedCoords = new HashSet<MapIndices>(1000);
[ViewVariables]
private HashSet<TileAtmosphere> _highPressureDelta = new HashSet<TileAtmosphere>(1000);
[ViewVariables]
private ProcessState _state = ProcessState.TileEqualize;
private enum ProcessState
{
TileEqualize,
ActiveTiles,
ExcitedGroups,
HighPressureDelta,
Hotspots,
}
/// <inheritdoc />
public void PryTile(MapIndices indices)
{
if (IsSpace(indices) || IsAirBlocked(indices)) return;
var tile = _grid.GetTileRef(indices).Tile;
var tileDefinitionManager = IoCManager.Resolve<ITileDefinitionManager>();
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<IServerEntityManager>().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<IMapGridComponent>().Grid;
RepopulateTiles();
}
public override void OnAdd()
{
base.OnAdd();
_grid = Owner.GetComponent<IMapGridComponent>().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();
}
}
/// <inheritdoc />
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();
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddActiveTile(TileAtmosphere tile)
{
if (tile?.GridIndex != _grid.Index || tile?.Air == null) return;
tile.Excited = true;
_activeTiles.Add(tile);
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveActiveTile(TileAtmosphere tile)
{
if (tile == null) return;
_activeTiles.Remove(tile);
tile.Excited = false;
tile.ExcitedGroup?.Dispose();
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddHotspotTile(TileAtmosphere tile)
{
if (tile?.GridIndex != _grid.Index || tile?.Air == null) return;
_hotspotTiles.Add(tile);
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveHotspotTile(TileAtmosphere tile)
{
if (tile == null) return;
_hotspotTiles.Remove(tile);
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddHighPressureDelta(TileAtmosphere tile)
{
if (tile?.GridIndex != _grid.Index) return;
_highPressureDelta.Add(tile);
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool HasHighPressureDelta(TileAtmosphere tile)
{
return _highPressureDelta.Contains(tile);
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddExcitedGroup(ExcitedGroup excitedGroup)
{
_excitedGroups.Add(excitedGroup);
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveExcitedGroup(ExcitedGroup excitedGroup)
{
_excitedGroups.Remove(excitedGroup);
}
/// <inheritdoc />
public TileAtmosphere GetTile(GridCoordinates coordinates)
{
return GetTile(coordinates.ToMapIndices(_mapManager));
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
public bool IsAirBlocked(MapIndices indices)
{
var ac = GetObstructingComponent(indices);
return ac != null && ac.AirBlocked;
}
/// <inheritdoc />
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<Direction, TileAtmosphere> GetAdjacentTiles(MapIndices indices)
{
var sides = new Dictionary<Direction, TileAtmosphere>();
foreach (var dir in Cardinal())
{
var side = indices.Offset(dir);
sides[dir] = GetTile(side);
}
return sides;
}
/// <inheritdoc />
public int HighPressureDeltaCount => _highPressureDelta.Count;
public long EqualizationQueueCycleControl { get; set; }
/// <inheritdoc />
public float GetVolumeForCells(int cellCount)
{
return _grid.TileSize * cellCount * Atmospherics.CellVolume;
}
/// <inheritdoc />
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<AirtightComponent>(out var ac))
return ac;
}
return null;
}
private static IEnumerable<Direction> 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<IMapGridComponent>().Grid.Index;
if (!serializer.TryReadDataField("uniqueMixes", out List<GasMixture> uniqueMixes) ||
!serializer.TryReadDataField("tiles", out Dictionary<MapIndices, int> 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<GasMixture>();
var tiles = new Dictionary<MapIndices, int>();
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<GasMixture>());
serializer.DataField(ref tiles, "tiles", new Dictionary<MapIndices, int>());
}
}
public IEnumerator<TileAtmosphere> GetEnumerator()
{
return _tiles.Values.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
/// <inheritdoc />
public void BurnTile(MapIndices gridIndices)
{
// TODO ATMOS
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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<AirtightComponent>();
_collidableComponent = Owner.GetComponent<ICollidableComponent>();
_appearance = Owner.GetComponent<AppearanceComponent>();
_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);

View File

@@ -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<Type>(){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<IMapGridComponent>();
if (_pauseManager.IsGridPaused(grid.GridIndex))
continue;
gridEnt.GetComponent<GridAtmosphereComponent>().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);
}
}
}

View File

@@ -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<GasTileOverlayData> _queue = new HashSet<GasTileOverlayData>();
private Dictionary<GridId, HashSet<MapIndices>> _invalid = new Dictionary<GridId, HashSet<MapIndices>>();
private Dictionary<GridId, Dictionary<MapIndices, GasOverlayData>> _overlay =
new Dictionary<GridId, Dictionary<MapIndices, GasOverlayData>>();
[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<MapIndices>();
_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<MapIndices, GasOverlayData>();
_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<GasTileOverlayData>();
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<IMapManager>();
var entityMan = IoCManager.Resolve<IEntityManager>();
var list = new List<GasData>();
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();
}
}
}

View File

@@ -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
{

View File

@@ -19,6 +19,7 @@
"Marker",
"EmergencyLight",
"Clickable",
"CanSeeGases",
};
}

View File

@@ -0,0 +1,11 @@
using Content.Server.Atmos;
namespace Content.Server.Interfaces
{
public interface IGasMixtureHolder
{
public GasMixture Air { get; set; }
bool AssumeAir(GasMixture giver);
}
}

View File

@@ -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);
}
}

View File

@@ -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<IGameTicker>().GetObserverSpawnPoint();
if (canReturn && player.AttachedEntity.TryGetComponent(out SpeciesComponent species))
{
switch (species.CurrentDamageState)

View File

@@ -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;

View File

@@ -0,0 +1,174 @@
using System.Collections.Generic;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
namespace Content.Shared.Atmos
{
/// <summary>
/// Class to store atmos constants.
/// </summary>
public static class Atmospherics
{
static Atmospherics()
{
var protoMan = IoCManager.Resolve<IPrototypeManager>();
GasPrototypes = new GasPrototype[TotalNumberOfGases];
for (var i = 0; i < TotalNumberOfGases; i++)
{
GasPrototypes[i] = protoMan.Index<GasPrototype>(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<GasPrototype> Gases => GasPrototypes;
#region ATMOS
/// <summary>
/// The universal gas constant, in kPa*L/(K*mol)
/// </summary>
public const float R = 8.314462618f;
/// <summary>
/// 1 ATM in kPA.
/// </summary>
public const float OneAtmosphere = 101.325f;
/// <summary>
/// -270.3ºC in K. CMB stands for Cosmic Microwave Background.
/// </summary>
public const float TCMB = 2.7f;
/// <summary>
/// 0ºC in K
/// </summary>
public const float T0C = 273.15f;
/// <summary>
/// 20ºC in K
/// </summary>
public const float T20C = 293.15f;
/// <summary>
/// Liters in a cell.
/// </summary>
public const float CellVolume = 2500f;
/// <summary>
/// Moles in a 2.5 m^3 cell at 101.325 Pa and 20ºC
/// </summary>
public const float MolesCellStandard = (OneAtmosphere * CellVolume / (T20C * R));
#endregion
/// <summary>
/// Visible moles multiplied by this factor to get moles at which gas is at max visibility.
/// </summary>
public const float FactorGasVisibleMax = 20f;
/// <summary>
/// Minimum number of moles a gas can have.
/// </summary>
public const float GasMinMoles = 0.00000005f;
public const float OpenHeatTransferCoefficient = 0.4f;
/// <summary>
/// Ratio of air that must move to/from a tile to reset group processing
/// </summary>
public const float MinimumAirRatioToSuspend = 0.1f;
/// <summary>
/// Minimum ratio of air that must move to/from a tile
/// </summary>
public const float MinimumAirRatioToMove = 0.001f;
/// <summary>
/// Minimum amount of air that has to move before a group processing can be suspended
/// </summary>
public const float MinimumAirToSuspend = (MolesCellStandard * MinimumAirRatioToSuspend);
public const float MinimumTemperatureToMove = (T20C + 100f);
public const float MinimumMolesDeltaToMove = (MolesCellStandard * MinimumAirRatioToMove);
/// <summary>
/// Minimum temperature difference before group processing is suspended
/// </summary>
public const float MinimumTemperatureDeltaToSuspend = 4.0f;
/// <summary>
/// Minimum temperature difference before the gas temperatures are just set to be equal.
/// </summary>
public const float MinimumTemperatureDeltaToConsider = 0.5f;
/// <summary>
/// Minimum temperature for starting superconduction.
/// </summary>
public const float MinimumTemperatureStartSuperConduction = (T20C + 200f);
/// <summary>
/// Minimum heat capacity.
/// </summary>
public const float MinimumHeatCapacity = 0.0003f;
#region Excited Groups
/// <summary>
/// Number of full atmos updates ticks before an excited group breaks down (averages gas contents across turfs)
/// </summary>
public const int ExcitedGroupBreakdownCycles = 4;
/// <summary>
/// Number of full atmos updates before an excited group dismantles and removes its turfs from active
/// </summary>
public const int ExcitedGroupsDismantleCycles = 16;
#endregion
/// <summary>
/// Hard limit for tile equalization.
/// </summary>
public const int ZumosHardTileLimit = 2000;
/// <summary>
/// Limit for zone-based tile equalization.
/// </summary>
public const int ZumosTileLimit = 200;
/// <summary>
/// Total number of gases. Increase this if you want to add more!
/// </summary>
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;
}
/// <summary>
/// Gases to Ids. Keep these updated with the prototypes!
/// </summary>
public enum Gas
{
Oxygen = 0,
Nitrogen = 1,
CarbonDioxide = 2,
Phoron = 3,
Tritium = 4,
}
}

View File

@@ -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; }
/// <summary>
/// Specific heat for gas.
/// </summary>
public float SpecificHeat { get; private set; }
/// <summary>
/// Minimum amount of moles for this gas to be visible.
/// </summary>
public float GasMolesVisible { get; private set; }
/// <summary>
/// Visibility for this gas will be max after this value.
/// </summary>
public float GasMolesVisibleMax => GasMolesVisible * Atmospherics.FactorGasVisibleMax;
/// <summary>
/// If this reagent is in gas form, this is the path to the overlay that will be used to make the gas visible.
/// </summary>
public string GasOverlayTexture { get; private set; }
/// <summary>
/// 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.
/// </summary>
public string GasOverlayState { get; set; }
/// <summary>
/// State for the gas RSI overlay.
/// </summary>
public string GasOverlaySprite { get; set; }
/// <summary>
/// Sprite specifier for the gas overlay.
/// </summary>
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;
}
}
/// <summary>
/// Path to the tile overlay used when this gas appears visible.
/// </summary>
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);
}
}
}

View File

@@ -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<IMetabolizable> Metabolism => _metabolism;
public string SpriteReplacementPath => _spritePath;
public ReagentPrototype()

View File

@@ -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;
}
}
}
}

Binary file not shown.

View File

@@ -171,6 +171,12 @@
- anchor
- unanchor
- tubeconnections
- addatmos
- addgas
- fillgas
- listgases
- removegas
- settemp
CanViewVar: true
CanAdminPlace: true
CanScript: true

View File

@@ -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

View File

@@ -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 {}

View File

@@ -51,6 +51,8 @@
interfaces:
- key: enum.WiresUiKey.Key
type: WiresBoundUserInterface
- type: Airtight
adjacentAtmosphere: true
- type: Occluder
- type: SnapGrid
offset: Center

View File

@@ -17,6 +17,7 @@
sprite: Constructible/Structures/closet.rsi
state: generic_door
- type: Clickable
- type: MovedByPressure
- type: InteractionOutline
- type: Collidable
shapes:

View File

@@ -33,6 +33,8 @@
sizeY: 32
- type: SnapGrid
offset: Center
- type: Airtight
- type: IconSmooth
key: walls
base: solid

View File

@@ -31,6 +31,7 @@
thresholdvalue: 100
- type: SnapGrid
offset: Center
- type: Airtight
- type: Window
base: window

View File

@@ -16,6 +16,7 @@
DoRangeCheck: false
- type: IgnorePause
- type: Ghost
- type: CanSeeGases
- type: Sprite
netsync: false
drawdepth: Ghosts

View File

@@ -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

View File

@@ -7,6 +7,7 @@
size: 5
- type: Clickable
- type: InteractionOutline
- type: MovedByPressure
- type: Collidable
shapes:
- !type:PhysShapeAabb

View File

@@ -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

View File

@@ -13,6 +13,7 @@
color: "#808080"
boilingPoint: -183.0
meltingPoint: -218.4
gasOverlayTexture: /Textures/Effects/Gas/deleteme.png
- type: reagent
id: chem.S

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 B

View File

@@ -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]]}]}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 B

View File

@@ -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]]}]}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -43,11 +43,13 @@
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/FilterSettingsManager/AttributeFilterXml/@EntryValue">&lt;data /&gt;</s:String>
<s:String x:Key="/Default/FilterSettingsManager/CoverageFilterXml/@EntryValue">&lt;data&gt;&lt;IncludeFilters /&gt;&lt;ExcludeFilters&gt;&lt;Filter ModuleMask="*.UnitTesting" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /&gt;&lt;/ExcludeFilters&gt;&lt;/data&gt;</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Atmos/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=autoconnect/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=BYOND/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/FilterSettingsManager/AttributeFilterXml/@EntryValue">&lt;data /&gt;</s:String>
<s:String x:Key="/Default/FilterSettingsManager/CoverageFilterXml/@EntryValue">&lt;data&gt;&lt;IncludeFilters /&gt;&lt;ExcludeFilters&gt;&lt;Filter ModuleMask="Lidgren.Network" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /&gt;&lt;/ExcludeFilters&gt;&lt;/data&gt;</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Collidable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Cooldowns/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=flashable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Fluidsynth/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=freepats/@EntryIndexedValue">True</s:Boolean>
@@ -59,6 +61,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=occluder/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Occluders/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Patreon/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Phoron/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=placeable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=preemptively/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=prefs/@EntryIndexedValue">True</s:Boolean>
@@ -70,7 +73,8 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=stunnable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=swsl/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=underplating/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=unexcite/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=uplink/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Vulkan_0027s/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Wirecutter/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Wirecutter/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Zumos/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>