Airtight rooms permanently corrupted when a door closes and a Gate/hangar door opens in the same tic
Airtight rooms permanently corrupted when a door closes and a Gate/hangar door opens in the same tick (MyGridGasSystem)
=== SUMMARY ===
In Space Engineers 1.210.14 the host's incremental room state (MyGridGasSystem) gets permanently corrupted when, in the same gas-processing batch, one door finishes closing and an outer door without PressurizeCube (Gate, airtight hangar doors, old Door, small-grid doors) starts opening. The whole outside of the grid is turned into a new room marked AIRTIGHT, and the real top room (m_rooms[0]) is left with zero cells.
From then on, rooms that merge with the outside never split off again. Players see two symptoms, depending on what happens next: a vent keeps "pressurizing" a room whose outer door is open (air is pumped into space), or a room with every door closed stays "Not pressurized". In our multiplayer game, building and grinding blocks next to it did not help; reloading the world (full regeneration) fixes the grid.
The trigger is a plain vanilla setup: an Event Controller that opens the outer door when the inner door closes, which is the standard airlock automation. Two players using two doors at the same moment have the same effect, and large grids widen the window because each batch runs longer. It is reproducible every time on the attached vanilla save, without mods.
=== ENVIRONMENT AND ATTACHMENTS ===
- Game version: 1.210.14 (Steam, Windows 11)
- Mode: Creative, offline single player; the same code runs on the host of every multiplayer session
- World settings: Oxygen on, Airtightness (pressurization) on
- Mods: none needed. The attached save has an empty mod list; the bug was confirmed on it without mods
- Save: "Pressurization test". The test grid is the static large grid "Большая структура 667" (EntityId 134147274047590667); the other grids in the world are unrelated
- Diagnostic files: PBF_Report_2026-10-24_20-28-31.txt and PBF_AutoLog_2026-10-24_20-27-56.txt, written by our diagnostic mod during the same steps
The test grid has three spaces in a row: the vent room (151 cells, air vent "Vent" in pressurize mode, O2/H2 generator, battery), a small airlock (15 cells) and space.
- "Door": Door / LargeBlockCentredDoorGlass, between the vent room and the airlock
- "Gates": Door / LargeBlockGate (5x2, no PressurizeCube), between the airlock and space
- "Event Controller": EventControllerLarge, event "Door opened" on "Door", slot 2 (door closed) = "Gates" -> Open
The diagnostic mod ("Pressurization Bug Finder", made by us for this bug) reads the engine's rooms through the ModAPI (IMyGridGasSystem), recomputes them from scratch with the same airtightness rules (IsAirtightBetweenPositions, IsAirtightBlock) and logs every door event the gas system receives. It changes nothing in the game. It is only needed to read the logs below; the bug itself shows up in the vent's terminal info.
=== STEPS TO REPRODUCE ===
On the attached save (every time, no mods):
1. Load "Pressurization test". Wait until the vent "Vent" shows the room as pressurized.
2. Open "Door" (vent room -> airlock). Wait until it is fully open.
3. Close "Door". The moment it is fully closed, the Event Controller opens "Gates" (airlock -> space). The airlock depressurizes, which is correct. The vent room stays pressurized.
4. Open "Door" again. Wait a few seconds.
5. Close "Door" again. "Gates" stays open.
From scratch on any grid: a sealed room with an air vent, an inner door of any type, an outer door that is a Gate or an airtight hangar door, and an Event Controller with the event "Door opened" on the inner door whose slot 2 opens the outer door.
EXPECTED
- Step 4: the vent room merges with the airlock and space through the open gate, shows "Not pressurized" and loses its air.
- Step 5: with "Door" closed again, the vent room is sealed, shows "Pressurized" and refills.
ACTUAL
- Step 3: the engine turns the whole outside of the grid into a new room marked airtight. Nothing is visible yet.
- Step 4: the vent still reports an airtight room and keeps pressurizing it while the air leaks into space through the open gate (oxygen level 1.00 -> 0.15).
- Step 5: with every door of the vent room closed, the vent shows "Not pressurized" and stays so. The state does not recover on its own; reloading the world fixes it. In our multiplayer game, building and grinding blocks next to the room did not help either.
=== ROOT CAUSE IN MyGridGasSystem ===
CheckPositionForEmptyRoom creates an AIRTIGHT room from any cell that has no room, even when its flood reaches the grid bounds. In the reported case that cell belongs to a door that has just started opening and whose "removed" event has not been processed yet, so the flood runs through the open door into space. (Names are from a decompiled 1.210.14 Sandbox.Game.dll.)
1. Closed doors without PressurizeCube have no room. AddBlock(MyAddedBlockInfo) sets m_cubeRoom[cell] = null for door cells when the door is not MyAirtightSlideDoor and its definition has no PressurizeCube (SkipsRefresh == false). The initial generation also skips closed doors (CheckPositionForEmptyRoom returns for Status == Closed/Closing). So the cells of a closed Gate or hangar door are null.
2. Adds are processed before removes. Update() calls StartRefreshRoomData() when m_addedBlocks is not empty and then StartRemoveBlocks(), which returns because m_isProcessingData is already true. When a door close event (OnDoorStateChanged(door, false) -> m_addedBlocks) and a door open event (-> m_deletedBlocks) are queued in the same tick, the open is handled one batch later.
3. Every add batch ends with GenerateEmptyRooms(). RefreshRoomData() runs AddBlock for each added block, RefreshTopRoom(), RefreshDirtyRooms(), GenerateGasBlockRooms() and GenerateEmptyRooms(). The last one visits every cell in the bounds and calls CheckPositionForEmptyRoom on it.
4. The opening door is no longer skipped. For the Gate cell the link is still null (its removal is waiting in m_deletedBlocks), and its Status is now Opening, so the "closed door" early return does not apply.
5. The flood leaves the grid. GetRoomBlocks(position) reads the live door state on the worker thread. The Gate is opening, so IsAirtightBlock is false on its faces and the flood covers the airlock and the whole outside up to m_storedGridMin/Max. GetRoomBlocks silently stops at the bounds; it does not report that it reached them.
6. The outside becomes an airtight room. CreateAirtightRoom(roomBlocks, 0f, position) sets IsAirtight = true and m_cubeRoom.Add(cell, link) for every cell, overwriting the top room's links. The top room (m_rooms[0]) stays in the list with its old BlockCount and zero cells.
7. It never heals. RefreshTopRoom() only works on m_rooms[0], which owns nothing now. The new "outside" room is refreshed by RefreshRoomBlocks() like an inner room: from its StartingPosition (the Gate's first cell), and when that flood reaches the bounds (flag == false) it only sets IsAirtight = false and reassigns no cells. Any room merged into it (step 4 of the repro) stays merged, so its vent reports "Not pressurized" with all doors closed. A full regeneration on world load builds correct rooms again.
The same applies to GenerateGasBlockRooms(): it also calls CreateAirtightRoom on an unbounded flood from a gas block whose cell has no room.
=== EVIDENCE FROM THE ATTACHED LOGS ===
The close event of "Door" and the open event of "Gates" reach the gas system in the same tick 1028; the next batch (run #2) leaves the outside in a new airtight room that starts at the Gate's first cell (0,1,4). From PBF_AutoLog_2026-10-24_20-27-56.txt (tick = game update counted from session start), newest first:
- 1755: doors settled, gas system idle: engine says the vent room is not airtight (1355 cells, start (0,1,4)); a from-scratch recompute says sealed (151 cells, only "Door" on its border)
- 1550: vent room engine state: airtight True -> False, level 0.15, 1355 cells, start (0,1,4)
- 1546: "Door" fully closed -> OnDoorStateChanged(false); batch run #5 follows
- 1360: batch run #4: the vent room merges into the airtight "outside" room and stays airtight while air escapes through the open gate
- 1359: "Door" starts opening (gate still open) -> OnDoorStateChanged(true)
- 1030: batch run #2 done: the outside corner (-1,-1,-31) now belongs to a new room, airtight=True, 1204 cells, start (0,1,4); the top room keeps BlockCount 1179 and 0 cells
- 1028: "Door" fully closed -> OnDoorStateChanged(false) (add); in the same tick the Event Controller opens "Gates" -> OnDoorStateChanged(true) (remove)
- 761: "Door" opens; the vent room merges with the sealed airlock (correct)
- 10: vent room: airtight, 151 cells, start (3,1,0)
Engine rooms right after run #2 (IMyGridGasSystem.GetRooms, cells counted from IMyOxygenRoom.Blocks), in m_rooms order:
- Room 1 (top room): IsAirtight=false, BlockCount=1179, cells=0, StartingPosition=(-1,-1,-31) = grid min - 1. Owns nothing.
- Room 2: IsAirtight=true, BlockCount=151, cells=151, StartingPosition=(3,1,0). Vent room, correct.
- Room 3: IsAirtight=true, BlockCount=15, cells=0, StartingPosition=(2,1,0). Old airlock room, owns nothing.
- Room 4 (new): IsAirtight=TRUE, BlockCount=1204, cells=1204, StartingPosition=(0,1,4) = Gate cell. The airlock plus all of space.
We first saw the same state on a 6043-block ship with 87 doors (airtight hangar doors, Warfare 2): the top room had 0 cells, the outside belonged to an airtight room started at a hangar door cell, and about 40 more rooms had 0 cells but BlockCount around 39000. That run is in PBF_Report_2026-10-24_17-37-14.txt and can be attached on request.
=== AFFECTED DOORS AND REAL-WORLD TRIGGERS ===
The outer door must be one whose closed cell has no room: every door without PressurizeCube except MyAirtightSlideDoor. In vanilla 1.210.14 with all DLC:
- Airtight Hangar Door: AirtightHangarDoor / (empty)
- Military hangar doors: AirtightHangarDoor / AirtightHangarDoorWarfare2A, 2B, 2C (Warfare 2)
- Gate: Door / LargeBlockGate (Frostbite)
- Even wide gate: Door / LargeBlockEvenWideDoor (Contact)
- Door, large and small grid: Door / (empty), SmallDoor
- Centered door, small grid (and glass): Door / SmallBlockCentredDoor, SmallBlockCentredDoorGlass (Core Systems)
- Sci-Fi Sliding Door, small grid: Door / SmallSideDoor (Sparks of the Future)
The inner door can be any door; any other "block added" event in the same tick works too (a door finishing closing anywhere on the grid, a block being placed or welded past its last build stage).
Ways the two events land in one batch in normal play:
- Airlock automation: an Event Controller "door closed -> open the outer door" fires inside OnDoorClosed, the same tick as the close event. Deterministic.
- Several players: one closes a door while another opens a gate or hangar door; on a server with many players this happens all the time.
- Large grids: while a batch runs on the worker thread, new events wait in the queues, and the next batch again takes adds first. The longer a batch takes, the wider the window. In our multiplayer game the large base broke while a ship did not.
In our group's multiplayer game this looked like: "the base lost pressurization, every door is closed, every vent works, only a reload helps".
=== SUGGESTED FIXES ===
The first fix alone removes the reported corruption; the others close the related holes we saw in the code.
1. Never create an airtight room from a flood that reached the bounds. Make GetRoomBlocks report whether it touched m_storedGridMin/Max (as RefreshRoomBlocks already does with flag). In CheckPositionForEmptyRoom and GenerateGasBlockRooms, give such cells to the top room (m_rooms[0].Link) instead of calling CreateAirtightRoom, or create the room with IsAirtight = false and merge it into the top room.
2. Skip cells whose block has a pending removal. In CheckPositionForEmptyRoom, ignore doors that are in m_deletedBlocks, or treat a door whose last event was "open" as already removed.
3. Keep the event order. Process adds and removes of one batch in the order they arrived (one queue with a kind per entry), or process m_deletedBlocks before m_addedBlocks in Update().
4. Do not let the top room lose its cells. RemoveBlock (both rooms non-airtight branch) and RefreshRoomBlocks (absorbing a room whose BlockCount reaches 0, which also copies its StartingPosition) look, from reading the code, like they can also move the outside into a non-top room; we have not isolated a case. Always merge into m_rooms[0] when it is one of the two rooms, and never remove it.
5. Self-check (cheap). After each batch, if the cell at m_storedGridMin does not belong to m_rooms[0], set m_generatedDataPending = true and regenerate. That alone would turn this permanent bug into a few seconds of wrong pressure.
=== RELATED DOOR BUGS FOUND ALONG THE WAY ===
Two smaller bugs show up when a door is changed from inside its own OnDoorOpened / OnDoorClosed callback, which is exactly what an Event Controller with the "Door opened" event does (MyEventDoorOpened subscribes to both, and TriggerAction runs synchronously on the server). We reproduced both in a clean vanilla world; logs on request.
A. Slide and hangar doors: the close event is lost, the room never re-seals (MyAirtightDoorGeneric: "Sliding Door" LargeBlockSlideDoor and all airtight hangar doors)
- Repro: room with an air vent, one Sliding Door to space, Event Controller "Door opened" on that door, slot 1 = the same door -> Close. Open the door twice.
- Result: the first time the door hangs open (bug B); the second time it closes, but the vent stays "Not pressurized" (in our tests until reload). Our diagnostic log shows Closing -> Closed with no OnDoorStateChanged(false) at all.
- Cause: in UpdateBeforeSimulation, when the door is fully open, the engine invokes OnDoorOpened and then sets m_stateChange = false. Closing the door inside the callback runs OnStateChange(), which sets m_stateChange = true, and that is overwritten right after. Without the flag the "fully closed" branch never runs, so neither OnDoorStateChanged(false) nor OnDoorClosed fire, and MyGridGasSystem never adds the door back. (MyDoor is not affected: it fires the close event from its state every update.)
- Fix: clear m_stateChange before invoking the callbacks, not after.
B. All doors: the target status pushes the door back (MyDoor and MyAirtightDoorGeneric)
- After the callbacks, the server sets m_syncTargetStatus to Open (or Closed). If a callback has already reversed Open, OnSyncTargetStatusChanged drives the door the other way with m_forceUpdate.
- Closed from OnDoorOpened: the door hangs fully open with Open = false, Status = Closing, and the close sound repeats. The door only recovers when something sets Open = true again; with MyDoor that can be ContactCallbackInternal when a character touches the leaf.
- Opened from OnDoorClosed (Event Controller slot 2 = same door -> Open): the door stays visually closed with Open = true, OpenRatio = 0, Status = Opening. Faces marked PressurizedClosed need Status == Closed, so the room is "Not pressurized" behind a door that looks closed.
- Fix: set m_syncTargetStatus before invoking the callbacks, or skip it when Open changed during them.
I have the same bug
Replies have been locked on this page!