]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Atmospherics Substepping (#40465)
authorArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
Fri, 31 Oct 2025 20:45:50 +0000 (13:45 -0700)
committerGitHub <noreply@github.com>
Fri, 31 Oct 2025 20:45:50 +0000 (20:45 +0000)
* initial shitcode commit

* command boilerplate

* command flushed out and docs fixes

* missed one important thing in method extraction

* do loc properly

* rest later

* address review

* this worked on my laptop but not on my desktop but okay

* review comments

* address review

Content.Server/Atmos/Commands/PauseAtmosCommand.cs [new file with mode: 0644]
Content.Server/Atmos/Commands/SubstepAtmosCommand.cs [new file with mode: 0644]
Content.Server/Atmos/EntitySystems/AtmosphereSystem.BenchmarkHelpers.cs
Content.Server/Atmos/EntitySystems/AtmosphereSystem.Processing.cs
Resources/Locale/en-US/commands/pauseatmos-command.ftl [new file with mode: 0644]
Resources/Locale/en-US/commands/substepatmos-command.ftl [new file with mode: 0644]

diff --git a/Content.Server/Atmos/Commands/PauseAtmosCommand.cs b/Content.Server/Atmos/Commands/PauseAtmosCommand.cs
new file mode 100644 (file)
index 0000000..984f2a0
--- /dev/null
@@ -0,0 +1,69 @@
+using Content.Server.Administration;
+using Content.Server.Atmos.Components;
+using Content.Server.Atmos.EntitySystems;
+using Content.Shared.Administration;
+using Robust.Shared.Console;
+
+namespace Content.Server.Atmos.Commands;
+
+[AdminCommand(AdminFlags.Debug)]
+public sealed class PauseAtmosCommand : LocalizedEntityCommands
+{
+    [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
+
+    public override string Command => "pauseatmos";
+
+    public override void Execute(IConsoleShell shell, string argStr, string[] args)
+    {
+        var grid = default(EntityUid);
+
+        switch (args.Length)
+        {
+            case 0:
+                if (!EntityManager.TryGetComponent<TransformComponent>(shell.Player?.AttachedEntity,
+                        out var playerxform) ||
+                    playerxform.GridUid == null)
+                {
+                    shell.WriteError(Loc.GetString("cmd-error-no-grid-provided-or-invalid-grid"));
+                    return;
+                }
+
+                grid = playerxform.GridUid.Value;
+                break;
+            case 1:
+                if (!EntityUid.TryParse(args[0], out var parsedGrid) || !EntityManager.EntityExists(parsedGrid))
+                {
+                    shell.WriteError(Loc.GetString("cmd-error-couldnt-parse-entity"));
+                    return;
+                }
+
+                grid = parsedGrid;
+                break;
+        }
+
+        if (!EntityManager.TryGetComponent<GridAtmosphereComponent>(grid, out var gridAtmos))
+        {
+            shell.WriteError(Loc.GetString("cmd-error-no-gridatmosphere"));
+            return;
+        }
+
+        var newEnt = new Entity<GridAtmosphereComponent>(grid, gridAtmos);
+
+        _atmosphereSystem.SetAtmosphereSimulation(newEnt, !newEnt.Comp.Simulated);
+        shell.WriteLine(Loc.GetString("cmd-pauseatmos-set-atmos-simulation",
+            ("grid", EntityManager.ToPrettyString(grid)),
+            ("state", newEnt.Comp.Simulated)));
+    }
+
+    public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
+    {
+        if (args.Length == 1)
+        {
+            return CompletionResult.FromHintOptions(
+                CompletionHelper.Components<GridAtmosphereComponent>(args[0], EntityManager),
+                Loc.GetString("cmd-pauseatmos-completion-grid-pause"));
+        }
+
+        return CompletionResult.Empty;
+    }
+}
diff --git a/Content.Server/Atmos/Commands/SubstepAtmosCommand.cs b/Content.Server/Atmos/Commands/SubstepAtmosCommand.cs
new file mode 100644 (file)
index 0000000..554abff
--- /dev/null
@@ -0,0 +1,104 @@
+using Content.Server.Administration;
+using Content.Server.Atmos.Components;
+using Content.Server.Atmos.EntitySystems;
+using Content.Shared.Administration;
+using Content.Shared.Atmos.Components;
+using Robust.Shared.Console;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+
+namespace Content.Server.Atmos.Commands;
+
+[AdminCommand(AdminFlags.Debug)]
+public sealed class SubstepAtmosCommand : LocalizedEntityCommands
+{
+    [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
+
+    public override string Command => "substepatmos";
+
+    public override void Execute(IConsoleShell shell, string argStr, string[] args)
+    {
+        var grid = default(EntityUid);
+
+        switch (args.Length)
+        {
+            case 0:
+                if (!EntityManager.TryGetComponent<TransformComponent>(shell.Player?.AttachedEntity,
+                        out var playerxform) ||
+                    playerxform.GridUid == null)
+                {
+                    shell.WriteError(Loc.GetString("cmd-error-no-grid-provided-or-invalid-grid"));
+                    return;
+                }
+
+                grid = playerxform.GridUid.Value;
+                break;
+            case 1:
+                if (!EntityUid.TryParse(args[0], out var parsedGrid) || !EntityManager.EntityExists(parsedGrid))
+                {
+                    shell.WriteError(Loc.GetString("cmd-error-couldnt-parse-entity"));
+                    return;
+                }
+
+                grid = parsedGrid;
+                break;
+        }
+
+        // i'm straight piratesoftwaremaxxing
+        if (!EntityManager.TryGetComponent<GridAtmosphereComponent>(grid, out var gridAtmos))
+        {
+            shell.WriteError(Loc.GetString("cmd-error-no-gridatmosphere"));
+            return;
+        }
+
+        if (!EntityManager.TryGetComponent<GasTileOverlayComponent>(grid, out var gasTile))
+        {
+            shell.WriteError(Loc.GetString("cmd-error-no-gastileoverlay"));
+            return;
+        }
+
+        if (!EntityManager.TryGetComponent<MapGridComponent>(grid, out var mapGrid))
+        {
+            shell.WriteError(Loc.GetString("cmd-error-no-mapgrid"));
+            return;
+        }
+
+        var xform = EntityManager.GetComponent<TransformComponent>(grid);
+
+        if (xform.MapUid == null || xform.MapID == MapId.Nullspace)
+        {
+            shell.WriteError(Loc.GetString("cmd-error-no-valid-map"));
+            return;
+        }
+
+        var newEnt =
+            new Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent>(grid,
+                gridAtmos,
+                gasTile,
+                mapGrid,
+                xform);
+
+        if (gridAtmos.Simulated)
+        {
+            shell.WriteLine(Loc.GetString("cmd-substepatmos-info-implicitly-paused-simulation",
+                ("grid", EntityManager.ToPrettyString(grid))));
+        }
+
+        _atmosphereSystem.SetAtmosphereSimulation(newEnt, false);
+        _atmosphereSystem.RunProcessingFull(newEnt, xform.MapUid.Value, _atmosphereSystem.AtmosTickRate);
+
+        shell.WriteLine(Loc.GetString("cmd-substepatmos-info-substepped-grid", ("grid", EntityManager.ToPrettyString(grid))));
+    }
+
+    public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
+    {
+        if (args.Length == 1)
+        {
+            return CompletionResult.FromHintOptions(
+                CompletionHelper.Components<GridAtmosphereComponent>(args[0], EntityManager),
+                Loc.GetString("cmd-substepatmos-completion-grid-substep"));
+        }
+
+        return CompletionResult.Empty;
+    }
+}
index f86ebcee7345d47175d09fde602ba44a36f7d0c3..62cbbae68a81de70f7e8ea81a9b95351a658f9b0 100644 (file)
@@ -46,4 +46,27 @@ public sealed partial class AtmosphereSystem
 
         return processingPaused;
     }
+
+    /// <summary>
+    /// Fully runs one <see cref="GridAtmosphereComponent"/> entity through the entire Atmos processing loop.
+    /// </summary>
+    /// <param name="ent">The entity to simulate.</param>
+    /// <param name="mapAtmosphere">The <see cref="MapAtmosphereComponent"/> that belongs to the grid's map.</param>
+    /// <param name="frameTime">Elapsed time to simulate. Recommended value is <see cref="AtmosTickRate"/>.</param>
+    public void RunProcessingFull(Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent,
+        Entity<MapAtmosphereComponent?> mapAtmosphere,
+        float frameTime)
+    {
+        while (ProcessAtmosphere(ent, mapAtmosphere, frameTime) != AtmosphereProcessingCompletionState.Finished) { }
+    }
+
+    /// <summary>
+    /// Allows or disallows atmosphere simulation on a <see cref="GridAtmosphereComponent"/>.
+    /// </summary>
+    /// <param name="ent">The atmosphere to pause or unpause processing.</param>
+    /// <param name="simulate">The state to set. True means that the atmosphere is allowed to simulate, false otherwise.</param>
+    public void SetAtmosphereSimulation(Entity<GridAtmosphereComponent> ent, bool simulate)
+    {
+        ent.Comp.Simulated = simulate;
+    }
 }
index 9b8654af6dcee452f7e2453ad08e537321d7031b..72e4b5f151131cd76315b40b893bb3a536139d74 100644 (file)
@@ -649,143 +649,196 @@ namespace Content.Server.Atmos.EntitySystems
                 if (atmosphere.LifeStage >= ComponentLifeStage.Stopping || Paused(owner) || !atmosphere.Simulated)
                     continue;
 
-                atmosphere.Timer += frameTime;
-
-                if (atmosphere.Timer < AtmosTime)
-                    continue;
-
-                // We subtract it so it takes lost time into account.
-                atmosphere.Timer -= AtmosTime;
-
                 var map = new Entity<MapAtmosphereComponent?>(xform.MapUid.Value, _mapAtmosQuery.CompOrNull(xform.MapUid.Value));
 
-                switch (atmosphere.State)
+                var completionState = ProcessAtmosphere(ent, map, frameTime);
+
+                switch (completionState)
                 {
-                    case AtmosphereProcessingState.Revalidate:
-                        if (!ProcessRevalidate(ent))
-                        {
-                            atmosphere.ProcessingPaused = true;
-                            return;
-                        }
-
-                        atmosphere.ProcessingPaused = false;
-
-                        // Next state depends on whether monstermos equalization is enabled or not.
-                        // Note: We do this here instead of on the tile equalization step to prevent ending it early.
-                        //       Therefore, a change to this CVar might only be applied after that step is over.
-                        atmosphere.State = MonstermosEqualization
-                            ? AtmosphereProcessingState.TileEqualize
-                            : AtmosphereProcessingState.ActiveTiles;
-                        continue;
-                    case AtmosphereProcessingState.TileEqualize:
-                        if (!ProcessTileEqualize(ent))
-                        {
-                            atmosphere.ProcessingPaused = true;
-                            return;
-                        }
-
-                        atmosphere.ProcessingPaused = false;
-                        atmosphere.State = AtmosphereProcessingState.ActiveTiles;
-                        continue;
-                    case AtmosphereProcessingState.ActiveTiles:
-                        if (!ProcessActiveTiles(ent))
-                        {
-                            atmosphere.ProcessingPaused = true;
-                            return;
-                        }
-
-                        atmosphere.ProcessingPaused = false;
-                        // Next state depends on whether excited groups are enabled or not.
-                        atmosphere.State = ExcitedGroups ? AtmosphereProcessingState.ExcitedGroups : AtmosphereProcessingState.HighPressureDelta;
-                        continue;
-                    case AtmosphereProcessingState.ExcitedGroups:
-                        if (!ProcessExcitedGroups(ent))
-                        {
-                            atmosphere.ProcessingPaused = true;
-                            return;
-                        }
-
-                        atmosphere.ProcessingPaused = false;
-                        atmosphere.State = AtmosphereProcessingState.HighPressureDelta;
-                        continue;
-                    case AtmosphereProcessingState.HighPressureDelta:
-                        if (!ProcessHighPressureDelta((ent, ent)))
-                        {
-                            atmosphere.ProcessingPaused = true;
-                            return;
-                        }
-
-                        atmosphere.ProcessingPaused = false;
-                        atmosphere.State = DeltaPressureDamage
-                            ? AtmosphereProcessingState.DeltaPressure
-                            : AtmosphereProcessingState.Hotspots;
+                    case AtmosphereProcessingCompletionState.Return:
+                        return;
+                    case AtmosphereProcessingCompletionState.Continue:
                         continue;
-                    case AtmosphereProcessingState.DeltaPressure:
-                        if (!ProcessDeltaPressure(ent))
-                        {
-                            atmosphere.ProcessingPaused = true;
-                            return;
-                        }
-
-                        atmosphere.ProcessingPaused = false;
-                        atmosphere.State = AtmosphereProcessingState.Hotspots;
-                        continue;
-                    case AtmosphereProcessingState.Hotspots:
-                        if (!ProcessHotspots(ent))
-                        {
-                            atmosphere.ProcessingPaused = true;
-                            return;
-                        }
-
-                        atmosphere.ProcessingPaused = false;
-                        // Next state depends on whether superconduction is enabled or not.
-                        // Note: We do this here instead of on the tile equalization step to prevent ending it early.
-                        //       Therefore, a change to this CVar might only be applied after that step is over.
-                        atmosphere.State = Superconduction
-                            ? AtmosphereProcessingState.Superconductivity
-                            : AtmosphereProcessingState.PipeNet;
-                        continue;
-                    case AtmosphereProcessingState.Superconductivity:
-                        if (!ProcessSuperconductivity(atmosphere))
-                        {
-                            atmosphere.ProcessingPaused = true;
-                            return;
-                        }
-
-                        atmosphere.ProcessingPaused = false;
-                        atmosphere.State = AtmosphereProcessingState.PipeNet;
-                        continue;
-                    case AtmosphereProcessingState.PipeNet:
-                        if (!ProcessPipeNets(atmosphere))
-                        {
-                            atmosphere.ProcessingPaused = true;
-                            return;
-                        }
-
-                        atmosphere.ProcessingPaused = false;
-                        atmosphere.State = AtmosphereProcessingState.AtmosDevices;
-                        continue;
-                    case AtmosphereProcessingState.AtmosDevices:
-                        if (!ProcessAtmosDevices(ent, map))
-                        {
-                            atmosphere.ProcessingPaused = true;
-                            return;
-                        }
-
-                        atmosphere.ProcessingPaused = false;
-                        atmosphere.State = AtmosphereProcessingState.Revalidate;
-
-                        // We reached the end of this atmosphere's update tick. Break out of the switch.
+                    case AtmosphereProcessingCompletionState.Finished:
                         break;
                 }
-
-                // And increase the update counter.
-                atmosphere.UpdateCounter++;
             }
 
             // We finished processing all atmospheres successfully, therefore we won't be paused next tick.
             _simulationPaused = false;
         }
+
+        /// <summary>
+        /// Processes a <see cref="GridAtmosphereComponent"/> through its processing stages.
+        /// </summary>
+        /// <param name="ent">The entity to process.</param>
+        /// <param name="mapAtmosphere">The <see cref="MapAtmosphereComponent"/> belonging to the
+        /// <see cref="GridAtmosphereComponent"/>'s map.</param>
+        /// <param name="frameTime">The elapsed time since the last frame.</param>
+        /// <returns>An <see cref="AtmosphereProcessingCompletionState"/> that represents the completion state.</returns>
+        private AtmosphereProcessingCompletionState ProcessAtmosphere(Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent,
+            Entity<MapAtmosphereComponent?> mapAtmosphere,
+            float frameTime)
+        {
+            // They call me the deconstructor the way i be deconstructing it
+            // and by it, i mean... my entity
+            var (owner, atmosphere, visuals, grid, xform) = ent;
+
+            atmosphere.Timer += frameTime;
+
+            if (atmosphere.Timer < AtmosTime)
+                return AtmosphereProcessingCompletionState.Continue;
+
+            // We subtract it so it takes lost time into account.
+            atmosphere.Timer -= AtmosTime;
+
+            switch (atmosphere.State)
+            {
+                case AtmosphereProcessingState.Revalidate:
+                    if (!ProcessRevalidate(ent))
+                    {
+                        atmosphere.ProcessingPaused = true;
+                        return AtmosphereProcessingCompletionState.Return;
+                    }
+
+                    atmosphere.ProcessingPaused = false;
+
+                    // Next state depends on whether monstermos equalization is enabled or not.
+                    // Note: We do this here instead of on the tile equalization step to prevent ending it early.
+                    //       Therefore, a change to this CVar might only be applied after that step is over.
+                    atmosphere.State = MonstermosEqualization
+                        ? AtmosphereProcessingState.TileEqualize
+                        : AtmosphereProcessingState.ActiveTiles;
+                    return AtmosphereProcessingCompletionState.Continue;
+                case AtmosphereProcessingState.TileEqualize:
+                    if (!ProcessTileEqualize(ent))
+                    {
+                        atmosphere.ProcessingPaused = true;
+                        return AtmosphereProcessingCompletionState.Return;
+                    }
+
+                    atmosphere.ProcessingPaused = false;
+                    atmosphere.State = AtmosphereProcessingState.ActiveTiles;
+                    return AtmosphereProcessingCompletionState.Continue;
+                case AtmosphereProcessingState.ActiveTiles:
+                    if (!ProcessActiveTiles(ent))
+                    {
+                        atmosphere.ProcessingPaused = true;
+                        return AtmosphereProcessingCompletionState.Return;
+                    }
+
+                    atmosphere.ProcessingPaused = false;
+                    // Next state depends on whether excited groups are enabled or not.
+                    atmosphere.State = ExcitedGroups ? AtmosphereProcessingState.ExcitedGroups : AtmosphereProcessingState.HighPressureDelta;
+                    return AtmosphereProcessingCompletionState.Continue;
+                case AtmosphereProcessingState.ExcitedGroups:
+                    if (!ProcessExcitedGroups(ent))
+                    {
+                        atmosphere.ProcessingPaused = true;
+                        return AtmosphereProcessingCompletionState.Return;
+                    }
+
+                    atmosphere.ProcessingPaused = false;
+                    atmosphere.State = AtmosphereProcessingState.HighPressureDelta;
+                    return AtmosphereProcessingCompletionState.Continue;
+                case AtmosphereProcessingState.HighPressureDelta:
+                    if (!ProcessHighPressureDelta((ent, ent)))
+                    {
+                        atmosphere.ProcessingPaused = true;
+                        return AtmosphereProcessingCompletionState.Return;
+                    }
+
+                    atmosphere.ProcessingPaused = false;
+                    atmosphere.State = DeltaPressureDamage
+                        ? AtmosphereProcessingState.DeltaPressure
+                        : AtmosphereProcessingState.Hotspots;
+                    return AtmosphereProcessingCompletionState.Continue;
+                case AtmosphereProcessingState.DeltaPressure:
+                    if (!ProcessDeltaPressure(ent))
+                    {
+                        atmosphere.ProcessingPaused = true;
+                        return AtmosphereProcessingCompletionState.Return;
+                    }
+
+                    atmosphere.ProcessingPaused = false;
+                    atmosphere.State = AtmosphereProcessingState.Hotspots;
+                    return AtmosphereProcessingCompletionState.Continue;
+                case AtmosphereProcessingState.Hotspots:
+                    if (!ProcessHotspots(ent))
+                    {
+                        atmosphere.ProcessingPaused = true;
+                        return AtmosphereProcessingCompletionState.Return;
+                    }
+
+                    atmosphere.ProcessingPaused = false;
+                    // Next state depends on whether superconduction is enabled or not.
+                    // Note: We do this here instead of on the tile equalization step to prevent ending it early.
+                    //       Therefore, a change to this CVar might only be applied after that step is over.
+                    atmosphere.State = Superconduction
+                        ? AtmosphereProcessingState.Superconductivity
+                        : AtmosphereProcessingState.PipeNet;
+                    return AtmosphereProcessingCompletionState.Continue;
+                case AtmosphereProcessingState.Superconductivity:
+                    if (!ProcessSuperconductivity(atmosphere))
+                    {
+                        atmosphere.ProcessingPaused = true;
+                        return AtmosphereProcessingCompletionState.Return;
+                    }
+
+                    atmosphere.ProcessingPaused = false;
+                    atmosphere.State = AtmosphereProcessingState.PipeNet;
+                    return AtmosphereProcessingCompletionState.Continue;
+                case AtmosphereProcessingState.PipeNet:
+                    if (!ProcessPipeNets(atmosphere))
+                    {
+                        atmosphere.ProcessingPaused = true;
+                        return AtmosphereProcessingCompletionState.Return;
+                    }
+
+                    atmosphere.ProcessingPaused = false;
+                    atmosphere.State = AtmosphereProcessingState.AtmosDevices;
+                    return AtmosphereProcessingCompletionState.Continue;
+                case AtmosphereProcessingState.AtmosDevices:
+                    if (!ProcessAtmosDevices(ent, mapAtmosphere))
+                    {
+                        atmosphere.ProcessingPaused = true;
+                        return AtmosphereProcessingCompletionState.Return;
+                    }
+
+                    atmosphere.ProcessingPaused = false;
+                    atmosphere.State = AtmosphereProcessingState.Revalidate;
+
+                    // We reached the end of this atmosphere's update tick. Break out of the switch.
+                    break;
+            }
+
+            atmosphere.UpdateCounter++;
+
+            return AtmosphereProcessingCompletionState.Finished;
+        }
+    }
+
+    /// <summary>
+    /// An enum representing the completion state of a <see cref="GridAtmosphereComponent"/>'s processing steps.
+    /// The processing of a <see cref="GridAtmosphereComponent"/> spans over multiple stages and sticks,
+    /// with the method handling the processing having multiple return types.
+    /// </summary>
+    public enum AtmosphereProcessingCompletionState : byte
+    {
+        /// <summary>
+        /// Method is returning, ex. due to delegating processing to the next tick.
+        /// </summary>
+        Return,
+
+        /// <summary>
+        /// Method is continuing, ex. due to finishing a single processing stage.
+        /// </summary>
+        Continue,
+
+        /// <summary>
+        /// Method is finished with the GridAtmosphere.
+        /// </summary>
+        Finished,
     }
 
     public enum AtmosphereProcessingState : byte
diff --git a/Resources/Locale/en-US/commands/pauseatmos-command.ftl b/Resources/Locale/en-US/commands/pauseatmos-command.ftl
new file mode 100644 (file)
index 0000000..1eb2e13
--- /dev/null
@@ -0,0 +1,6 @@
+cmd-pauseatmos-desc = Pauses or unpauses the atmosphere simulation for the provided grid entity.
+cmd-pauseatmos-help = Usage: {$command} <EntityUid>
+
+cmd-pauseatmos-set-atmos-simulation = Set atmospherics simulation on {$grid} to state {$state}.
+
+cmd-pauseatmos-completion-grid-pause = EntityUid of the grid you want to pause/unpause. Automatically uses the grid you're standing on if empty.
diff --git a/Resources/Locale/en-US/commands/substepatmos-command.ftl b/Resources/Locale/en-US/commands/substepatmos-command.ftl
new file mode 100644 (file)
index 0000000..bb89085
--- /dev/null
@@ -0,0 +1,15 @@
+cmd-substepatmos-desc = Substeps the atmosphere simulation by a single atmostick for the provided grid entity. Implicitly pauses atmospherics simulation.
+cmd-substepatmos-help = Usage: {$command} <EntityUid>
+
+cmd-error-no-grid-provided-or-invalid-grid = You must either provide a grid entity or be standing on a grid to substep.
+cmd-error-couldnt-parse-entity = Entity provided could not be parsed or does not exist. Try standing on a grid you want to substep.
+cmd-error-no-gridatmosphere = Entity provided doesn't have a GridAtmosphereComponent.
+cmd-error-no-gastileoverlay = Entity provided doesn't have a GasTileOverlayComponent.
+cmd-error-no-mapgrid = Entity provided doesn't have a MapGridComponent.
+cmd-error-no-xform = Entity provided doesn't have a TransformComponent?
+cmd-error-no-valid-map = The grid provided is not on a valid map?
+
+cmd-substepatmos-info-implicitly-paused-simulation = Implicitly paused atmospherics simulation on {$grid}.
+cmd-substepatmos-info-substepped-grid = Substepped atmospherics simulation by one atmostick on {$grid}.
+
+cmd-substepatmos-completion-grid-substep = EntityUid of the grid you want to substep. Automatically uses the grid you're standing on if empty.