Atmosphere¶
The per-cell gas volume and its registry. Atmosphere (Assets.Scripts.Atmospherics.Atmosphere : IReferencable, IEvaluable, IDensePoolable) is one cell's gas state. AtmosphericsManager (Assets.Scripts.Atmospherics.AtmosphericsManager : ThreadedManager) holds the static cell -> atmosphere map. An Atmosphere carries a back-reference to the Room it belongs to.
AtmosphericsManager: the cell -> atmosphere map¶
private static readonly Dictionary<WorldGrid, Atmosphere> AllWorldAtmospheresLookUp = new Dictionary<WorldGrid, Atmosphere>(65535);
public static int WorldAtmospheresCount => AllWorldAtmospheresLookUp.Count;
public static Atmosphere Find(WorldGrid worldGrid)
{
lock (AllWorldAtmospheresLookUp)
{
AllWorldAtmospheresLookUp.TryGetValue(worldGrid, out var value);
return value;
}
}
Find is a locked TryGetValue. The map is mutated under the same lock on register / unregister (AllWorldAtmospheresLookUp.Add(atmosphere.WorldGrid, atmosphere) / .Remove(...)). The lock matters because atmospherics runs threaded: the dictionary is touched from worker threads, so a main-thread reader must respect the same lock.
Atmosphere fields¶
public Room Room;
public GasMixture GasMixture = new GasMixture(new MoleQuantity(0.0));
public readonly List<Grid3> OpenNeighbors = new List<Grid3>();
public readonly List<Grid3> OpenNeighborsForMainThread = new List<Grid3>();
public WorldGrid WorldGrid => _wGrid;
Room is the bounding-room back-reference (set by Room.AddGrid, see below). OpenNeighbors is the connectivity graph the simulation flows gas across; OpenNeighborsForMainThread is the main-thread-safe copy, swapped via SwapOpenNeighbors(). IsRoom distinguishes a room-bounded atmosphere from open world (if (!IsRoom && OpenNeighbors.Count == 0)).
Room <-> Atmosphere binding¶
Room.AddGrid binds a cell's atmosphere to the room:
public bool AddGrid(WorldGrid grid)
{
if (Grids.Contains(grid))
{
return false;
}
Grids.Add(grid);
RoomController.World.AddOrUpdateRoomLookup(grid, this);
Atmosphere atmosphereLocal = GridController.World.AtmosphericsController.GetAtmosphereLocal(grid);
if (atmosphereLocal != null)
{
atmosphereLocal.Room = this;
}
RoomManager.EvaluateRoomTypeRules(this);
return true;
}
So from a cell you can reach its room two ways: RoomController.World.GetRoom(grid) (the direct map) or AtmosphericsManager.Find(grid).Room (via the atmosphere). The first is cheaper and lock-free.
Finding the atmosphere at a grid cell¶
Read in the 0.2.6428.27798 decompile. AtmosphericsController (in Assets.Scripts, 201657; the world's instance is the static AtmosphericsController.World, 201683) has three instance lookups by cell, and they differ in what they answer where the cell has no atmosphere of its own. A fourth, the static ReadonlyGlobalAtmosphere(Grid3) (201745-201748), always answers the planet's shared reading described below.
GetAtmosphereLocal (201966-201973) answers a crew module's internal atmosphere for a cell inside one, and otherwise the map lookup AtmosphericsManager.Find:
public Atmosphere GetAtmosphereLocal(WorldGrid worldGrid, bool allowCrewModules = true)
{
if (allowCrewModules && Cell.IsInCrewModule(worldGrid, out var crewModule))
{
return crewModule.InternalAtmosphere;
}
return AtmosphericsManager.Find(worldGrid);
}
At this version the map is a ConcurrentDictionary and Find takes no lock (446291, 446572-446576). "AtmosphericsManager: the cell -> atmosphere map" above records the locked Dictionary of 0.2.6228.27061 and was not re-read in this pass:
private static readonly ConcurrentDictionary<WorldGrid, Atmosphere> AllWorldAtmospheresLookUp = new ConcurrentDictionary<WorldGrid, Atmosphere>(Environment.ProcessorCount, 32768);
public static Atmosphere Find(WorldGrid worldGrid)
{
AllWorldAtmospheresLookUp.TryGetValue(worldGrid, out var value);
return value;
}
SampleGlobalAtmosphere (201957-201964) answers the planet's reading when there is no local one:
public Atmosphere SampleGlobalAtmosphere(WorldGrid grid)
{
if (grid == WorldGrid.INVALID)
{
return null;
}
return GetAtmosphereLocal(grid) ?? PlanetaryAtmosphereSimulation.ReadOnlyGlobal(grid);
}
and that reading is one of two shared, read-only objects, not an atmosphere of the cell (PlanetaryAtmosphereSimulation.ReadOnlyGlobal, 221543-221550):
public static Atmosphere ReadOnlyGlobal(WorldGrid wGrid)
{
if (IsInSpaceAtmosphere(wGrid))
{
return _readOnlySpace;
}
return _readOnlyGlobal;
}
_readOnlyGlobal and _readOnlySpace are static fields (221499, 221501), built with the parameterless constructor, so they are never registered, with Mode = Global and a GasMixture set read-only (221670-221685):
_readOnlySpace = new Atmosphere
{
Volume = Chemistry.GridVolume,
GasMixture = GasMixtureHelper.Create(),
Mode = AtmosphereHelper.AtmosphereMode.Global
};
_readOnlyGlobal = new Atmosphere
{
Volume = Chemistry.GridVolume,
Mode = AtmosphereHelper.AtmosphereMode.Global
};
GasMixture gasMixture = _globalGasMix.ToInstancedGasMixture();
gasMixture.Scale((Chemistry.GridVolume / _globalGasMix.VolumeForGas()).ToDouble());
_readOnlyGlobal.GasMixture.Set(gasMixture);
_readOnlyGlobal.GasMixture.SetReadOnly(isReadOnly: true);
_readOnlySpace.GasMixture.SetReadOnly(isReadOnly: true);
CloneGlobalAtmosphere (201931-201955) is the one of the three that creates: it answers the local atmosphere (set Active), or a new world atmosphere at the cell seeded from the planet's global mix, except at or above the space height or while a structure event for the cell is queued (see SprayCan, "Where the pollution lands, and how to read it", for both). It throws on a client. AtmosphericsManager.CloneGlobalAtmosphereThreadSafe (447008-447020) wraps it: a lock-free Find first, then the call under _worldAtmosphereCreationLock. Of the grid-addressed atmospheric events, CloneGlobal (energy), CloneGlobalAddGas (gas, the spray can's pollution among them) and CloneGlobalRemoveEnergy go through it, and DivideWorldAtmosphere uses it for each open neighbour (201543-201606). CreateAndAddWorldAtmos (queued by CreateEmpty and CreateEmptyAndAdd) and StructureReleaseGrid do not: they create with new Atmosphere(_worldGrid, 0L) directly and seed nothing from the planet, apart from StructureReleaseGrid's own conditional TakeGlobalGasMix (201499-201541):
public Atmosphere CloneGlobalAtmosphere(WorldGrid worldGrid, long referenceId = 0L, bool setStateActive = true, bool allowCrewModules = true)
{
if (Assets.Scripts.Networking.NetworkManager.IsClient)
{
throw new System.Exception("Clone Global Atmosphere called on Client");
}
Atmosphere atmosphere = GetAtmosphereLocal(worldGrid, allowCrewModules);
if (referenceId > 0 && atmosphere != null && atmosphere.ReferenceId != referenceId)
{
ConsoleWindow.PrintError("Tried to Create a new World Atmosphere with reference Id '" + StringManager.Get((int)referenceId) + "' at '" + StringManager.GetGridString(worldGrid) + "' Where one already exists");
}
if ((atmosphere != null) & setStateActive)
{
atmosphere.AtmosLifeState = AtmosLifeState.Active;
}
if (atmosphere == null)
{
atmosphere = new Atmosphere(worldGrid, referenceId);
if (!AtmosphericEventInstance.IsGridAwaiting(worldGrid))
{
PlanetaryAtmosphereSimulation.CloneGlobalGasMix(atmosphere);
}
}
return atmosphere;
}
Every atmosphere, and what an atmosphere says about itself¶
Registered atmospheres sit in one pool, AtmosphericsManager.AllAtmospheres (446289). A world atmosphere registers during construction: Init calls AtmosphericsManager.Register (443370-443373), which adds it to the pool and, for World mode, to the cell map (446620-446630). A network or thing atmosphere is queued with RegisterFromMainThread (443288-443291, 443316-443319) and added by HandleMainThreadRegistrations (446594-446618), which the game tick calls (209916). The parameterless constructor registers nothing, which is how the two read-only planet readings are built; a thing atmosphere for a cursor thing (443305) and one built with Atmosphere(long, VolumeLitres) and a zero id (443329) are not registered either. The pool's ForEach holds the pool's own lock for the whole walk (234876-234882), as do its Add, Remove and RemoveWhere (234806-234865):
public static readonly ConcurrentDensePool<Atmosphere> AllAtmospheres = new ConcurrentDensePool<Atmosphere>("AllAtmospheres", 32768);
An atmosphere's Mode (442676) says what it belongs to, and IsGlobalAtmosphere (442649) is the global case:
public AtmosphereHelper.AtmosphereMode Mode { get; set; } = AtmosphereHelper.AtmosphereMode.None;
public bool IsGlobalAtmosphere => Mode == AtmosphereHelper.AtmosphereMode.Global;
public enum AtmosphereMode : byte
{
[XmlEnum("World")]
World,
[XmlEnum("Network")]
Network,
[XmlEnum("Thing")]
Thing,
[XmlEnum("Global")]
Global,
[XmlEnum("None")]
None
}
On a joined client Temperature (442897-442907) answers a cached value, except for a global atmosphere:
public TemperatureKelvin Temperature
{
get
{
if (!Assets.Scripts.Networking.NetworkManager.IsClient || Mode == AtmosphereHelper.AtmosphereMode.Global)
{
return GasMixture.Temperature;
}
return _temperatureCachedClient;
}
}
GasMixture (448349) is a struct with one public Mole field per gas, 28 of them in this order: Oxygen, Nitrogen, CarbonDioxide, Methane, Pollutant, Water, PollutedWater, NitrousOxide, LiquidNitrogen, LiquidOxygen, LiquidMethane, Steam, LiquidCarbonDioxide, LiquidPollutant, LiquidNitrousOxide, Hydrogen, LiquidHydrogen, Hydrazine, LiquidHydrazine, LiquidAlcohol, Helium, LiquidSodiumChloride, Silanol, LiquidSilanol, HydrochloricAcid, LiquidHydrochloricAcid, Ozone, LiquidOzone. GetMoleValue(Chemistry.GasType) (449227) picks one by type, and a Mole's moles are its Quantity (452511, a MoleQuantity; ToDouble() gives the number). The getter answers a cached copy when the mole is cachable and the caller is on the main thread or on a client: AtmosphereHelper.CanWriteAccess is ThreadedManager.IsThread (445494), which is true on any thread but the main one (223418):
public MoleQuantity Quantity
{
get
{
if (IsCachable && (!AtmosphereHelper.CanWriteAccess || Assets.Scripts.Networking.NetworkManager.IsClient))
{
return _quantityCached;
}
return _quantity;
}
set
{
if (!ReadOnly)
{
_quantity = value;
}
}
}
The gas types (Chemistry.GasType, 447336-447401):
[Flags]
public enum GasType : uint
{
[XmlEnum("Undefined")]
Undefined = 0u,
[XmlEnum("Oxygen")]
Oxygen = 1u,
[XmlEnum("Nitrogen")]
Nitrogen = 2u,
[XmlEnum("CarbonDioxide")]
CarbonDioxide = 4u,
[XmlEnum("Volatiles")]
Methane = 8u,
[XmlEnum("Pollutant")]
Pollutant = 0x10u,
[XmlEnum("Water")]
Water = 0x20u,
[XmlEnum("NitrousOxide")]
NitrousOxide = 0x40u,
[XmlEnum("LiquidNitrogen")]
LiquidNitrogen = 0x80u,
[XmlEnum("LiquidOxygen")]
LiquidOxygen = 0x100u,
[XmlEnum("LiquidVolatiles")]
LiquidMethane = 0x200u,
[XmlEnum("Steam")]
Steam = 0x400u,
[XmlEnum("LiquidCarbonDioxide")]
LiquidCarbonDioxide = 0x800u,
[XmlEnum("LiquidPollutant")]
LiquidPollutant = 0x1000u,
[XmlEnum("LiquidNitrousOxide")]
LiquidNitrousOxide = 0x2000u,
[XmlEnum("Hydrogen")]
Hydrogen = 0x4000u,
[XmlEnum("LiquidHydrogen")]
LiquidHydrogen = 0x8000u,
[XmlEnum("PollutedWater")]
PollutedWater = 0x10000u,
[XmlEnum("Hydrazine")]
Hydrazine = 0x20000u,
[XmlEnum("LiquidHydrazine")]
LiquidHydrazine = 0x40000u,
[XmlEnum("LiquidAlcohol")]
LiquidAlcohol = 0x80000u,
[XmlEnum("Helium")]
Helium = 0x100000u,
[XmlEnum("LiquidSodiumChloride")]
LiquidSodiumChloride = 0x200000u,
[XmlEnum("Silanol")]
Silanol = 0x400000u,
[XmlEnum("LiquidSilanol")]
LiquidSilanol = 0x800000u,
[XmlEnum("HydrochloricAcid")]
HydrochloricAcid = 0x1000000u,
[XmlEnum("LiquidHydrochloricAcid")]
LiquidHydrochloricAcid = 0x2000000u,
[XmlEnum("Ozone")]
Ozone = 0x4000000u,
[XmlEnum("LiquidOzone")]
LiquidOzone = 0x8000000u,
[XmlEnum("Air")]
Air = Oxygen | Nitrogen,
[XmlEnum("Fuel")]
Fuel = Oxygen | Methane
}
At this version the neighbour lists are Grid3Buffers (442410-442412), and neither OpenNeighborsForMainThread nor SwapOpenNeighbors appears anywhere in the 0.2.6428.27798 decompiles; "Atmosphere fields" above is stamped at 0.2.6228.27061 and was not re-read in this pass:
public readonly Grid3Buffer OpenNeighbors = new Grid3Buffer();
public readonly Grid3Buffer ClosedNeighbors = new Grid3Buffer();
A dynamic thing's cached atmosphere¶
A DynamicThing keeps a reference to an atmosphere, WorldAtmosphere (304574-304592). The getter drops the reference once IsInvalidWorld() answers true for it, which happens to a World-mode atmosphere whose RegisteredWorldGrid no longer matches its WorldGrid (443474-443485); AtmosphericsManager.Deregister sets RegisteredWorldGrid to WorldGrid.INVALID (446650):
public Atmosphere WorldAtmosphere
{
get
{
if (_worldAtmosphere == null)
{
return null;
}
if (_worldAtmosphere.IsInvalidWorld())
{
_worldAtmosphere = null;
}
return _worldAtmosphere;
}
set
{
_worldAtmosphere = value;
}
}
public bool IsInvalidWorld()
{
if (Mode == AtmosphereHelper.AtmosphereMode.World)
{
if (!(RegisteredWorldGrid != WorldGrid))
{
return WorldGrid == WorldGrid.INVALID;
}
return true;
}
return false;
}
The general way it is set is SetWorldAtmosphere (305415-305453). For a thing in a slot: a crew module's air in a rocket interior, the parent's internal atmosphere for a slot that uses it, the parent's own WorldAtmosphere for a dynamic parent, and otherwise the crew module's air or SampleGlobalAtmosphere at the root parent's cell. For a thing in no slot: the crew module's air or SampleGlobalAtmosphere at its own cell. SampleGlobalAtmosphere answers the planet's shared read-only reading when the cell has no atmosphere:
public void SetWorldAtmosphere()
{
Slot parentSlot = ParentSlot;
if (parentSlot != null)
{
if (parentSlot.Parent is IRocketInterior rocketInterior && rocketInterior.CrewModule != null)
{
WorldAtmosphere = rocketInterior.CrewModule.InternalAtmosphere;
return;
}
if (parentSlot.UseInternalAtmosphere)
{
WorldAtmosphere = parentSlot.Parent.InternalAtmosphere;
return;
}
if ((bool)parentSlot.Parent.AsDynamicThing)
{
WorldAtmosphere = parentSlot.Parent.AsDynamicThing.WorldAtmosphere;
return;
}
Thing thing = parentSlot.Parent?.RootParent ?? this;
Cell cell = base.GridController.GetCell(thing.WorldGrid);
WorldAtmosphere = (Cell.IsInCrewModule(cell, out var crewModule) ? crewModule.InternalAtmosphere : base.GridController.AtmosphericsController.SampleGlobalAtmosphere(thing.WorldGrid));
return;
}
WorldAtmosphere = (Cell.IsInCrewModule(Cell, out var crewModule2) ? crewModule2.InternalAtmosphere : base.GridController.AtmosphericsController.SampleGlobalAtmosphere(base.WorldGrid));
if (_hasAtmosphere != HasAtmosphere)
{
if (HasAtmosphere)
{
OnAtmosphereGain();
}
else
{
OnAtmosphereLost();
}
_hasAtmosphere = HasAtmosphere;
}
}
SetWorldAtmosphere has two callers, both in OcclusionManager.OcclusionDynamicThingAction: 220816 for a thing in a slot, right after its WorldGrid is set to the root parent's centre, and 220824 for a thing in no slot (see SprayCan for the excerpt). That delegate is the occlusion manager's dynamic-thing pass (ThreadedWork -> ThreadedWorkDynamicThingSync -> AllDynamicThings.ForEach, 221230-221277), run on the manager's own thread (ThreadedManager.ThreadedLoop, 223497-223514), while the game state is Running, at most once per rendered frame (221233-221245), for every DynamicThing (each registers itself in Awake, 306443). Both calls come after the delegate's if (GameManager.RunSimulation) block has closed (220791-220805), so every peer makes them, a joined client included, and the dedicated server's copy of the delegate is textually identical.
Other code writes WorldAtmosphere directly. A burning dynamic thing takes AtmosphericsManager.CloneGlobalAtmosphereThreadSafe at its cell (306383). An Entity with no BreathingAtmosphere clones one at its cell (LifeBreathe, 308877-308879), and Human's BreathingAtmosphere setter writes WorldAtmosphere unless its helmet internals are on (366884-366893). Several DynamicThing subclasses that emit into or sample their cell assign it themselves, for example DynamicScrubber (303924), RoadFlare (360434), Jetpack (355486, at its parent's cell) and WeldingTorch (363367).
So the reference is only as fresh as the last pass or write: an atmosphere created at the cell since then (a spray can's use creates one where there was none) is not what a slotted item's WorldAtmosphere points at until the pass has run again, and until then it can be the shared global reading. On a joined client it is the client's own copy of an atmosphere, never the authority's object. A reading that has to be current looks the cell up instead.
Verification history¶
-
2026-05-22: page created from a room/grid/atmospherics decompile pass (feasibility study on networking composite cladding for SprayPaintPlus). Sections verified against
Assets.Scripts.Atmospherics.AtmosphereandAssets.Scripts.Atmospherics.AtmosphericsManagerinAssembly-CSharp, game version 0.2.6228.27061. New page; no conflicts. -
2026-09-27: added "Finding the atmosphere at a grid cell" (the three instance lookups by cell and the static fourth, the lock-free
ConcurrentDictionarybehindFindat this version, the two shared read-only planet readings and how they are built, the creating lookup, its refusal on a client, its thread-safe wrapper, and which grid-addressed atmospheric events go through it), "Every atmosphere, and what an atmosphere says about itself" (which atmospheres register in the pool and how, the pool's lock,Mode, the client's cachedTemperature, the 28 gases ofGasMixture, the cachedMole.Quantitya main-thread or client reader gets, andChemistry.GasType) and "A dynamic thing's cached atmosphere" (WorldAtmosphere,IsInvalidWorld,SetWorldAtmosphereand its only two callers in the occlusion pass, the other code that writes the reference, and how stale it can be), all from the 0.2.6428.27798 decompile. Drafted while building a test-rig atmosphere reader and re-read line by line before landing. Four draft claims were wrong and were corrected before landing: the refresh of a dynamic thing's reference is not authority-only (the occlusion pass runs on every peer);SetWorldAtmosphereis not the only writer; not every grid-addressed event goes throughCloneGlobalAtmosphere(CreateAndAddWorldAtmosandStructureReleaseGridcreate directly); and not every live atmosphere is in the pool (the two read-only planet readings never register). Additive to the older sections. Noted without re-verifying them: at 0.2.6428.27798 the cell map is aConcurrentDictionaryread without a lock,OpenNeighborsandClosedNeighborsareGrid3Buffers, andOpenNeighborsForMainThreadandSwapOpenNeighborsno longer appear, so "AtmosphericsManager: the cell -> atmosphere map" and "Atmosphere fields" describe 0.2.6228.27061 only. The top-levelverified_inmoved to 0.2.6428.27798 for the new sections.
Open questions¶
None at creation.