--- /dev/null
+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.");
+ }
+}
// 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()
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))
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>
tile.ArchivedCycle = 0;
tile.LastShare = 0f;
tile.Hotspot = new Hotspot();
+ NotifyDeviceTileChanged((ent.Owner, ent.Comp1, ent.Comp3), tile.GridIndices);
return;
}
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(
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;
_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);
+ }
+ }
}
InitializeGridAtmosphere();
InitializeMap();
- _mapAtmosQuery = GetEntityQuery<MapAtmosphereComponent>();
_atmosQuery = GetEntityQuery<GridAtmosphereComponent>();
+ _mapAtmosQuery = GetEntityQuery<MapAtmosphereComponent>();
_airtightQuery = GetEntityQuery<AirtightComponent>();
_firelockQuery = GetEntityQuery<FirelockComponent>();
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)
--- /dev/null
+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;