From: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
Date: Tue, 23 Dec 2025 08:12:52 +0000 (-0800)
Subject: Fix atmos devices not correctly reffing the changed atmos (#41585)
X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=0ed5619e8bd85d39e3d7f635a859d9984da6bd9e;p=space-station-14.git
Fix atmos devices not correctly reffing the changed atmos (#41585)
---
diff --git a/Content.IntegrationTests/Tests/Atmos/AtmosMonitoringTest.cs b/Content.IntegrationTests/Tests/Atmos/AtmosMonitoringTest.cs
new file mode 100644
index 0000000000..83d7b9be53
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Atmos/AtmosMonitoringTest.cs
@@ -0,0 +1,130 @@
+using System.Numerics;
+using Content.Server.Atmos.Monitor.Components;
+using Content.Shared.Atmos;
+using Robust.Shared.Console;
+using Robust.Shared.Map;
+using Robust.Shared.Maths;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.IntegrationTests.Tests.Atmos;
+
+///
+/// Test for determining that an AtmosMonitoringComponent/System correctly references
+/// the GasMixture of the tile it is on if the tile's GasMixture ever changes.
+///
+[TestOf(typeof(Atmospherics))]
+public sealed class AtmosMonitoringTest : AtmosTest
+{
+ // We can just reuse the dP test, I just want a grid.
+ protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml");
+
+ private readonly EntProtoId _airSensorProto = new("AirSensor");
+ private readonly EntProtoId _wallProto = new("WallSolid");
+
+ ///
+ /// Tests if the monitor properly nulls out its reference to the tile mixture
+ /// when a wall is placed on top of it, and restores the reference when the wall is removed.
+ ///
+ [Test]
+ public async Task NullOutTileAtmosphereGasMixture()
+ {
+ // run an atmos update to initialize everything For Real surely
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ var gridNetEnt = SEntMan.GetNetEntity(RelevantAtmos.Owner);
+ TargetCoords = new NetCoordinates(gridNetEnt, Vector2.Zero);
+ var netEnt = await Spawn(_airSensorProto);
+ var airSensorUid = SEntMan.GetEntity(netEnt);
+ Transform.TryGetGridTilePosition(airSensorUid, out var vec);
+
+ // run another one to ensure that the ref to the GasMixture was picked up
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ // should be in the middle
+ Assert.That(vec,
+ Is.EqualTo(Vector2i.Zero),
+ "Air sensor not in expected position on grid (0, 0)");
+
+ var atmosMonitor = SEntMan.GetComponent(airSensorUid);
+ var tileMixture = SAtmos.GetTileMixture(airSensorUid);
+
+ Assert.That(tileMixture,
+ Is.SameAs(atmosMonitor.TileGas),
+ "Atmos monitor's TileGas does not match actual tile mixture after spawn.");
+
+ // ok now spawn a wall or something on top of it
+ var wall = await Spawn(_wallProto);
+ var wallUid = SEntMan.GetEntity(wall);
+
+ // ensure that atmospherics registers the change - the gas mixture should no longer exist
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ // the monitor's ref to the gas should be null now
+ Assert.That(atmosMonitor.TileGas,
+ Is.Null,
+ "Atmos monitor's TileGas is not null after wall placed on top. Possible dead reference.");
+ // the actual mixture on the tile should be null now too
+ var nullTileMixture = SAtmos.GetTileMixture(airSensorUid);
+ Assert.That(nullTileMixture, Is.Null, "Tile mixture is not null after wall placed on top.");
+
+ // ok now delete the wall
+ await Delete(wallUid);
+
+ // ensure that atmospherics registers the change - the gas mixture should be back
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ // gas mixture should now exist again
+ var newTileMixture = SAtmos.GetTileMixture(airSensorUid);
+ Assert.That(newTileMixture, Is.Not.Null, "Tile mixture is null after wall removed.");
+ // monitor's ref to the gas should be back too
+ Assert.That(atmosMonitor.TileGas,
+ Is.SameAs(newTileMixture),
+ "Atmos monitor's TileGas does not match actual tile mixture after wall removed.");
+ }
+
+ ///
+ /// Tests if the monitor properly updates its reference to the tile mixture
+ /// when the FixGridAtmos command is called.
+ ///
+ [Test]
+ public async Task FixGridAtmosReplaceMixtureOnTileChange()
+ {
+ // run an atmos update to initialize everything For Real surely
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ var gridNetEnt = SEntMan.GetNetEntity(RelevantAtmos.Owner);
+ TargetCoords = new NetCoordinates(gridNetEnt, Vector2.Zero);
+ var netEnt = await Spawn(_airSensorProto);
+ var airSensorUid = SEntMan.GetEntity(netEnt);
+ Transform.TryGetGridTilePosition(airSensorUid, out var vec);
+
+ // run another one to ensure that the ref to the GasMixture was picked up
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ // should be in the middle
+ Assert.That(vec,
+ Is.EqualTo(Vector2i.Zero),
+ "Air sensor not in expected position on grid (0, 0)");
+
+ var atmosMonitor = SEntMan.GetComponent(airSensorUid);
+ var tileMixture = SAtmos.GetTileMixture(airSensorUid);
+
+ Assert.That(tileMixture,
+ Is.SameAs(atmosMonitor.TileGas),
+ "Atmos monitor's TileGas does not match actual tile mixture after spawn.");
+
+ SAtmos.RebuildGridAtmosphere((ProcessEnt.Owner, ProcessEnt.Comp1, ProcessEnt.Comp3));
+
+ // EXTREMELY IMPORTANT: The reference to the tile mixture on the tile should be completely different.
+ var newTileMixture = SAtmos.GetTileMixture(airSensorUid);
+ Assert.That(newTileMixture,
+ Is.Not.SameAs(tileMixture),
+ "Tile mixture is the same instance after fixgridatmos was ran. It should be a new instance.");
+
+ // The monitor's ref to the tile mixture should have updated too.
+ Assert.That(atmosMonitor.TileGas,
+ Is.SameAs(newTileMixture),
+ "Atmos monitor's TileGas does not match actual tile mixture after fixgridatmos was ran.");
+ }
+}
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Commands.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Commands.cs
index 246c3a571f..11ebfe2066 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Commands.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Commands.cs
@@ -19,7 +19,9 @@ public sealed partial class AtmosphereSystem
// Fix Grid Atmos command.
_consoleHost.RegisterCommand("fixgridatmos",
"Makes every tile on a grid have a roundstart gas mix.",
- "fixgridatmos ", FixGridAtmosCommand, FixGridAtmosCommandCompletions);
+ "fixgridatmos ",
+ FixGridAtmosCommand,
+ FixGridAtmosCommandCompletions);
}
private void ShutdownCommands()
@@ -36,42 +38,6 @@ public sealed partial class AtmosphereSystem
return;
}
- var mixtures = new GasMixture[9];
- for (var i = 0; i < mixtures.Length; i++)
- mixtures[i] = new GasMixture(Atmospherics.CellVolume) { Temperature = Atmospherics.T20C };
-
- // 0: Air
- mixtures[0].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesStandard);
- mixtures[0].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesStandard);
-
- // 1: Vaccum
-
- // 2: Oxygen (GM)
- mixtures[2].AdjustMoles(Gas.Oxygen, Atmospherics.MolesCellGasMiner);
-
- // 3: Nitrogen (GM)
- mixtures[3].AdjustMoles(Gas.Nitrogen, Atmospherics.MolesCellGasMiner);
-
- // 4: Plasma (GM)
- mixtures[4].AdjustMoles(Gas.Plasma, Atmospherics.MolesCellGasMiner);
-
- // 5: Instant Plasmafire (r)
- mixtures[5].AdjustMoles(Gas.Oxygen, Atmospherics.MolesCellGasMiner);
- mixtures[5].AdjustMoles(Gas.Plasma, Atmospherics.MolesCellGasMiner);
- mixtures[5].Temperature = 5000f;
-
- // 6: (Walk-In) Freezer
- mixtures[6].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesFreezer);
- mixtures[6].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesFreezer);
- mixtures[6].Temperature = Atmospherics.FreezerTemp; // Little colder than an actual freezer but gives a grace period to get e.g. themomachines set up, should keep warm for a few door openings
-
- // 7: Nitrogen (101kpa) for vox rooms
- mixtures[7].AdjustMoles(Gas.Nitrogen, Atmospherics.MolesCellStandard);
-
- // 8: Air (GM)
- mixtures[8].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesGasMiner);
- mixtures[8].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesGasMiner);
-
foreach (var arg in args)
{
if (!NetEntity.TryParse(arg, out var netEntity) || !TryGetEntity(netEntity, out var euid))
@@ -92,32 +58,80 @@ public sealed partial class AtmosphereSystem
continue;
}
- // Force Invalidate & update air on all tiles
- Entity grid =
- new(euid.Value, gridAtmosphere, Comp(euid.Value), gridComp, Transform(euid.Value));
+ RebuildGridAtmosphere((euid.Value, gridAtmosphere, gridComp));
+ }
+ }
- RebuildGridTiles(grid);
+ ///
+ /// Rebuilds all s on a grid to have roundstart gas mixes.
+ ///
+ /// Please be responsible with this method. Used only by tests and fixgridatmos.
+ public void RebuildGridAtmosphere(Entity ent)
+ {
+ var mixtures = new GasMixture[9];
+ for (var i = 0; i < mixtures.Length; i++)
+ {
+ mixtures[i] = new GasMixture(Atmospherics.CellVolume) { Temperature = Atmospherics.T20C };
+ }
- var query = GetEntityQuery();
- foreach (var (indices, tile) in gridAtmosphere.Tiles.ToArray())
- {
- if (tile.Air is not {Immutable: false} air)
- continue;
-
- air.Clear();
- var mixtureId = 0;
- var enumerator = _mapSystem.GetAnchoredEntitiesEnumerator(grid, grid, indices);
- while (enumerator.MoveNext(out var entUid))
- {
- if (query.TryComp(entUid, out var marker))
- mixtureId = marker.Mode;
- }
-
- var mixture = mixtures[mixtureId];
- Merge(air, mixture);
- air.Temperature = mixture.Temperature;
- }
- }
+ // 0: Air
+ mixtures[0].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesStandard);
+ mixtures[0].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesStandard);
+
+ // 1: Vaccum
+
+ // 2: Oxygen (GM)
+ mixtures[2].AdjustMoles(Gas.Oxygen, Atmospherics.MolesCellGasMiner);
+
+ // 3: Nitrogen (GM)
+ mixtures[3].AdjustMoles(Gas.Nitrogen, Atmospherics.MolesCellGasMiner);
+
+ // 4: Plasma (GM)
+ mixtures[4].AdjustMoles(Gas.Plasma, Atmospherics.MolesCellGasMiner);
+
+ // 5: Instant Plasmafire (r)
+ mixtures[5].AdjustMoles(Gas.Oxygen, Atmospherics.MolesCellGasMiner);
+ mixtures[5].AdjustMoles(Gas.Plasma, Atmospherics.MolesCellGasMiner);
+ mixtures[5].Temperature = 5000f;
+
+ // 6: (Walk-In) Freezer
+ mixtures[6].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesFreezer);
+ mixtures[6].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesFreezer);
+ mixtures[6].Temperature = Atmospherics.FreezerTemp; // Little colder than an actual freezer but gives a grace period to get e.g. themomachines set up, should keep warm for a few door openings
+
+ // 7: Nitrogen (101kpa) for vox rooms
+ mixtures[7].AdjustMoles(Gas.Nitrogen, Atmospherics.MolesCellStandard);
+
+ // 8: Air (GM)
+ mixtures[8].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesGasMiner);
+ mixtures[8].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesGasMiner);
+
+
+ // Force Invalidate & update air on all tiles
+ Entity grid =
+ new(ent.Owner, ent.Comp1, Comp(ent), ent.Comp2, Transform(ent));
+
+ RebuildGridTiles(grid);
+
+ var query = GetEntityQuery();
+ foreach (var (indices, tile) in ent.Comp1.Tiles.ToArray())
+ {
+ if (tile.Air is not {Immutable: false} air)
+ continue;
+
+ air.Clear();
+ var mixtureId = 0;
+ var enumerator = _mapSystem.GetAnchoredEntitiesEnumerator(grid, grid, indices);
+ while (enumerator.MoveNext(out var entUid))
+ {
+ if (query.TryComp(entUid, out var marker))
+ mixtureId = marker.Mode;
+ }
+
+ var mixture = mixtures[mixtureId];
+ Merge(air, mixture);
+ air.Temperature = mixture.Temperature;
+ }
}
///
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Processing.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Processing.cs
index c0f081f9ba..d562fe5111 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Processing.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Processing.cs
@@ -265,6 +265,7 @@ namespace Content.Server.Atmos.EntitySystems
tile.ArchivedCycle = 0;
tile.LastShare = 0f;
tile.Hotspot = new Hotspot();
+ NotifyDeviceTileChanged((ent.Owner, ent.Comp1, ent.Comp3), tile.GridIndices);
return;
}
@@ -275,6 +276,10 @@ namespace Content.Server.Atmos.EntitySystems
if (data.FixVacuum)
GridFixTileVacuum(tile);
+
+ // Since we assigned the tile a new GasMixture we need to tell any devices
+ // on this tile that the reference has changed.
+ NotifyDeviceTileChanged((ent.Owner, ent.Comp1, ent.Comp3), tile.GridIndices);
}
private void QueueRunTiles(
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Utils.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Utils.cs
index a402cf20f3..9b53d0d16c 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Utils.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Utils.cs
@@ -1,10 +1,8 @@
using System.Runtime.CompilerServices;
using Content.Server.Atmos.Components;
-using Content.Server.Maps;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
-using Content.Shared.Maps;
-using Robust.Shared.Map;
+using Content.Shared.Atmos.Piping.Components;
using Robust.Shared.Map.Components;
namespace Content.Server.Atmos.EntitySystems;
@@ -176,4 +174,21 @@ public partial class AtmosphereSystem
_tile.PryTile(tileRef);
}
+
+ ///
+ /// Notifies all subscribing entities on a particular tile that the tile has changed.
+ /// Atmos devices may store references to tiles, so this is used to properly resync devices
+ /// after a significant atmos change on that tile, for example a tile getting a new .
+ ///
+ /// The grid atmosphere entity.
+ /// The tile to check for devices on.
+ private void NotifyDeviceTileChanged(Entity ent, Vector2i tile)
+ {
+ var inTile = _mapSystem.GetAnchoredEntities(ent.Owner, ent.Comp2, tile);
+ var ev = new AtmosDeviceTileChangedEvent();
+ foreach (var uid in inTile)
+ {
+ RaiseLocalEvent(uid, ref ev);
+ }
+ }
}
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs
index 8120caca4e..df380912b6 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs
@@ -64,8 +64,8 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
InitializeGridAtmosphere();
InitializeMap();
- _mapAtmosQuery = GetEntityQuery();
_atmosQuery = GetEntityQuery();
+ _mapAtmosQuery = GetEntityQuery();
_airtightQuery = GetEntityQuery();
_firelockQuery = GetEntityQuery();
diff --git a/Content.Server/Atmos/Monitor/Systems/AtmosMonitoringSystem.cs b/Content.Server/Atmos/Monitor/Systems/AtmosMonitoringSystem.cs
index 452b300331..7333b7b814 100644
--- a/Content.Server/Atmos/Monitor/Systems/AtmosMonitoringSystem.cs
+++ b/Content.Server/Atmos/Monitor/Systems/AtmosMonitoringSystem.cs
@@ -57,6 +57,13 @@ public sealed class AtmosMonitorSystem : EntitySystem
SubscribeLocalEvent(OnPacketRecv);
SubscribeLocalEvent(OnAtmosDeviceLeaveAtmosphere);
SubscribeLocalEvent(OnAtmosDeviceEnterAtmosphere);
+ SubscribeLocalEvent(OnAtmosDeviceTileChangedEvent);
+ }
+
+ private void OnAtmosDeviceTileChangedEvent(Entity ent, ref AtmosDeviceTileChangedEvent args)
+ {
+ if (!ent.Comp.MonitorsPipeNet)
+ ent.Comp.TileGas = _atmosphereSystem.GetContainingMixture(ent.Owner, true);
}
private void OnAtmosDeviceLeaveAtmosphere(EntityUid uid, AtmosMonitorComponent atmosMonitor, ref AtmosDeviceDisabledEvent args)
diff --git a/Content.Shared/Atmos/Piping/Components/AtmosDeviceTileChangedEvent.cs b/Content.Shared/Atmos/Piping/Components/AtmosDeviceTileChangedEvent.cs
new file mode 100644
index 0000000000..daedcfdaa5
--- /dev/null
+++ b/Content.Shared/Atmos/Piping/Components/AtmosDeviceTileChangedEvent.cs
@@ -0,0 +1,9 @@
+namespace Content.Shared.Atmos.Piping.Components;
+
+///
+/// Raised directed on entities when the tile that they reside in has had their
+/// associated TileAtmosphere changed significantly, i.e. a tile/ being added, removed,
+/// or replaced. Important when atmos devices need to update any stored references to their tile's atmosphere.
+///
+[ByRefEvent]
+public readonly record struct AtmosDeviceTileChangedEvent;