# Atmos Firelocks Fix Walkthrough
#### Abstract
Hey, Roomba here. I fixed firelocks.
This paper exists as a way to document what I was thinking, in order to better understand the changes made (and how you can maybe see that there can be better solutions).
Because I don't really know what I'm doing. Think of me as a monkey, banging on a typewriter, who managed to type the works of Shakespeare, but instead of a typewriter, it is Jetbrains Rider, and instead of the works of Shakespeare, it is atmos fixes.
So don't treat this document as holy text - this is just my view of how things work under the hood. I'm also sorry if I repeat myself - it's just something that I do.
## Background
### The Problem
In short, firelocks simply *did not work*. When a spacing happened, all firelocks in a room seemed to drop, and even still, the firelocks dropped so slow that some gas would actually escape, thinning out the air in the room.
This was horrible for gameplay! Over time, a station could get progressively more and more spaced from people just opening more and more firelocks. And since firelocks were dropping when they didn't need to, it lead to people treating firelocks as this lukewarm annoyance rather than a prominent announcement of danger. Which is bad!
So what was supposed to happen?
## Atmospherics Processing & Monstermos
Atmospherics is a sleeping giant putting on a theatrical performance. When it doesn't need to work, it will stay still, and when it needs to do math, it will try to optimize and abstract things as much as possible.
Part of this effort is Monstermos, which is a optimization routine that Atmospherics does in order to simplify its calculations while still achieving the same effect. Instead of doing processes on each tile unit or small excited groups, Monstermos will batch tiles into one large volume and do operations on that large tile group, given certain limits.
The logic we're interested in is when atmospherics decides to perform an explosive depressurization on a monstermos volume.
### Technical
Atmospherics decides to perform an explosive depressurization by calling `ExplosivelyDepressurize` when it detects a space tile after executing a [Breadth-first search algorithm](https://en.wikipedia.org/wiki/Breadth-first_search) (or BFS) in `EqualizePressureInZone`. This is all in `AtmosphereSystem.Monstermos.cs`.
`ExplosivelyDepressurize` will then execute another BFS starting from the origin of space. For each iteration of the loop, it will check both the tile it's on, and the tile one tile ahead of the direction of its search for a firelock via `ConsiderFirelocks`.
The code we're interested in is replicated below. It has been re-documented for the purposes of this walkthrough.
```C#
if (!otherTile.Space)
{
// Step through each cardinal direction
for (var j = 0; j < Atmospherics.Directions; j++)
{
// Grab the tile one tile ahead of our current direction
var otherTile2 = otherTile.AdjacentTiles[j];
// Tile has no air, so there is nothing to add to our array
if (otherTile2?.Air == null)
continue;
// We've already processed this tile
if (otherTile2.MonstermosInfo.LastQueueCycle == queueCycle)
continue;
// Grab the current direction
var direction = (AtmosDirection) (1 << j);
// We pulled a valid directional tile (since we checked the tile is not null)
// so assert that the tile we pulled is indeed real and unblocked
DebugTools.Assert(otherTile.AdjacentBits.IsFlagSet(direction));
// Assert that the opposite direction is also unblocked, as it should be
// a two way street
DebugTools.Assert(otherTile2.AdjacentBits.IsFlagSet(direction.GetOpposite()));
// Drop any firelocks here and one tile ahead to block the progression
// of spacing throughout the volume
ConsiderFirelocks(ent, otherTile, otherTile2);
// The firelocks might have closed on us;
// recheck our flags to see if the direction is now blocked
if (!otherTile.AdjacentBits.IsFlagSet(direction))
continue;
// Mark this tile as processed
otherTile2.MonstermosInfo = new MonstermosInfo { LastQueueCycle = queueCycle };
// Add this tile to the tiles-to-depressurize array that will be used later
_depressurizeTiles[tileCount++] = otherTile2;
// Break out if we've hit the limit
if (tileCount >= limit)
break;
}
}
```
#### `ConsiderFirelocks`
`ConsiderFirelocks` is a helper method that drops any firelock that is capable of dropping on that tile.
The code is replicated below, with added documentation:
```C#
private void ConsiderFirelocks(
Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent,
TileAtmosphere tile,
TileAtmosphere other)
{
// Init. our variable that determines if we've dropped firelocks
var reconsiderAdjacent = false;
var mapGrid = ent.Comp3;
// Enumerate over all entities on this tile
foreach (var entity in _map.GetAnchoredEntities(ent.Owner, mapGrid, tile.GridIndices))
{
// if the entity is a firelock...
if (_firelockQuery.TryGetComponent(entity, out var firelock))
// Drop the firelock right now and mark that we've dropped one
reconsiderAdjacent |= _firelockSystem.EmergencyPressureStop(entity, firelock);
}
// Do the same for the tile ahead of us as well
foreach (var entity in _map.GetAnchoredEntities(ent.Owner, mapGrid, other.GridIndices))
{
if (_firelockQuery.TryGetComponent(entity, out var firelock))
reconsiderAdjacent |= _firelockSystem.EmergencyPressureStop(entity, firelock);
}
// If we haven't dropped any firelocks, there is no reason to
// recalculate tiles
if (!reconsiderAdjacent)
return;
// Recalculate their airtightness flags of the tiles we've affected right now
UpdateAdjacentTiles(ent, tile);
UpdateAdjacentTiles(ent, other);
InvalidateVisuals(ent, tile);
InvalidateVisuals(ent, other);
}
```
#### `EmergencyPressureStop`
`EmergencyPressureStop` is a `SharedFirelockSystem` method that simply drops any firelock if it is capable of dropping, and if it is dropping, returns whether or not that firelock is currently considered "solid" enough to be airtight via `OnPartialClose` in `DoorSystem`.
```C#
public bool EmergencyPressureStop(EntityUid uid, FirelockComponent? firelock = null, DoorComponent? door = null)
{
if (!Resolve(uid, ref firelock, ref door))
return false;
if (door.State != DoorState.Open
|| firelock.EmergencyCloseCooldown != null
&& _gameTiming.CurTime < firelock.EmergencyCloseCooldown)
return false;
if (!_doorSystem.TryClose(uid, door))
return false;
return _doorSystem.OnPartialClose(uid, door);
}
```
#### `UpdateAdjacentTiles`
`UpdateAdjacentTiles` is an atmospherics method that updates a tile's adjacency flags.
These determine where air is allowed to flow.
Atmospherics heavily relies on bitflag manipulation to quickly and easily store and change the allowed directions in which air is to move.
The method in question has been re-documented below:
```C#
private void UpdateAdjacentTiles(
Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent,
TileAtmosphere tile,
bool activate = false)
{
var uid = ent.Owner;
var atmos = ent.Comp1;
// Retrieve the current tile's cached airtight data and the directions it blocks
var blockedDirs = tile.AirtightData.BlockedDirections;
if (activate)
AddActiveTile(atmos, tile);
// Establish invalid flags as a base if we could not determine
// directions
tile.AdjacentBits = AtmosDirection.Invalid;
// Loop through each direction
for (var i = 0; i < Atmospherics.Directions; i++)
{
// Get the current direction
var direction = (AtmosDirection)(1 << i);
// Get the tile ahead of our current direction
var adjacentIndices = tile.GridIndices.Offset(direction);
// Init a TileAtmosphere for the following work
TileAtmosphere? adjacent;
// if the tile is an actual tile, and not a grid representative...
if (!tile.NoGridTile)
{
// Get the TileAtmosphere that's actually on this tile
adjacent = GetOrNewTile(uid, atmos, adjacentIndices);
}
// If the tile isn't actually a tile...
else if (!atmos.Tiles.TryGetValue(adjacentIndices, out adjacent))
{
// Unset the flags for this direction - air is not allowed to flow to here
tile.AdjacentBits &= ~direction;
// There is no tile here for our origin tile to follow - it is null.
tile.AdjacentTiles[i] = null;
continue;
}
// Get the adj. tile's cached airtight data and the directions it blocks
var adjBlockDirs = adjacent.AirtightData.BlockedDirections;
if (activate)
AddActiveTile(atmos, adjacent);
// Get the opposite index (what we use to define directions in loops)
// of cour current index
var oppositeIndex = i.ToOppositeIndex();
// Get the opposite direction
var oppositeDirection = (AtmosDirection)(1 << oppositeIndex);
// Put it all together;
// determine if the cached airtightness directions block air from flowing
if (adjBlockDirs.IsFlagSet(oppositeDirection) || blockedDirs.IsFlagSet(direction))
{
// Adjacency is blocked by some airtight entity.
// Remove the direction from the list of directions
tile.AdjacentBits &= ~direction;
// Do this for the opposite tile as well
adjacent.AdjacentBits &= ~oppositeDirection;
// Set both refs to the tile to null, as there is no tile for
// air to travel to
tile.AdjacentTiles[i] = null;
adjacent.AdjacentTiles[oppositeIndex] = null;
}
else
{
// No airtight entity in the way.
// Add the directions to the flags
tile.AdjacentBits |= direction;
adjacent.AdjacentBits |= oppositeDirection;
// Update the refs to the tile since the tile does indeed exist
tile.AdjacentTiles[i] = adjacent;
adjacent.AdjacentTiles[oppositeIndex] = tile;
}
DebugTools.Assert(!(tile.AdjacentBits.IsFlagSet(direction) ^
adjacent.AdjacentBits.IsFlagSet(oppositeDirection)));
if (!adjacent.AdjacentBits.IsFlagSet(adjacent.MonstermosInfo.CurrentTransferDirection))
adjacent.MonstermosInfo.CurrentTransferDirection = AtmosDirection.Invalid;
}
if (!tile.AdjacentBits.IsFlagSet(tile.MonstermosInfo.CurrentTransferDirection))
tile.MonstermosInfo.CurrentTransferDirection = AtmosDirection.Invalid;
}
```
### Summary
Okay, so how does this all come together?
In short, when Monstermos performs a spacing, it will:
- Check if there's any firelocks in the way
- If there are, it will drop them, and update the tile's directions that tell Atmospherics where air can flow
- Check if the direction is now blocked, and if it is, **stop** the progression of the BFS
So why is it not working now?
## Initial Investigation
Around 6 days before I put up the pull request for the firelocks fix, I got motivated to attempt another fix for firelocks code. I just wanted a break from all of the other things I was busy with in the SS14 space. I was basically just walking in trying naive things.
### Monstermos BFS
I first started to look at the code that called `ExplosivelyDepressurize`.
At the time, I thought that the first `tileCount for` loop:
```C#
for (var i = 0; i < tileCount; i++)
{
var otherTile = _depressurizeTiles[i];
otherTile.MonstermosInfo.LastCycle = cycleNum;
otherTile.MonstermosInfo.CurrentTransferDirection = AtmosDirection.Invalid;
// Tiles in the _depressurizeTiles array cannot have null air.
if (!otherTile.Space)
{
for (var j = 0; j < Atmospherics.Directions; j++)
{
var otherTile2 = otherTile.AdjacentTiles[j];
```
was not a BFS fill, but a loop that simply enumerates all tiles that are eligible for consideration, and adds them to an array. It would simply count up all of the tiles that had air, and all of the tiles that were space, for further calculations.
I then looked at the logic that executed a BFS fill from the origin of space, for the purposes of establishing the order at which tiles get spaced:
```C#
for (var i = 0; i < progressionCount; i++)
{
// From a tile exposed to space
var otherTile = _depressurizeProgressionOrder[i];
for (var j = 0; j < Atmospherics.Directions; j++)
{
// Flood fill into this new direction
var direction = (AtmosDirection) (1 << j);
// Tiles in _depressurizeProgressionOrder cannot have null air.
if (!otherTile.AdjacentBits.IsFlagSet(direction) && !otherTile.Space)
continue;
var tile2 = otherTile.AdjacentTiles[j];
if (tile2?.MonstermosInfo.LastQueueCycle != queueCycle)
continue;
DebugTools.Assert(tile2.AdjacentBits.IsFlagSet(direction.GetOpposite()));
// If flood fill has already reached this tile, continue.
if (tile2.MonstermosInfo.LastSlowQueueCycle == queueCycleSlow)
continue;
if(tile2.Space)
continue;
tile2.MonstermosInfo.CurrentTransferDirection = j.ToOppositeDir();
tile2.MonstermosInfo.CurrentTransferAmount = 0.0f;
tile2.PressureSpecificTarget = otherTile.PressureSpecificTarget;
tile2.MonstermosInfo.LastSlowQueueCycle = queueCycleSlow;
_depressurizeProgressionOrder[progressionCount++] = tile2;
}
}
```
I incorrectly thought that this was the logic that was deciding which tiles were actually selected to be spaced based on a BFS fill, and attempted to insert firelock checks here, in order to stop the BFS progression. This did not work, as the tiles were destined to be spaced already via the first `tilecount for` loop; this array simply determined the order at which the tiles were spaced.
### Tileflags
So I removed these added checks and instead looked back to the original `tilecount for` loop. I correctly concluded that this loop served the purpose of allowing firelocks to stop the progression of spacing, so I spent the next while painfully debugging the loop. I inserted many debug prints and inspected where the flood-fill was moving and what it was doing, and *why* it was progressing. Eventually, my interest was caught on the line that decided that the firelock was blocked:
```C#
// The firelocks might have closed on us.
if (!otherTile.AdjacentBits.IsFlagSet(direction))
continue;
```
`IsFlagSet` is a very simple helper method that simply determines if the flags are set in the desired direction:
```C#
public static bool IsFlagSet(this AtmosDirection direction, AtmosDirection other)
{
return (direction & other) == other;
}
```
I somehow reached the conclusion that this bitflag checker was not correctly determining whether or not the flags were set. The logic for this was as follows.
Assume the following tilemap:
```mermaid
flowchart TD
A ~~~ B[ ];
A ~~~ C[C, or some
airtight ent];
A <--> D;
```
According to debugging, air could flow from tile A to tile D, but air could not flow from tile A to tile C. This caused me to incorrectly conclude that the `IsFlagSet` comparison check that we were using was erroneous, as it could see a diagonal component, therefore it could conclude that the flag was not actually blocked. This is massively incorrect.
I tried to create an alternative helper method to `IsFlagSet` named `IsFlagSetCardinal` in hopes of getting Atmospherics to only consider cardinal directions, however as you'd probably guess, this was a step backwards and was largely misunderstanding how tileflags worked.
I re-read how Atmospherics determines airflow directions and how bitflags worked, and moved onto another approach.
### Firelocks
I turned my attention back to `ConsiderFirelocks` and `EmergencyPressureStop`.
I first thought that the `EmergencyPressureStop` method was not being triggered in a wide enough area, so for the purposes of debugging, I forced Atmospherics to trigger all firelocks and update all tiles in a 3x3 area from the center point (the tile that we were checking for a firelock).
This was based on the flawed diagonal bitflag reasoning. This, unfortunately, did not work.
I then debugged `EmergencyPuressureStop` even further:
```C#
public bool EmergencyPressureStop(EntityUid uid, FirelockComponent? firelock = null, DoorComponent? door = null)
{
if (!Resolve(uid, ref firelock, ref door))
return false;
if (door.State != DoorState.Open
|| firelock.EmergencyCloseCooldown != null
&& _gameTiming.CurTime < firelock.EmergencyCloseCooldown)
return false;
if (!_doorSystem.TryClose(uid, door))
return false;
return _doorSystem.OnPartialClose(uid, door);
}
```
I found that the method would sometimes re-call the `EmergencyPressureStop` method, which would return `false` via the second if-statement (if the door was not open but still closing).
I attempted to solve this by hardcoding in a `true` return if the door was in the closing state. This is wrong, as the `bool` return for `EmergencyPressureStop` is supposed to tell Atmospherics whether or not the door has been successfully dropped by `EmergencyPressureStop` and if things need updating - not the state of the door. Attempting to force a `true` return would simply cause Atmospherics to constantly re-validate tiles that it shouldn't need to, as these tiles are already blocked.
I also, very, very naively set the time to partial close on the door to zero seconds. This did not work, though I did not know why until later.
### Tile Updates and Airtightness
I then turned my attention to `UpdateAdjacentTiles` after noticing that tiles forced to update by the method were performing the wrong logic.
When debugging and updating a direction that was very obviously blocked by a just-closed firelock, the method determined that the direction is clear and set the tileflags to allow air to flow through, which is wrong.
I hypothesized that this was because that the firelocks were simply not being considered as airtight, due to the fact that airlocks only become airtight after `DoorSystem` marks the tile as airtight in its update loop.
This made sense, as further testing showed that firelocks were behaving as if they were *not* airtight, allowing air to flow through them.


Doors that need to be updated are marked as active and wait until it is time for them to change state in `DoorSystem`'s next update loop:
```C#
public override void Update(float frameTime)
{
var time = GameTiming.CurTime;
foreach (var ent in _activeDoors.ToList())
{
var door = ent.Comp;
if (door.Deleted || door.NextStateChange == null)
{
_activeDoors.Remove(ent);
continue;
}
if (Paused(ent))
continue;
if (door.NextStateChange.Value < time)
NextState(ent, time);
...
```
Following `NextState`:
```C#
private void NextState(Entity<DoorComponent> ent, TimeSpan time)
{
var door = ent.Comp;
door.NextStateChange = null;
...
switch (door.State)
{
...
case DoorState.Closing:
// Either fully or partially close this door.
if (door.Partial)
SetState(ent, DoorState.Closed, door);
else
OnPartialClose(ent, door);
break;
...
```
Following `OnPartialClose`:
```C#
public bool OnPartialClose(EntityUid uid, DoorComponent? door = null, PhysicsComponent? physics = null)
{
if (!Resolve(uid, ref door, ref physics))
return false;
// Make sure no entity walked into the airlock when it started closing.
if (!CanClose(uid, door, partial: true))
{
...
}
door.Partial = true;
SetCollidable(uid, true, door, physics);
```
Following `SetCollidable` (serverside):
```C#
protected override void SetCollidable(
EntityUid uid,
bool collidable,
DoorComponent? door = null,
PhysicsComponent? physics = null,
OccluderComponent? occluder = null)
{
if (!Resolve(uid, ref door))
return;
if (door.ChangeAirtight && TryComp(uid, out AirtightComponent? airtight))
_airtightSystem.SetAirblocked((uid, airtight), collidable);
...
```
So you can see that we have to wait a long time in order for the firelocks to become airtight according to `DoorSystem`, by virtue of the update loop. This is bad! Atmospherics expects these firelocks to be airtight *right after we close them*, so we must update them on the spot.
To do this, I created a method in the serverside `FirelockSystem` so that I could import `AirtightSystem` as a dependency:
```C#
/// <summary>
/// A serverside method that calls EmergencyPressureStop on an airlock, making the airlock airtight
/// if it is starting to close.
/// </summary>
/// <param name="ent">The firelock being pressurestopped.</param>
/// <returns>A true/false depending on if the firelock was EmergencyPressureStopped.</returns>
public bool EmergencyPressureStopAirtight(Entity<FirelockComponent?, DoorComponent?, AirtightComponent?> ent)
{
var returnBool = EmergencyPressureStop(ent.Owner, ent.Comp1, ent.Comp2);
// We got the go-ahead to start closing, so mark this firelock as airtight right now.
if (returnBool && Resolve(ent, ref ent.Comp3))
{
_airtight.SetAirblocked((ent.Owner, ent.Comp3), true);
}
return returnBool;
}
```
This code simply calls the `SetAirblocked` method, setting a firelock that successfully pressurestopped to airtight *right now*.
I went in-game to test this fix, thinking I had finally fixed the last !soap issues, but it didn't work.
### Tile Updates, Again
I then turned my attention to `UpdateAdjacentTiles`, again, after noticing that tiles forced to update by the method were returning an `Invalid` tile after my forced airtightness changes. This caused all checks involving tileflags to not work properly.
This was because that, for the following lines:
```C#
var blockedDirs = tile.AirtightData.BlockedDirections;
```
```C#
var adjBlockDirs = adjacent.AirtightData.BlockedDirections;
```
`blockedDirs` and `adjBlockDirs` would always be `Invalid`.
A very, very, tired Roomba hypothesized that `Invalid` was a state that was applied when airtightness data was changed, and it was not updated on-demand, rather it was updated in Atmospherics' processing loop. I found code that indeed enqueued airtight coordinates to be updated in a process run at a separate time:
```C#
private void UpdateProcessing(float frameTime)
{
...
for (; _currentRunAtmosphereIndex < _currentRunAtmosphere.Count; _currentRunAtmosphereIndex++)
{
...
switch (atmosphere.State)
{
case AtmosphereProcessingState.Revalidate:
if (!ProcessRevalidate(ent))
{
atmosphere.ProcessingPaused = true;
return;
}
...
```
Following `ProcessRevalidate`:
```C#
private bool ProcessRevalidate(Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent)
{
...
if (!atmosphere.ProcessingPaused)
{
atmosphere.CurrentRunInvalidatedTiles.Clear();
atmosphere.CurrentRunInvalidatedTiles.EnsureCapacity(atmosphere.InvalidatedCoords.Count);
foreach (var indices in atmosphere.InvalidatedCoords)
{
var tile = GetOrNewTile(uid, atmosphere, indices, invalidateNew: false);
atmosphere.CurrentRunInvalidatedTiles.Enqueue(tile);
// Update tile.IsSpace and tile.MapAtmosphere, and tile.AirtightData.
UpdateTileData(ent, mapAtmos, tile);
...
```
Following `UpdateTileData`:
```C#
/// <summary>
/// Checks whether a tile has a corresponding grid-tile, or whether it is a "map" tile. Also checks whether the
/// tile should be considered "space"
/// </summary>
private void UpdateTileData(
Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent,
MapAtmosphereComponent? mapAtmos,
TileAtmosphere tile)
{
...
UpdateAirtightData(ent.Owner, ent.Comp1, ent.Comp3, tile);
...
```
Following `UpdateAirtightData`:
```C#
private void UpdateAirtightData(EntityUid uid, GridAtmosphereComponent atmos, MapGridComponent grid, TileAtmosphere tile)
{
var oldBlocked = tile.AirtightData.BlockedDirections;
tile.AirtightData = tile.NoGridTile
? default
: GetAirtightData(uid, grid, tile.GridIndices);
if (tile.AirtightData.BlockedDirections != oldBlocked && tile.ExcitedGroup != null)
ExcitedGroupDispose(atmos, tile.ExcitedGroup);
}
```
Following `GetAirtightData`:
```C#
private AirtightData GetAirtightData(EntityUid uid, MapGridComponent grid, Vector2i tile)
{
var blockedDirs = AtmosDirection.Invalid;
var noAirWhenBlocked = false;
var fixVacuum = false;
foreach (var ent in _map.GetAnchoredEntities(uid, grid, tile))
{
if (!_airtightQuery.TryGetComponent(ent, out var airtight))
continue;
fixVacuum |= airtight.FixVacuum;
if (!airtight.AirBlocked)
continue;
blockedDirs |= airtight.AirBlockedDirection;
noAirWhenBlocked |= airtight.NoAirWhenFullyAirBlocked;
if (blockedDirs == AtmosDirection.All && noAirWhenBlocked && fixVacuum)
break;
}
return new AirtightData(blockedDirs, noAirWhenBlocked, fixVacuum);
}
```
So we can see that we need to manually bypass the queue in order to update the airtightness in time for Monstermos to properly recognize that the firelocks have become airtight.
To fix this, we call `UpdateAirtightData` manually in `ConsiderFirelocks`, after we drop firelocks:
```C#
...
// Before updating the adjacent tile flags that determine whether air is allowed to flow
// or not, we explicitly update airtight data on these tiles right now.
// This ensures that UpdateAdjacentTiles has updated data before updating flags.
// This allows monstermos' floodfill check that determines if firelocks have dropped
// to work correctly.
UpdateAirtightData(ent.Owner, ent.Comp1, ent.Comp3, tile);
UpdateAirtightData(ent.Owner, ent.Comp1, ent.Comp3, other);
UpdateAdjacentTiles(ent, tile);
UpdateAdjacentTiles(ent, other);
...
```
And this fix is the one that worked!
## Post-Investigation Clarity & Conclusion
After fully documenting my investigation for Slarti, I realized that I didn't actually need to manually call AirtightSystem in order to mark the door as closed - this was already being done in `OnPartialClose`, which was called by `EmergencyPressureStop`!
The problem all along was simply Atmospherics not getting updated airtightness info fast enough - this was because a lot of airtightness code was refactored to be processed and cached for performance optimizations in https://github.com/space-wizards/space-station-14/pull/22521.
A disclaimer was noted in the PR about airtightness changes:
> This does change some behaviour. In particular blocked directions are now only updated if the tile was invalidated prior to the current atmos-update, and will only ever be updated once per atmos-update.
Remember, I said that the airtightness data was cached - so it wasn't fresh data for `UpdateAdjacentTiles`. Which is why it was shown as `Invalid` and thus updating flags failed, which caused firelocks to fail and Monstermos to ignore them until Atmospherics had completed one full tick and updated the airtightness.
Looking at the update order for Atmospherics, it all makes sense:
```mermaid
flowchart TD;
A[Revalidate] -->
B[TileEqualize] -->
C[ActiveTiles] -->
D[ExcitedGroups] -->
E[HighPressureDelta] -->
F[Hotspots] -->
G[Superconductivity] -->
H[PipeNet] -->
I[AtmosDevices]
```
All in all, the diff for the bugfix that has plagued atmospherics forever is a whopping 9 lines:
```diff
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Monstermos.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Monstermos.cs
index cca529fd58758..24463442cdf82 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Monstermos.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Monstermos.cs
@@ -596,8 +596,17 @@ private void ConsiderFirelocks(
if (!reconsiderAdjacent)
return;
+ // Before updating the adjacent tile flags that determine whether air is allowed to flow
+ // or not, we explicitly update airtight data on these tiles right now.
+ // This ensures that UpdateAdjacentTiles has updated data before updating flags.
+ // This allows monstermos' floodfill check that determines if firelocks have dropped
+ // to work correctly.
+ UpdateAirtightData(ent.Owner, ent.Comp1, ent.Comp3, tile);
+ UpdateAirtightData(ent.Owner, ent.Comp1, ent.Comp3, other);
+
UpdateAdjacentTiles(ent, tile);
UpdateAdjacentTiles(ent, other);
+
InvalidateVisuals(ent, tile);
InvalidateVisuals(ent, other);
}
```
Thanks for coming to my TED talk.