Document Atmospherics ExcitedGroups (#41269)

Document ExcitedGroups
This commit is contained in:
ArtisticRoomba
2025-11-03 03:49:36 -08:00
committed by GitHub
parent 7b6f01ca69
commit e6bbd40e72
4 changed files with 228 additions and 140 deletions

View File

@@ -4,148 +4,199 @@ using Content.Shared.Atmos.Components;
using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
namespace Content.Server.Atmos.EntitySystems
namespace Content.Server.Atmos.EntitySystems;
public sealed partial class AtmosphereSystem
{
public sealed partial class AtmosphereSystem
/*
Handles Excited Groups, an optimization routine executed during LINDA
that groups active tiles together.
Groups of active tiles that have very low mole deltas between them
are dissolved after a cooldown period, performing a final equalization
on all tiles in the group before deactivating them.
If tiles are so close together in pressure that the final equalization
would result in negligible gas transfer, the group is dissolved without
performing an equalization.
This prevents LINDA from constantly transferring tiny amounts of gas
between tiles that are already nearly equalized.
*/
/// <summary>
/// Adds a tile to an <see cref="ExcitedGroups"/>, resetting the group's cooldowns in the process.
/// </summary>
/// <param name="excitedGroup">The <see cref="ExcitedGroups"/> to add the tile to.</param>
/// <param name="tile">The <see cref="TileAtmosphere"/> to add.</param>
private void ExcitedGroupAddTile(ExcitedGroup excitedGroup, TileAtmosphere tile)
{
private void ExcitedGroupAddTile(ExcitedGroup excitedGroup, TileAtmosphere tile)
DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!");
DebugTools.Assert(tile.ExcitedGroup == null, "Tried to add a tile to an excited group when it's already in another one!");
excitedGroup.Tiles.Add(tile);
tile.ExcitedGroup = excitedGroup;
ExcitedGroupResetCooldowns(excitedGroup);
}
/// <summary>
/// Removes a tile from an <see cref="ExcitedGroups"/>.
/// </summary>
/// <param name="excitedGroup">The <see cref="ExcitedGroups"/> to remove the tile from.</param>
/// <param name="tile">The <see cref="TileAtmosphere"/> to remove.</param>
private void ExcitedGroupRemoveTile(ExcitedGroup excitedGroup, TileAtmosphere tile)
{
DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!");
DebugTools.Assert(tile.ExcitedGroup == excitedGroup, "Tried to remove a tile from an excited group it's not present in!");
tile.ExcitedGroup = null;
excitedGroup.Tiles.Remove(tile);
}
/// <summary>
/// Merges two <see cref="ExcitedGroups"/>, transferring all tiles from one to the other.
/// The larger group receives the tiles of the smaller group.
/// The smaller group is then disposed of without deactivating its tiles.
/// </summary>
/// <param name="gridAtmosphere">The <see cref="GridAtmosphereComponent"/> of the grid.</param>
/// <param name="ourGroup">The first <see cref="ExcitedGroups"/> to merge.</param>
/// <param name="otherGroup">The second <see cref="ExcitedGroups"/> to merge.</param>
private void ExcitedGroupMerge(GridAtmosphereComponent gridAtmosphere, ExcitedGroup ourGroup, ExcitedGroup otherGroup)
{
DebugTools.Assert(!ourGroup.Disposed, "Excited group is disposed!");
DebugTools.Assert(!otherGroup.Disposed, "Excited group is disposed!");
DebugTools.Assert(gridAtmosphere.ExcitedGroups.Contains(ourGroup), "Grid Atmosphere does not contain Excited Group!");
DebugTools.Assert(gridAtmosphere.ExcitedGroups.Contains(otherGroup), "Grid Atmosphere does not contain Excited Group!");
var ourSize = ourGroup.Tiles.Count;
var otherSize = otherGroup.Tiles.Count;
ExcitedGroup winner;
ExcitedGroup loser;
if (ourSize > otherSize)
{
DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!");
DebugTools.Assert(tile.ExcitedGroup == null, "Tried to add a tile to an excited group when it's already in another one!");
excitedGroup.Tiles.Add(tile);
tile.ExcitedGroup = excitedGroup;
ExcitedGroupResetCooldowns(excitedGroup);
winner = ourGroup;
loser = otherGroup;
}
else
{
winner = otherGroup;
loser = ourGroup;
}
private void ExcitedGroupRemoveTile(ExcitedGroup excitedGroup, TileAtmosphere tile)
foreach (var tile in loser.Tiles)
{
tile.ExcitedGroup = winner;
winner.Tiles.Add(tile);
}
loser.Tiles.Clear();
ExcitedGroupDispose(gridAtmosphere, loser);
ExcitedGroupResetCooldowns(winner);
}
/// <summary>
/// Resets the cooldowns of an excited group.
/// </summary>
/// <param name="excitedGroup">The <see cref="ExcitedGroups"/> to reset cooldowns for.</param>
private void ExcitedGroupResetCooldowns(ExcitedGroup excitedGroup)
{
DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!");
excitedGroup.BreakdownCooldown = 0;
excitedGroup.DismantleCooldown = 0;
}
/// <summary>
/// Performs a final equalization on all tiles in an excited group before deactivating it.
/// </summary>
/// <param name="ent">The grid.</param>
/// <param name="excitedGroup">The <see cref="ExcitedGroups"/> to equalize and dissolve.</param>
private void ExcitedGroupSelfBreakdown(
Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent,
ExcitedGroup excitedGroup)
{
DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!");
DebugTools.Assert(ent.Comp1.ExcitedGroups.Contains(excitedGroup), "Grid Atmosphere does not contain Excited Group!");
var combined = new GasMixture(Atmospherics.CellVolume);
var tileSize = excitedGroup.Tiles.Count;
if (excitedGroup.Disposed)
return;
if (tileSize == 0)
{
ExcitedGroupDispose(ent.Comp1, excitedGroup);
return;
}
// Combine all gasses in the group into a single mixture
// for distribution into each individual tile.
foreach (var tile in excitedGroup.Tiles)
{
if (tile?.Air == null)
continue;
Merge(combined, tile.Air);
// If this tile is space and space is all-consuming, the final equalization
// will result in a vacuum, so we can skip the rest of the equalization.
if (!ExcitedGroupsSpaceIsAllConsuming || !tile.Space)
continue;
combined.Clear();
break;
}
combined.Multiply(1 / (float)tileSize);
// Distribute the combined mixture evenly to all tiles in the group.
foreach (var tile in excitedGroup.Tiles)
{
if (tile?.Air == null)
continue;
tile.Air.CopyFrom(combined);
InvalidateVisuals(ent, tile);
}
excitedGroup.BreakdownCooldown = 0;
}
/// <summary>
/// Deactivates and removes all tiles from an excited group without performing a final equalization.
/// Used when an excited group is expected to be nearly equalized already to avoid unnecessary processing.
/// </summary>
/// <param name="gridAtmosphere">The <see cref="GridAtmosphereComponent"/> of the grid.</param>
/// <param name="excitedGroup">The <see cref="ExcitedGroups"/> to dissolve.</param>
private void DeactivateGroupTiles(GridAtmosphereComponent gridAtmosphere, ExcitedGroup excitedGroup)
{
foreach (var tile in excitedGroup.Tiles)
{
DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!");
DebugTools.Assert(tile.ExcitedGroup == excitedGroup, "Tried to remove a tile from an excited group it's not present in!");
tile.ExcitedGroup = null;
excitedGroup.Tiles.Remove(tile);
RemoveActiveTile(gridAtmosphere, tile);
}
private void ExcitedGroupMerge(GridAtmosphereComponent gridAtmosphere, ExcitedGroup ourGroup, ExcitedGroup otherGroup)
excitedGroup.Tiles.Clear();
}
/// <summary>
/// Removes and disposes of an excited group without performing any final equalization
/// or deactivation of its tiles.
/// </summary>
private void ExcitedGroupDispose(GridAtmosphereComponent gridAtmosphere, ExcitedGroup excitedGroup)
{
if (excitedGroup.Disposed)
return;
DebugTools.Assert(gridAtmosphere.ExcitedGroups.Contains(excitedGroup), "Grid Atmosphere does not contain Excited Group!");
excitedGroup.Disposed = true;
gridAtmosphere.ExcitedGroups.Remove(excitedGroup);
foreach (var tile in excitedGroup.Tiles)
{
DebugTools.Assert(!ourGroup.Disposed, "Excited group is disposed!");
DebugTools.Assert(!otherGroup.Disposed, "Excited group is disposed!");
DebugTools.Assert(gridAtmosphere.ExcitedGroups.Contains(ourGroup), "Grid Atmosphere does not contain Excited Group!");
DebugTools.Assert(gridAtmosphere.ExcitedGroups.Contains(otherGroup), "Grid Atmosphere does not contain Excited Group!");
var ourSize = ourGroup.Tiles.Count;
var otherSize = otherGroup.Tiles.Count;
ExcitedGroup winner;
ExcitedGroup loser;
if (ourSize > otherSize)
{
winner = ourGroup;
loser = otherGroup;
}
else
{
winner = otherGroup;
loser = ourGroup;
}
foreach (var tile in loser.Tiles)
{
tile.ExcitedGroup = winner;
winner.Tiles.Add(tile);
}
loser.Tiles.Clear();
ExcitedGroupDispose(gridAtmosphere, loser);
ExcitedGroupResetCooldowns(winner);
tile.ExcitedGroup = null;
}
private void ExcitedGroupResetCooldowns(ExcitedGroup excitedGroup)
{
DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!");
excitedGroup.BreakdownCooldown = 0;
excitedGroup.DismantleCooldown = 0;
}
private void ExcitedGroupSelfBreakdown(
Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent,
ExcitedGroup excitedGroup)
{
DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!");
DebugTools.Assert(ent.Comp1.ExcitedGroups.Contains(excitedGroup), "Grid Atmosphere does not contain Excited Group!");
var combined = new GasMixture(Atmospherics.CellVolume);
var tileSize = excitedGroup.Tiles.Count;
if (excitedGroup.Disposed)
return;
if (tileSize == 0)
{
ExcitedGroupDispose(ent.Comp1, excitedGroup);
return;
}
foreach (var tile in excitedGroup.Tiles)
{
if (tile?.Air == null)
continue;
Merge(combined, tile.Air);
if (!ExcitedGroupsSpaceIsAllConsuming || !tile.Space)
continue;
combined.Clear();
break;
}
combined.Multiply(1 / (float)tileSize);
foreach (var tile in excitedGroup.Tiles)
{
if (tile?.Air == null)
continue;
tile.Air.CopyFrom(combined);
InvalidateVisuals(ent, tile);
}
excitedGroup.BreakdownCooldown = 0;
}
/// <summary>
/// This de-activates and removes all tiles in an excited group.
/// </summary>
private void DeactivateGroupTiles(GridAtmosphereComponent gridAtmosphere, ExcitedGroup excitedGroup)
{
foreach (var tile in excitedGroup.Tiles)
{
tile.ExcitedGroup = null;
RemoveActiveTile(gridAtmosphere, tile);
}
excitedGroup.Tiles.Clear();
}
/// <summary>
/// This removes an excited group without de-activating its tiles.
/// </summary>
private void ExcitedGroupDispose(GridAtmosphereComponent gridAtmosphere, ExcitedGroup excitedGroup)
{
if (excitedGroup.Disposed)
return;
DebugTools.Assert(gridAtmosphere.ExcitedGroups.Contains(excitedGroup), "Grid Atmosphere does not contain Excited Group!");
excitedGroup.Disposed = true;
gridAtmosphere.ExcitedGroups.Remove(excitedGroup);
foreach (var tile in excitedGroup.Tiles)
{
tile.ExcitedGroup = null;
}
excitedGroup.Tiles.Clear();
}
excitedGroup.Tiles.Clear();
}
}

View File

@@ -129,9 +129,16 @@ namespace Content.Server.Atmos.EntitySystems
switch (tile.LastShare)
{
// Refresh this tile's suspension cooldown if it had significant sharing.
case > Atmospherics.MinimumAirToSuspend:
ExcitedGroupResetCooldowns(tile.ExcitedGroup);
break;
// If this tile moved a very small amount of air, but not enough to matter,
// we set the dismantle cooldown to 0.
// This dissolves the group without performing an equalization as we expect
// the group to be mostly equalized already if we're moving around miniscule
// amounts of air.
case > Atmospherics.MinimumMolesDeltaToMove:
tile.ExcitedGroup.DismantleCooldown = 0;
break;

View File

@@ -365,7 +365,6 @@ namespace Content.Server.Atmos.EntitySystems
ExcitedGroupSelfBreakdown(ent, excitedGroup);
else if (excitedGroup.DismantleCooldown > Atmospherics.ExcitedGroupsDismantleCycles)
DeactivateGroupTiles(gridAtmosphere, excitedGroup);
// TODO ATMOS. What is the point of this? why is this only de-exciting the group? Shouldn't it also dismantle it?
if (number++ < LagCheckIterations)
continue;

View File

@@ -1,13 +1,44 @@
namespace Content.Server.Atmos
namespace Content.Server.Atmos;
/// <summary>
/// <para>Internal Atmospherics class that stores data about a group of <see cref="TileAtmosphere"/>s
/// that are excited and need to be processed.</para>
///
/// <para>Excited Groups is an optimization routine executed during LINDA
/// that bunches small groups of active <see cref="TileAtmosphere"/>s
/// together and performs equalization processing on the entire group when the group dissolves.
/// Dissolution happens when LINDA operations between the tiles decrease to very low mole deltas.</para>
/// </summary>
public sealed class ExcitedGroup
{
public sealed class ExcitedGroup
{
[ViewVariables] public bool Disposed = false;
/// <summary>
/// Whether this Active Group has been disposed of.
/// Used to make sure we don't perform operations on active groups that
/// we've already dissolved.
/// </summary>
[ViewVariables]
public bool Disposed = false;
[ViewVariables] public readonly List<TileAtmosphere> Tiles = new(100);
/// <summary>
/// List of tiles that belong to this excited group.
/// </summary>
[ViewVariables]
public readonly List<TileAtmosphere> Tiles = new(100);
[ViewVariables] public int DismantleCooldown { get; set; } = 0;
/// <summary>
/// Cycles before this excited group will be queued for dismantling.
/// Dismantling is the process of equalizing the atmosphere
/// across all tiles in the excited group and removing the group.
/// </summary>
[ViewVariables]
public int DismantleCooldown = 0;
[ViewVariables] public int BreakdownCooldown { get; set; } = 0;
}
/// <summary>
/// Cycles before this excited group will be allowed to break down and deactivate.
/// Breakdown occurs when the excited group is small enough and inactive enough
/// to be safely removed without equalization. Used where the mole deltas across
/// the group are very low but not high enough for an equalization to occur.
/// </summary>
[ViewVariables]
public int BreakdownCooldown = 0;
}