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;