Перейти к основному содержанию
Этот документ описывает рекомендации по отправке изменений в атмосферику. Как и в случае с проектом Coding Conventions, атмосферный код имеет особые требования, которые следует соблюдать при создании новых функций или написании исправлений.
Атмосферика находится в плохом состоянии. Она прошла через несколько рефакторингов, как от оригинального создателя SS14, так и от последующих мейнтейнеров.Были сделаны серьёзные изменения, которые молча сломали ключевые функции атмосферики, что негативно сказалось на игре в целом, до такой степени, что были приняты плохие дизайнерские решения для противодействия предполагаемым эффектам.В настоящее время мейнтейнеры (в основном я, Roomba) предпринимают усилия по полному передокументированию и написанию тестов для атмосферики. Таким образом, эти рекомендации представляют собой все уроки, которые я извлек до сих пор при исправлении, поддержке и расширении атмосферики.

Общие правила написания кода

Повторные утверждения

Вы должны быть знакомы с Coding Conventions проекта и их основными принципами:
  • Не копируйте код.
  • Не используйте магические строки/числа.
  • Полностью комментируйте назначение вашего кода, не просто комментируйте, что он делает.

Все члены должны быть документированы независимо от уровня доступа

Весь код должен быть документирован, даже private члены, даже если это простые вспомогательные методы или поля данных. Это очень помогает мейнтейнерам при проверке вашего кода и помогает контрибьюторам понять общую картину того, как функционирует атмосферика. В целом, публичные методы должны иметь надлежащее описание, документацию параметров, предостережения, документацию возвращаемого значения (если применимо), а также пример (если применимо). Распространение такого уровня документации на private члены приветствуется.
/// <summary>
/// Вычисляет безразмерную долю газа, необходимую для выравнивания давления между двумя газовыми смесями.
/// </summary>
/// <param name="gasMixture1">Первая газовая смесь, участвующая в выравнивании давления.
/// Эта смесь должна быть той, которая, как вы всегда ожидаете, будет иметь наибольшее давление.</param>
/// <param name="gasMixture2">Вторая газовая смесь, участвующая в выравнивании давления.</param>
/// <returns>Число с плавающей запятой (от 0 до 1), представляющее безразмерную долю газа, которую необходимо передать из
/// смеси с более высоким давлением в смесь с более низким давлением.</returns>
/// <remarks>
/// <para>
/// Это должным образом учитывает эффект
/// слияния газа от входа к выходу, влияющий на температуру
/// (и, возможно, увеличивающий давление) на выходе.
/// </para>
/// <para>
/// Предполагается, что газ расширяется свободно,
/// поэтому температура газа с большим давлением не меняется.
/// </para>
/// </remarks>
/// <example>
/// Если вы хотите вычислить моли, необходимые для выравнивания давления между входом и выходом,
/// умножьте возвращаемую долю на исходные моли.
/// </example>
public float FractionToEqualizePressure(GasMixture gasMixture1, GasMixture gasMixture2) {...}

Подсистемы должны находиться в своём собственном partial-классе

Подсистемы относятся к процедурам, которые атмосферика выполняет во время обновления. Это большие состояния обработки, такие как Revalidate, TileEqualize, ExcitedGroups, DeltaPressure и т.д. Большая часть логики, относящейся к этим подсистемам, должна находиться в своём собственном partial-классе в AtmosphereSystem в формате AtmosphereSystem.<Subsystem>.cs. Любые импорты [Dependency] должны находиться в корневом partial AtmosphereSystem. Любые const или приватные поля, которые интенсивно используются или управляют конфигурацией указанной подсистемы, должны находиться в partial-классе подсистемы. Если эти const поля полезны на клиенте, они должны находиться в статическом классе Atmospherics.

Избегайте god-методов, когда возможно

Не используйте ужасающе длинные (например, 350+ строк) методы, выполняющие основную часть логики вашей подсистемы. Разделите ваш метод на отдельные этапы обработки, которые вызывает ваш основной метод. Это может позволить превратить ваш вспомогательный метод в общие методы, которые можно вызывать по всей атмосферике. Например, если вы вывели полезную математическую функцию, которая помогает выравнивать давление между двумя GasMixture, сделайте её публичным методом, который системы могут вызывать в AtmosphereSystem.API.

Математические выкладки должны быть должным образом документированы

Для ваших математических выкладок должна быть предоставлена документация для любых функций, которые вы создали. Документация должна быть встроена в код в виде многострочного комментария и отформатирована в получитаемом виде. Предпочтительна нотация, похожая на LaTeX. Хотя само-документируемые имена переменных и однострочные комментарии на каждом шаге всегда желательны, предпочтителен общий обзор шагов, которые вы предприняли для достижения вашего решения.
public float FractionToEqualizePressure(GasMixture gasMixture1, GasMixture gasMixture2)
{
    /*
    Проблема: газ, сливающийся со входа на выход, может повлиять на
    температуру газа и вызвать повышение давления.
    Мы хотим, чтобы давление было выравнено, поэтому мы должны учесть это.

    Для ясности предположим, что gasMixture1 — это вход, а gasMixture2 — это выход.

    Нам требуется механическое равновесие, поэтому $ P_1' = P_2' $

    До передачи у нас есть:
    $ P_1 = \frac{n_1 R T_1}{V_1} $
    $ P_2 = \frac{n_2 R T_2}{V_2} $

    После удаления доли $ x $ молей со входа, у нас есть:
    $ P_1' = \frac{(1 - x) n_1 R T_1}{V_1} $
    
    [...]

Новые добавления должны иметь тесты

Будь то новый метод API или новая функция атмосферики, реализация должна иметь тесты, которые должным образом охватывают изменения. Тесты неоценимы для атмосферики, так как большая её часть не протестирована, а косвенные изменения ранее значительно нарушали ключевые механики. Существует доступный вспомогательный класс тестов AtmosTest, который может помочь вам написать тесты для вашей функции.

Улучшения производительности должны быть подтверждены бенчмарком

Было доказано несколько раз (моими собственными попытками улучшения производительности и другими), что некоторые изменения производительности имеют незначительный эффект или не имеют его вовсе. Это прискорбно, так как большинство изменений производительности делали код более сложным и трудным для чтения, будь то через многопоточность, код без ветвлений или векторизованный код. Таким образом, все улучшения производительности должны быть подтверждены бенчмарком. Вы обнаружите, что это фактически необходимо для осмысленной итерации к любой цели производительности, которую вы хотите достичь. Это также очень помогает будущим контрибьюторам опробовать свои собственные изменения производительности и уменьшает количество гипотетических постов для изменений контента, которые могут снизить производительность.

Не скрывайте потенциальную числовую нестабильность или шум

При написании кода для атмосферики не игнорируйте потенциальную числовую нестабильность, так как это может скрыть плохие крайние случаи во время тестирования или геймплея. Например, если вы делите HeatContainer на $ n $ частей, убедитесь, что $ n $ является uint, и используйте вспомогательные методы ArgumentOutOfRangeException, чтобы выбросить исключение, если $ n = 0 $:
public static HeatContainer[] Divide(this HeatContainer c, uint num)
{
    ArgumentOutOfRangeException.ThrowIfZero(num);

    var fraction = 1f / num;
    [...]
Если ваш численный метод имеет естественную числовую нестабильность, которая не должна проявляться в геймплее (например, взорванная экспонента), обязательно либо зафиксируйте потенциальное значение до известного диапазона хороших значений, либо вернитесь к более стабильному методу, записывая ошибку для отслеживания. Неотслеженная числовая нестабильность приводит к более болезненной отладке, так как проблема распространяется вниз по стеку вызовов. Это может легко привести к взрыву длительных этапов симуляции в атмосферике. Если вы делаете предположение в вашем коде (например, пропускаете проверку на null, потому что знаете, что ваши данные не null), обязательно:
  • Сделайте Debug assert (DebugTools.Assert или DebugTools.AssertNotNull) вашего условия.
  • Продемонстрируйте с помощью бенчмарка, что пропущенная логика стоит того, чтобы её пропустить.

Большие функции или состояния обработки должны быть максимально конфигурируемыми

При добавлении новой большой функции убедитесь, что конкретное состояние обработки может быть отключено через CVAR. Значения const, которые не критичны для производительности или не должны часто меняться, могут быть сделаны CVAR. Это позволяет форкам включать, отключать или иным образом настраивать определённые этапы атмосферики по своему усмотрению. Форк не должен вытаскивать всю подсистему, если он хочет её отключить.

Не прикрепляйте крупные функции к существующему состоянию обработки

При добавлении нового поведения для состояния обработки убедитесь, что вы не вкодируете дополнительные проверки и обработку логики. Это делает логику негибкой, потому что, если кто-то захочет добавить ещё одно поведение к функции, ему, вероятно, придётся добавить ещё больше проверок к тем, что вы добавили сами. Например, система Hotspot, которая проверяет, разрешено ли тайлу начинать газовый пожар:
if ((tile.Hotspot.Temperature < Atmospherics.FireMinimumTemperatureToExist) ||
    (tile.Hotspot.Volume <= 1f) ||
    tile.Air == null ||
    tile.Air.GetMoles(Gas.Oxygen) < 0.5f ||
    (tile.Air.GetMoles(Gas.Plasma) < 0.5f &&
    tile.Air.GetMoles(Gas.Tritium) < 0.5f))
{...}
Не добавляйте больше проверок в эту систему, чтобы сделать возможными пожары реагентов, как показано ниже:
if ((tile.Hotspot.Temperature < Atmospherics.FireMinimumTemperatureToExist) ||
    (tile.Hotspot.Volume <= 1f) ||
    tile.Air == null ||
    tile.Air.GetMoles(Gas.Oxygen) < 0.5f ||
    (tile.Air.GetMoles(Gas.Plasma) < 0.5f &&
    tile.Air.GetMoles(Gas.Tritium) < 0.5f)) &&
    tile.SolutionFlammability == 0))
{...}
Вместо этого логика Hotspot должна быть переработана так, чтобы и газовые состояния, и состояния реагентов могли вносить вклад в общий показатель воспламеняемости, который увеличивает температуру тайла. Это позволит будущим контрибьюторам просто добавлять больше механик, которые вносят вклад в общую воспламеняемость тайла (например, ковёр или деревянная мебель, Диона, стоящая на тайле и т.д.). Вкодирование большего количества проверок в подсистему вместо создания собственной специализированной подсистемы или обобщения вкладов также означает, что будет сложнее сделать вашу функцию настраиваемой/отключаемой.

Логика атмосферики должна учитывать свой временной бюджет

Атмосферика распределяет свою обработку на несколько тиков, чтобы смягчить удар по времени тика EntitySystem. Из-за этого подсистемы должны учитывать, сколько времени они уже потратили с начала тика, и при необходимости уступать обработку следующему тику. Это обычно делается через секундомер симуляции в AtmosphereSystem._simulationStopwatch. Состояния обработки проверяют этот секундомер каждые несколько итераций и уступают, если время обработки превысило бюджет для тика:
private bool ProcessHotspots(
    Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent)
{
    var atmosphere = ent.Comp1;
    if(!atmosphere.ProcessingPaused)
        QueueRunTiles(atmosphere.CurrentRunTiles, atmosphere.HotspotTiles);

    var number = 0;
    while (atmosphere.CurrentRunTiles.TryDequeue(out var hotspot))
    {
        ProcessHotspot(ent, hotspot);

        if (number++ < LagCheckIterations)
            continue;

        number = 0;
        // Остальное обработаем в следующий раз.
        if (_simulationStopwatch.Elapsed.TotalMilliseconds >= AtmosMaxProcessTime)
        {
            return false;
        }
    }

    return true;
}
Таким образом, ваша подсистема не должна быть спроектирована так, чтобы выполнять весь этап симуляции за один раз; она должна иметь возможность приостанавливать и возобновлять свою обработку в течение многих тиков. Многие системы достигают этого, обрабатывая то, что нужно сделать, на единицу за раз и периодически проверяя секундомер, чтобы узнать, не вышло ли время. Обратите внимание, что это усложняется для многопоточных и/или векторизованных этапов обработки — следует использовать бенчмаркинг, чтобы гарантировать, что отдельные прогоны этапа обработки не превышают максимально допустимое время.

Переиспользуйте память, когда возможно — избегайте аллокаций в куче для симуляции

При написании кода для атмосферики следите за любыми аллокациями в куче, даже если это что-то вроде перечисления foreach или List<T>. Аллокации объектов создают ненужное давление на GC, особенно если эти аллокации находятся в коде симуляции, коде, который выполняется каждый атмосферный тик. Поэтому переиспользуйте память, такую как массивы, насколько это возможно. Например, AtmosphereSystem.Monstermos содержит массивы для постановки тайлов в очередь и вычисления распространения давления:
public sealed partial class AtmosphereSystem
{
    [Dependency] private readonly FirelockSystem _firelockSystem = default!;

    private readonly TileAtmosphereComparer _monstermosComparer = new();

    private readonly TileAtmosphere?[] _equalizeTiles = new TileAtmosphere[Atmospherics.MonstermosHardTileLimit];
    private readonly TileAtmosphere[] _equalizeGiverTiles = new TileAtmosphere[Atmospherics.MonstermosTileLimit];
    private readonly TileAtmosphere[] _equalizeTakerTiles = new TileAtmosphere[Atmospherics.MonstermosTileLimit];
    private readonly TileAtmosphere[] _equalizeQueue = new TileAtmosphere[Atmospherics.MonstermosTileLimit];
    private readonly TileAtmosphere[] _depressurizeTiles = new TileAtmosphere[Atmospherics.MonstermosHardTileLimit];
    private readonly TileAtmosphere[] _depressurizeSpaceTiles = new TileAtmosphere[Atmospherics.MonstermosHardTileLimit];
    private readonly TileAtmosphere[] _depressurizeProgressionOrder = new TileAtmosphere[Atmospherics.MonstermosHardTileLimit * 2];
    ... 
}
Обратите внимание, что начальные размеры массивов — все const значения в соответствии с ранее упомянутыми правилами. Поскольку вы обычно пишете для Content.Server и только для Content.Server, вы можете воспользоваться stackalloc и ArrayPool. Обратите внимание, что вы всегда должны быть очень осторожны при использовании stackalloc, так как желаемый размер массива для ваших данных может потенциально переполнить стек. В контекстах многопоточности (например, IParallelRobustJob) предпочтительнее использовать ArrayPool для быстрой аренды массивов. Не забудьте всегда возвращать их в случае исключения.
Последнее изменение 21 июня 2026 г.