Figure 1.1: My avatar on github, the /tg/station forums, and BYOND
Hi friends! My name is Dunc. As some background, I have been contributing to /tg/ atmos in various ways since 2015. I was tutored by Aranclanos and eventually followed in his footsteps in writing several major refactors of the atmos system. I am a maintainer for the /tg/ codebase and any atmos issues or PRs are my concern. Most importantly, however, is the fact that as of the time of writing I am the only person who understands each component of the current atmos system. That fact is by far the most important reason for the existence of this document; it is my responsibility to share what I know.
Atmospherics is a very complicated and intimidating system of SS13, and as such very few contributors have ever made changes to it. Even fewer is the number of contributors who have made changes to the more fundamental aspects of atmos, such as Environmental Atmos or gas mixtures. There are several other factors for this, of course. In the case of Environmental, its arcane nature coupled with its extremely important gameplay effects leave it a very undesirable target for even the least sane coder. As for gas mixtures, they were virtually untouchable without extensive reworks of the code. This pastebin is a good example; it lists all the files one would need to make changes in order to add a new type of gas in the old system. As you can imagine, the sheer bulk of work one would need to do to accomplish this essentially invalidated any such attempts. However, my primary goal since beginning to code for /tg/ has been to bring atmos to a state where any coder will be able to understand how and why it works, as well as cleanly and relatively easily make changes or additions to the system. While much progress to this end has been achieved, still very few have taken advantage of the new frameworks to try to implement meaningful features or changes. The purpose of this document is to lay out the inner workings of the entire atmos system, such that someone who does not have an intimate understand of the system like myself will be able to contribute to the system nonetheless.
Recognizing this desire, I hope and believe that you who are reading this are willing to learn and contribute.
2. Introduction to Atmos
3. The Air Controller
Figure 3.1: the structure of one air controller tick
The air controller is, at its core, quite simple, yet it is absolutely fundamental to the atmospheric system. The air controller is the clock which triggers all continuous actions within the atmos system, such as vents distributing air or gas moving between tiles. The actions taken by the air controller are quite simple, and will be enumerated here. Much of the substance of the air ticker is due to the game's master controller, whose intricacies I will not delve into for this document. As such, this is a simplified list of the air controller's actions in a single tick:
/obj/machinery/atmospherics) in the
atmos_machinerylist, and may remove the machinery from said list if
- Active turfs
- Excited groups
- Increases the
- If either cooldown for a given excited group has passed its threshold, calls
dismantle()appropriately on the excited group.
- Increases the
- High pressure deltas
- Sets each turf's
4. Gas Mixtures
If the air controller is the heart of atmos, then gas mixtures make up its blood. The bulk of all atmos calculations are performed within a given gas mixture datum (an instance of
/datum/gas_mixture), be it within a turf or within an emergency oxygen tank or within a pipe. In particular,
/datum/gas_mixture/proc/share() is the cornerstone of atmos simulation, as it and its stack perform all the calculations for equalizing two gas mixtures.
Gas mixtures contain some of the oldest code still in our codebase, and it is remarkable that overall, the logic behind the majority of gas mixture procs has gone unchanged since the days of Exadv1. Despite being in some sense "oldcode", the logic itself is quite robust and based in real world physics. Thankfully, gas mixtures already are quite well documented in terms of their behavior. Their file is well commented and kept up to date. I will, however, elaborate on some of the less obvious operations here. Additionally, I will document the structure of gas lists, and how one should interface with a gas mixture should you choose to use one in other code.
//transfer of thermal energy (via changed heat capacity) between self and sharer if(new_self_heat_capacity > MINIMUM_HEAT_CAPACITY) temperature = (old_self_heat_capacity*temperature - heat_capacity_self_to_sharer*temperature_archived + heat_capacity_sharer_to_self*sharer.temperature_archived)/new_self_heat_capacity
Snippet 4.1: excerpt from
The snippet above is an example of one particularly strange looking calculation. This part of
share() is updating the temperature of a gas mixture to account for lost or gained thermal energy as gas moves to/from the mixture, since gases themselves carry heat. To understand this snippet, it is important to understand the difference between heat and temperature. For the most part, the average coder need only concern himself with temperature, as it is a familiar experience for anybody. However, internally in atmos, heat (thermal energy) is the truly important quantity. Heat is defined as temperature multiplied by heat capacity, and is measured in joules. Typically within atmos, we are more concerned with manipulating heat than temperature; however, temperature is tracked rather than heat largely to make interfacing with the system simpler for the average coder. Thus, this snippet modifies heat in terms of temperature - it adds/subtracts three terms, each of which measure heat, to determine the new heat in the gas mixture. This heat is then divided by the mixture's heat capacity in order to determine temperature.
One trick to understanding passages like this is to do some simple dimensional analysis. Look only at the units, and ensure that whenever a variable is assigned that it is being assigned the appropriate unit. The snippet previously discussed can be represented with the following units:
temperature = ((J/K)*K - (J/K)*K + (J/K)*K)/(J/K). Simplified, you get
(J-J+J)*K/J and then simply
K, verifying that temperature is being set to a value in kelvins. This trick has proven invaluable to me when debugging the inner workings of gas mixtures.
The true beauty of the gas mixture datum is how it represents the gases it contains. A bit of history: gas mixtures used to represent gas in two ways - there were the four primary gases (oxygen, nitrogen, carbon dioxide, and plasma) which were hardcoded. Each gas mixture had two vars (moles and archived moles, a concept to be explained later) to represent each of these gases. Calculations such as thermal energy made use of predefined constants for these hardcoded gases. The benefit of this was that they were extremely quick - only a single datum var access was needed for each one. In contrast, there were trace gases, for which there were a list of gas datums. The only trace gas available in normal gameplay was nitrous oxide (N2O or sleeping agent), though through adminnery it was possible to create oxygen agent B and volatile fuel, curious gases which will be described later for historical reasons. Trace gases, in contrast to hardcoded gases, were quite modular. To add a new trace gas one needed only to define a new subtype of
/datum/gas and add appropriate behavior wherever desired, such as breath code. Unfortunately, of course, trace gases were slooooow. Calculations on trace gases were significantly more costly than hardcoded gases. The problem was obvious - it seemed impossible to have a gas definition which shared the modularity of trace gases without sacrificing too much of the performance of the hardcoded gases.
What then to do? There was no option to port an improvement from another codebase. As far as I am aware, there have been no significant downstream improvements to gas mixtures. The other major upstream codebase, Baystation12, uses a very different atmos system; in particular, their XGM gas mixtures have their own solution to this problem. To summarize XGM, there is a singleton which has associative lists of gas metadata (information such as specific heat, or which overlay to display when the gas is present) which gets accessed whenever such information is needed. To count moles, each gas mixture has an associative list of gas ids mapped to mole counts. There were a couple of problems with this approach:
- There was no measure of archived moles. While it would be easy to simply add a second associative list, this has non-trivial memory implications as well as a potential increase to total datum var accesses within internal atmos calculations.
- The singleton used for storing metadata helps with the memory impact that using full datums would have, but does not properly address the cost of datum var accesses, as to access metadata you must still access a datum var on the singleton.
For some time, without a clear solution, we simply stuck to the status quo and left gases non-modular. Eventually, however, there was an idea.
The Gas List
The solution we came to was beautifully simple, but founded on some unintuitive principles. While datum var accesses are quite slow, proc var accesses are acceptable. If we use a reference for a given var, this can be exploited by "caching" the reference inside of a proc var. How can we take advantage of this without using a datum, thus nullifying the benefit?
The answer was to use a list. The critical realization was that a gas datum functioned moreso as a struct than as a class. There were no procs attached to gas datums; only vars. While DM lacks a true struct with quick lookup times, a list works very well to perform the same function. Thus, the current structure of gas was created, under the name Listmos.
Each gas mixture has an associative list,
gases, which maps according to a key to a particular gas. This gas is itself a list (not an associative list, mind) with three elements; these elements correspond to the moles, archived moles, and to another list. This final list is a singleton - only one instance of it exists per gas, and all gas instances of a particular type point to this same list as their third element. The final list contains the metadata for the gas, such as specific heat or the name of the gas. The structure of the metadata list varies according to how many attributes are defined overall for all gases, but it is also non-associative since the structure can never change post-compile, so we save a little bit of performance by avoiding associative lookups.
Each type of gas is defined by defining a new subtype of
/datum/gas. These datums do not get instantiated; they merely serve as a convenient and familiar means for a coder unfamiliar with the inner workings of listmos to define a new gas. Additionally, the type paths serve a second use as the keys used to access a particular gas within the
gases list. It is easiest to demonstrate the manipulation of gas, including these list accesses, with an example.
Interfacing with a Gas Mixture
var/datum/gas_mixture/air = new air.assert_gas(/datum/gas/oxygen) air.gases[/datum/gas/oxygen][MOLES] = 100 world << air.gases[/datum/gas/oxygen][GAS_META][META_GAS_NAME] //outputs "Oxygen" world << air.gases.heat_capacity() //outputs 2000 (100 mol * 20 J/K/mol) air.gases[/datum/gas/oxygen][MOLES] -= 110 air.garbage_collect() //oxygen is now removed from the gases list, since it was empty
Snippet 4.2: gas mixture usage examples
Of particular note in this snippet are the two procs
garbage_collect(). These procs are very important while interfacing with gas mixtures. If you are uncertain about whether a given mixture has a particular gas, you must use
assert_gas() before any reads or writes from the gas. If you fail to use
assert_gas() then there will be runtime errors when you try to access the inner lists. When you remove any number of moles from a given gas, be sure to call
garbage_collect(). This proc removes all gases which have mole counts less than or equal to 0. This is a memory and performance enhancement to list accesses by reducing the size of the list, and also saves us from having to do sanity checks for negative moles whenever gas is removed. As a quick reference, here is a list of common procs/vars/list indices which the average coder may wish to use when interfacing with a gas mixture.
Gas Mixture Datum
/datum/gas_mixture/proc/assert_gas()- Used before accessing a particular type of gas.
/datum/gas_mixture/proc/assert_gases()- Shorthand for calling
/datum/gas_mixture/proc/garbage_collect()- Used after removing any number of moles from a mixture.
/datum/gas_mixture/proc/return_pressure()- Pressure is what should be displayed to players to quanitfy gas; measured in kilopascals.
/datum/gas_mixture/var/temperature- Measured in kelvins. Useful constants are
T20Cfor 0 and 20 degrees Celsius respectively, and
TCMB,the temperature of space and the lower bound for temperature in atmos.
/datum/gas_mixture/var/volume- Measured in liters.
gases[path][MOLES]- Quantity of a particular gas within a mixture.
gases[path][GAS_META][META_GAS_NAME]- The long name of a gas, ex. "Oxygen" or "Hyper-noblium"
gases[path][GAS_META][META_GAS_ID]- The internal ID of a given gas, ex. "o2" or "nob"
While defining a new gas on its own is very simple, there is no gas-specific behavior defined within
/datum/gas. This behavior gets defined in a few places, notably breath code (to be discussed later) and in reactions. The most important and well known reaction in SS13 is fire - the combustion of plasma. Reactions are used for several things - in particular, it is conventional (though by no means enforced) that to form a gas, a reaction must occur. Creating a new reaction is fairly simple - so simple, indeed, that even iamgoofball was able to do it. :^)
There are two procs needed when defining a new reaction,
init_reqs() initializes the requirements for the reaction to occur. There are two lists,
max_reqs, which map gas paths to number of moles. They also map two specific strings (
"ENER") to temperature in kelvins and thermal energy in joules. It is important to note that, currently,
max_reqs is not enabled. The code to handle it is commented out in
/datum/gas_mixture/proc/react() for the sake of improving performance while no reactions have a maximum requirement. Should you wish to enable
max_reqs() simply uncomment the code.
react(), it is where all the behavior of the reaction is defined. The proc must return one of
STOP_REACTIONS. The proc takes one or optionally two arguments. The first, mandatory, argument is a gas mixture on which to perform calculations; this mixture is what is reacting. The second, optional, argument is a turf, specifically the turf which contains the gas mixture. You may choose for the reaction to affect the turf in some way. Note that it is conventional for constants within reactions to be
#define'd at the top of the file and
#undef'd at the end.
5. Environmental Atmos
6. Atmos Machinery
Appendix A - Glossary
- Carbon dioxide - What the fuck is this?