]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Fix atmos devices not correctly reffing the changed atmos (#41585)
authorArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
Tue, 23 Dec 2025 08:12:52 +0000 (00:12 -0800)
committerGitHub <noreply@github.com>
Tue, 23 Dec 2025 08:12:52 +0000 (00:12 -0800)
Content.IntegrationTests/Tests/Atmos/AtmosMonitoringTest.cs [new file with mode: 0644]
Content.Server/Atmos/EntitySystems/AtmosphereSystem.Commands.cs
Content.Server/Atmos/EntitySystems/AtmosphereSystem.Processing.cs
Content.Server/Atmos/EntitySystems/AtmosphereSystem.Utils.cs
Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs
Content.Server/Atmos/Monitor/Systems/AtmosMonitoringSystem.cs
Content.Shared/Atmos/Piping/Components/AtmosDeviceTileChangedEvent.cs [new file with mode: 0644]

diff --git a/Content.IntegrationTests/Tests/Atmos/AtmosMonitoringTest.cs b/Content.IntegrationTests/Tests/Atmos/AtmosMonitoringTest.cs
new file mode 100644 (file)
index 0000000..83d7b9b
--- /dev/null
@@ -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;
+
+/// <summary>
+/// Test for determining that an AtmosMonitoringComponent/System correctly references
+/// the GasMixture of the tile it is on if the tile's GasMixture ever changes.
+/// </summary>
+[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");
+
+    /// <summary>
+    /// 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.
+    /// </summary>
+    [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<AtmosMonitorComponent>(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.");
+    }
+
+    /// <summary>
+    /// Tests if the monitor properly updates its reference to the tile mixture
+    /// when the FixGridAtmos command is called.
+    /// </summary>
+    [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<AtmosMonitorComponent>(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.");
+    }
+}
index 246c3a571fcdf7514b2005414bf976ce1468c5ae..11ebfe2066f57a0b362f11ae07af749a455a54ff 100644 (file)
@@ -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 <grid Ids>", FixGridAtmosCommand, FixGridAtmosCommandCompletions);
+            "fixgridatmos <grid Ids>",
+            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<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> grid =
-               new(euid.Value, gridAtmosphere, Comp<GasTileOverlayComponent>(euid.Value), gridComp, Transform(euid.Value));
+           RebuildGridAtmosphere((euid.Value, gridAtmosphere, gridComp));
+       }
+    }
 
-           RebuildGridTiles(grid);
+    /// <summary>
+    /// Rebuilds all <see cref="TileAtmosphere"/>s on a grid to have roundstart gas mixes.
+    /// </summary>
+    /// <remarks>Please be responsible with this method. Used only by tests and fixgridatmos.</remarks>
+    public void RebuildGridAtmosphere(Entity<GridAtmosphereComponent, MapGridComponent> 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<AtmosFixMarkerComponent>();
-           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<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> grid =
+            new(ent.Owner, ent.Comp1, Comp<GasTileOverlayComponent>(ent), ent.Comp2, Transform(ent));
+
+        RebuildGridTiles(grid);
+
+        var query = GetEntityQuery<AtmosFixMarkerComponent>();
+        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;
+        }
     }
 
     /// <summary>
index c0f081f9ba9c4383ce1c26948b8c240bd1427300..d562fe51117008cee227acd32967a16034efebc0 100644 (file)
@@ -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(
index a402cf20f36da806c0daa90b6be539b8e66e9a17..9b53d0d16cb4f5d1a5583cf1b1998a8b640a0917 100644 (file)
@@ -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);
     }
+
+    /// <summary>
+    /// 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 <see cref="GasMixture"/>.
+    /// </summary>
+    /// <param name="ent">The grid atmosphere entity.</param>
+    /// <param name="tile">The tile to check for devices on.</param>
+    private void NotifyDeviceTileChanged(Entity<GridAtmosphereComponent, MapGridComponent> 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);
+        }
+    }
 }
index 8120caca4e05f2880b84208c318072968236fe8a..df380912b6cdb66fa00d98e01780e2395de08402 100644 (file)
@@ -64,8 +64,8 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
         InitializeGridAtmosphere();
         InitializeMap();
 
-        _mapAtmosQuery = GetEntityQuery<MapAtmosphereComponent>();
         _atmosQuery = GetEntityQuery<GridAtmosphereComponent>();
+        _mapAtmosQuery = GetEntityQuery<MapAtmosphereComponent>();
         _airtightQuery = GetEntityQuery<AirtightComponent>();
         _firelockQuery = GetEntityQuery<FirelockComponent>();
 
index 452b30033145a8481401ecf9ce8f181988a11466..7333b7b8142e7f264fcee51b737db6d7710a1573 100644 (file)
@@ -57,6 +57,13 @@ public sealed class AtmosMonitorSystem : EntitySystem
         SubscribeLocalEvent<AtmosMonitorComponent, DeviceNetworkPacketEvent>(OnPacketRecv);
         SubscribeLocalEvent<AtmosMonitorComponent, AtmosDeviceDisabledEvent>(OnAtmosDeviceLeaveAtmosphere);
         SubscribeLocalEvent<AtmosMonitorComponent, AtmosDeviceEnabledEvent>(OnAtmosDeviceEnterAtmosphere);
+        SubscribeLocalEvent<AtmosMonitorComponent, AtmosDeviceTileChangedEvent>(OnAtmosDeviceTileChangedEvent);
+    }
+
+    private void OnAtmosDeviceTileChangedEvent(Entity<AtmosMonitorComponent> 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 (file)
index 0000000..daedcfd
--- /dev/null
@@ -0,0 +1,9 @@
+namespace Content.Shared.Atmos.Piping.Components;
+
+/// <summary>
+/// Raised directed on entities when the tile that they reside in has had their
+/// associated TileAtmosphere changed significantly, i.e. a tile/<see cref="GasMixture"/> being added, removed,
+/// or replaced. Important when atmos devices need to update any stored references to their tile's atmosphere.
+/// </summary>
+[ByRefEvent]
+public readonly record struct AtmosDeviceTileChangedEvent;