Fix atmos devices not correctly reffing the changed atmos (#41585)

This commit is contained in:
ArtisticRoomba
2025-12-23 00:12:52 -08:00
committed by GitHub
parent 3a3707d2a2
commit 0ed5619e8b
7 changed files with 246 additions and 66 deletions

View File

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

View 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,34 +58,82 @@ 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));
RebuildGridTiles(grid);
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;
}
RebuildGridAtmosphere((euid.Value, gridAtmosphere, gridComp));
}
}
/// <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 };
}
// 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>
/// Clears & re-creates all references to <see cref="TileAtmosphere"/>s stored on a grid.
/// </summary>

View 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(

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

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

View 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)

View File

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