Document Atmospherics Hotspot (#41283)

* hotspot partial docs

* Finalize docs
This commit is contained in:
ArtisticRoomba 2025-11-04 03:27:10 -08:00 committed by Vanessa
parent e18f1cdfd9
commit fc1bc7b722
2 changed files with 298 additions and 185 deletions

View File

@ -10,27 +10,59 @@ using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Atmos.EntitySystems
{
namespace Content.Server.Atmos.EntitySystems;
public sealed partial class AtmosphereSystem
{
/*
Handles Hotspots, which are gas-based tile fires that slowly grow and spread
to adjacent tiles if conditions are met.
You can think of a hotspot as a small flame on a tile that
grows by consuming a fuel and oxidizer from the tile's air,
with a certain volume and temperature.
This volume grows bigger and bigger as the fire continues,
until it effectively engulfs the entire tile, at which point
it starts spreading to adjacent tiles by radiating heat.
*/
/// <summary>
/// Collection of hotspot sounds to play.
/// </summary>
private static readonly ProtoId<SoundCollectionPrototype> DefaultHotspotSounds = "AtmosHotspot";
[Dependency] private readonly DecalSystem _decalSystem = default!;
[Dependency] private readonly IRobustRandom _random = default!;
/// <summary>
/// Number of cycles the hotspot system must process before it can play another sound
/// on a hotspot.
/// </summary>
private const int HotspotSoundCooldownCycles = 200;
/// <summary>
/// Cooldown counter for hotspot sounds.
/// </summary>
private int _hotspotSoundCooldown = 0;
[ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier? HotspotSound { get; private set; } = new SoundCollectionSpecifier(DefaultHotspotSounds);
public SoundSpecifier? HotspotSound = new SoundCollectionSpecifier(DefaultHotspotSounds);
/// <summary>
/// Processes a hotspot on a <see cref="TileAtmosphere"/>.
/// </summary>
/// <param name="ent">The grid entity that belongs to the tile to process.</param>
/// <param name="tile">The <see cref="TileAtmosphere"/> to process.</param>
private void ProcessHotspot(
Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent,
TileAtmosphere tile)
{
var gridAtmosphere = ent.Comp1;
// Hotspots that have fizzled out are assigned a new Hotspot struct
// with Valid set to false, so we can just check that here in
// one central place instead of manually removing it everywhere.
if (!tile.Hotspot.Valid)
{
gridAtmosphere.HotspotTiles.Remove(tile);
@ -39,6 +71,10 @@ namespace Content.Server.Atmos.EntitySystems
AddActiveTile(gridAtmosphere, tile);
// Prevent the hotspot from processing on the same cycle it was created (???)
// TODO ATMOS: Is this even necessary anymore? The queue is kept per processing stage
// and is not updated until tne next cycle, so the condition of a hotspot being created
// and processed in the same cycle is impossible.
if (!tile.Hotspot.SkippedFirstProcess)
{
tile.Hotspot.SkippedFirstProcess = true;
@ -48,8 +84,11 @@ namespace Content.Server.Atmos.EntitySystems
if (tile.ExcitedGroup != null)
ExcitedGroupResetCooldowns(tile.ExcitedGroup);
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.Hotspot = new Hotspot();
InvalidateVisuals(ent, tile);
@ -58,6 +97,8 @@ namespace Content.Server.Atmos.EntitySystems
PerformHotspotExposure(tile);
// This tile has now turned into a full-blown tile-fire.
// Start applying fire effects and spreading to adjacent tiles.
if (tile.Hotspot.Bypassing)
{
tile.Hotspot.State = 3;
@ -84,7 +125,12 @@ namespace Content.Server.Atmos.EntitySystems
// Add a random burned decal to the tile only if there are less than 4 of them
if (tileBurntDecals < 4)
_decalSystem.TryAddDecal(_burntDecals[_random.Next(_burntDecals.Length)], new EntityCoordinates(gridUid, tilePos), out _, cleanable: true);
{
_decalSystem.TryAddDecal(_burntDecals[_random.Next(_burntDecals.Length)],
new EntityCoordinates(gridUid, tilePos),
out _,
cleanable: true);
}
if (tile.Air.Temperature > Atmospherics.FireMinimumTemperatureToSpread)
{
@ -92,6 +138,8 @@ namespace Content.Server.Atmos.EntitySystems
foreach (var otherTile in tile.AdjacentTiles)
{
// TODO ATMOS: This is sus. Suss this out.
// Spread this fire to other tiles by exposing them to a hotspot if air can flow there.
// Unsure as to why this is sus.
if (otherTile == null)
continue;
@ -102,6 +150,7 @@ namespace Content.Server.Atmos.EntitySystems
}
else
{
// Little baby fire. Set the sprite state based on the current size of the fire.
tile.Hotspot.State = (byte)(tile.Hotspot.Volume > Atmospherics.CellVolume * 0.4f ? 2 : 1);
}
@ -115,7 +164,10 @@ namespace Content.Server.Atmos.EntitySystems
// A few details on the audio parameters for fire.
// The greater the fire state, the lesser the pitch variation.
// The greater the fire state, the greater the volume.
_audio.PlayPvs(HotspotSound, coordinates, HotspotSound.Params.WithVariation(0.15f / tile.Hotspot.State).WithVolume(-5f + 5f * tile.Hotspot.State));
_audio.PlayPvs(HotspotSound,
coordinates,
HotspotSound.Params.WithVariation(0.15f / tile.Hotspot.State)
.WithVolume(-5f + 5f * tile.Hotspot.State));
}
if (_hotspotSoundCooldown > HotspotSoundCooldownCycles)
@ -124,8 +176,27 @@ namespace Content.Server.Atmos.EntitySystems
// TODO ATMOS Maybe destroy location here?
}
private void HotspotExpose(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile,
float exposedTemperature, float exposedVolume, bool soh = false, EntityUid? sparkSourceUid = null)
/// <summary>
/// Exposes a tile to a hotspot of given temperature and volume, igniting it if conditions are met.
/// </summary>
/// <param name="gridAtmosphere">The <see cref="GridAtmosphereComponent"/> of the grid the tile is on.</param>
/// <param name="tile">The <see cref="TileAtmosphere"/> to expose.</param>
/// <param name="exposedTemperature">The temperature of the hotspot to expose.
/// You can think of this as exposing a temperature of a flame.</param>
/// <param name="exposedVolume">The volume of the hotspot to expose.
/// You can think of this as how big the flame is initially.
/// Bigger flames will ramp a fire faster.</param>
/// <param name="soh">Whether to "boost" a fire that's currently on the tile already.
/// Does nothing if the tile isn't already a hotspot.
/// This clamps the temperature and volume of the hotspot to the maximum
/// of the provided parameters and whatever's on the tile.</param>
/// <param name="sparkSourceUid">Entity that started the exposure for admin logging.</param>
private void HotspotExpose(GridAtmosphereComponent gridAtmosphere,
TileAtmosphere tile,
float exposedTemperature,
float exposedVolume,
bool soh = false,
EntityUid? sparkSourceUid = null)
{
if (tile.Air == null)
return;
@ -144,20 +215,22 @@ namespace Content.Server.Atmos.EntitySystems
{
if (plasma > 0.5f || tritium > 0.5f)
{
if (tile.Hotspot.Temperature < exposedTemperature)
tile.Hotspot.Temperature = exposedTemperature;
if (tile.Hotspot.Volume < exposedVolume)
tile.Hotspot.Volume = exposedVolume;
tile.Hotspot.Temperature = MathF.Max(tile.Hotspot.Temperature, exposedTemperature);
tile.Hotspot.Volume = MathF.Max(tile.Hotspot.Volume, exposedVolume);
}
}
return;
}
if ((exposedTemperature > Atmospherics.PlasmaMinimumBurnTemperature) && (plasma > 0.5f || tritium > 0.5f))
if (exposedTemperature > Atmospherics.PlasmaMinimumBurnTemperature && (plasma > 0.5f || tritium > 0.5f))
{
if (sparkSourceUid.HasValue)
_adminLog.Add(LogType.Flammable, LogImpact.High, $"Heat/spark of {ToPrettyString(sparkSourceUid.Value)} caused atmos ignition of gas: {tile.Air.Temperature.ToString():temperature}K - {oxygen}mol Oxygen, {plasma}mol Plasma, {tritium}mol Tritium");
{
_adminLog.Add(LogType.Flammable,
LogImpact.High,
$"Heat/spark of {ToPrettyString(sparkSourceUid.Value)} caused atmos ignition of gas: {tile.Air.Temperature.ToString():temperature}K - {oxygen}mol Oxygen, {plasma}mol Plasma, {tritium}mol Tritium");
}
tile.Hotspot = new Hotspot
{
@ -173,23 +246,33 @@ namespace Content.Server.Atmos.EntitySystems
}
}
/// <summary>
/// Performs hotspot exposure processing on a <see cref="TileAtmosphere"/>.
/// </summary>
/// <param name="tile">The <see cref="TileAtmosphere"/> to process.</param>
private void PerformHotspotExposure(TileAtmosphere tile)
{
if (tile.Air == null || !tile.Hotspot.Valid) return;
if (tile.Air == null || !tile.Hotspot.Valid)
return;
// Determine if the tile has become a full-blown fire if the volume of the fire has effectively reached
// the volume of the tile's air.
tile.Hotspot.Bypassing = tile.Hotspot.SkippedFirstProcess && tile.Hotspot.Volume > tile.Air.Volume * 0.95f;
// If the tile is effectively a full fire, use the tile's air for reactions, don't bother partitioning.
if (tile.Hotspot.Bypassing)
{
tile.Hotspot.Volume = tile.Air.ReactionResults[(byte)GasReaction.Fire] * Atmospherics.FireGrowthRate;
tile.Hotspot.Temperature = tile.Air.Temperature;
}
// Otherwise, pull out a fraction of the tile's air (the current hotspot volume) to perform reactions on.
else
{
var affected = tile.Air.RemoveVolume(tile.Hotspot.Volume);
affected.Temperature = tile.Hotspot.Temperature;
React(affected, tile);
tile.Hotspot.Temperature = affected.Temperature;
// Scale the fire based on the type of reaction that occured.
tile.Hotspot.Volume = affected.ReactionResults[(byte)GasReaction.Fire] * Atmospherics.FireGrowthRate;
Merge(tile.Air, affected);
}
@ -204,4 +287,3 @@ namespace Content.Server.Atmos.EntitySystems
}
}
}
}

View File

@ -1,19 +1,51 @@
namespace Content.Server.Atmos
{
namespace Content.Server.Atmos;
/// <summary>
/// Internal Atmospherics struct that stores data about a hotspot in a tile.
/// Hotspots are used to model (slow-spreading) fires and firestarters.
/// </summary>
public struct Hotspot
{
/// <summary>
/// Whether this hotspot is currently representing fire and needs to be processed.
/// Set when the hotspot "becomes alight". This is never set to false
/// because Atmospherics will just assign <see cref="TileAtmosphere"/>
/// a new <see cref="Hotspot"/> struct when the fire goes out.
/// </summary>
[ViewVariables]
public bool Valid;
/// <summary>
/// Whether this hotspot has skipped its first process cycle.
/// AtmosphereSystem.Hotspot skips processing a hotspot beyond
/// setting it to active (for LINDA processing) the first
/// time it is processed.
/// </summary>
[ViewVariables]
public bool SkippedFirstProcess;
/// <summary>
/// <para>Whether this hotspot is currently using the tile for reacting and fire processing
/// instead of a fraction of the tile's air.</para>
///
/// <para>When a tile is considered a hotspot, Hotspot will pull a fraction of that tile's
/// air out of the tile and perform a reaction on that air, merging it back afterward.
/// Bypassing triggers when the hotspot volume nears the tile's volume, making the system
/// use the tile's GasMixture instead of pulling a fraction out.</para>
/// </summary>
[ViewVariables]
public bool Bypassing;
/// <summary>
/// Current temperature of the hotspot's volume, in Kelvin.
/// </summary>
[ViewVariables]
public float Temperature;
/// <summary>
/// Current volume of the hotspot, in liters.
/// You can think of this as the volume of the current fire in the tile.
/// </summary>
[ViewVariables]
public float Volume;
@ -23,4 +55,3 @@
[ViewVariables]
public byte State;
}
}