Atmospherics has been in a rough state. It has been through several refactors, both by its original SS14 creator, and by other maintainers following suit.Major changes have been made that have silently broken core atmospherics features, which have impacted the overall game negatively to a point where poor design decisions were made to counteract the percieved effects.A recent effort is being undergone by Maintainers (mainly just me, Roomba) to fully redocument and write tests for Atmospherics.
As such, these guidelines represent all of the lessons I have learned so far in fixing, maintaining, and expanding Atmospherics.
General Coding Conventions
Re-assertions
You should be familiar with the projects Coding Conventions and their core tenants of:- Do not copy-paste code.
- Do not use magic strings/numbers.
- Comment your code’s purpose fully, don’t just comment what it does.
All members must be documented regardless of access level
All code should be documented, evenprivate members, even if they are simple helper methods or datafields.
This greatly assists maintainers in reviewing your code, and helps contributors establish the bigger picture on how Atmospherics functions.
Generally, public-facing methods should have a proper summary, parameter documentation, caveats to consider, method return documentation if applicable, as well as an example if applicable. Extending this level of documentation to private members is appreciated.
Subsystems should reside in their own partial class
Subsystems refer to the routines that Atmospherics performs during an update. These are the big processing states, likeRevalidate, TileEqualize, ExcitedGroups, DeltaPressure, etc.
Most of the logic relating to these subsystems should reside in their own partial class in AtmosphereSystem under the format AtmosphereSystem.<Subsystem>.cs.
Any [Dependency] imports should reside in the root AtmosphereSystem partial.
Any consts or private fields that are heavily used or control configuration for said subsystem should reside in the subsystem’s partial class.
If these const fields are useful on the client, they should belong in the Atmospherics static class.
Avoid god methods when possible
Do not have a horrifyingly long (ex. 350+ LOC) method that performs the bulk of your subsystem logic. Split your method up into distinct processing steps that your main method calls. This may allow you to turn your helper method into generic methods that can be called throughout Atmospherics. For example, if you’ve derived a useful mathematical function that assists in equalizing pressure between twoGasMixtures, make that a public method that systems can call in AtmosphereSystem.API.
Mathematical derivations must be properly documented
Documentation should be provided for your mathematical derivations for any functions you made. The documentation should be embedded in the code as a multi-line comment and be formatted in a semi-readable way. Pseudo-LaTeX notation is preferred. While self-documenting variable named and single-line comments on each step is always desired, a general overview on the steps you took to reach your solution is preferred.New additions should have tests
Whether it be a new API method or a new Atmospherics feature, the implementation should have tests that properly cover the changes. Tests are invaluable for Atmospherics, as most of it is untested, and indirect changes have significantly disrupted core mechanics before. There is an available test helper classAtmosTest that can help you write tests for your feature.
Performance improvements should be demonstrated by a benchmark
It has been proven multiple times (via my own performance improvement attempts, and others as well) that some performance changes have little to no impact. This is unfortunate as most performance changes made the code more complicated and hard to read, whether it be via multithreading, branchless code, or vectorized code. As such, all performance improvements should be demonstrated by a benchmark. You’ll find that this is effectively required for meaningfully iterating on any performance goal you’d like to achieve. It also greatly helps out future contributors trial out their own performance tweaks, and reduces hypothetical-posting for content changes that may decrease performance.Do not hide potential numerical instability or noise
When writing code for Atmospherics, do not ignore potential numerical instability, as this could hide bad edge-cases during testing or during gameplay. For example, if dividing aHeatContainer into $ n $ parts, ensure that $ n $ is a uint and use ArgumentOutOfRangeException helpers to throw if $ n = 0 $:
- Debug assert (
DebugTools.AssertorDebugTools.AssertNotNull) your condition. - Demonstrate that whatever logic you skip is worth skipping using a benchmark.
Large features or processing states should be as configurable as possible
When adding a new large feature, ensure that the specific processing state can be disabled via a CVAR.const values that aren’t performance critical or shouldn’t be changed often can be made into a CVAR.
This allows forks to enable, disable, or otherwise configure certain Atmospherics stages to their liking.
A fork shouldn’t have to pull out the entire subsystem if they want to disable it.
Don’t tack on major features to an existing processing state
When adding new behavior for a processing state, ensure that you are not effectively hardcoding more checks and logic handling. This makes the logic inflexible, because if anyone wants to add another behavior onto the feature, they likely have to append even more checks onto the checks you appended yourself. For example, take theHotspot system, which checks if a tile is allowed to start a gas fire on itself:
Hotspot logic should be reworked so that both gas states and reagent states can contribute to a general flammability score that increases the temperature of the tile.
This allows future contributors to simply add more mechanics that contribute to a tile’s overall flammability (ex. carpet or wooden furniture, a Diona standing on the tile, etc.).
Hardcoding more checks into the subsystem instead of making your own bespoke subsystem or making contributions generic also means that it wil be harder to make your feature configurable/disablable.
Atmospherics logic should be mindful of its time budget
Atmospherics spans its processing over multiple ticks in order to soften the blow that it makes toEntitySystem tick timing.
Because of this, subsystems have to be mindful of how much time they’ve taken since the tick began, and yield processing to the next processing tick when necessary.
This is commonly done via the simulation stopwatch in AtmosphereSystem._simulationStopwatch.
Processing states will inspect this stopwatch every few seconds and yield if processing time exceeded the budget for the tick:
Reuse memory whenever possible - avoid allocating on the heap for simulation
When writing code for Atmospherics, be mindful of any heap allocations that you make, even if it’s something like aforeach enumeration or a List<T>.
Object allocations put unnecessary pressure on the GC, especially if said object allocations are in simulation code, code that is running per atmostick.
As such, reuse memory like arrays as much as possible.
For example, AtmosphereSystem.Monstermos holds arrays for queueing tiles and computing pressure spreading:
const values as per the previously mentioned conventions.
Since you’re usually writing for Content.Server and Content.Server only, you can take advantage of stackalloc and ArrayPool.
Note that you should always be very careful when using stackalloc as your desired array size for your data has the potential to overflow the stack.
In multithreading contexts (ex. IParallelRobustJob), it is preferable to use ArrayPool to quickly rent arrays for use.
Be sure to always return them in the event of an exception.