2023-04-06 11:43:12 +12:00
using Content.Shared.Atmos.Components ;
2022-07-25 14:10:18 +12:00
using Content.Shared.Atmos.Prototypes ;
2025-09-03 23:17:39 -04:00
using Content.Shared.CCVar ;
using Robust.Shared.Configuration ;
2023-04-06 11:43:12 +12:00
using Robust.Shared.GameStates ;
2022-07-25 14:10:18 +12:00
using Robust.Shared.Prototypes ;
2020-08-18 00:12:21 +10:00
using Robust.Shared.Serialization ;
2021-06-19 13:25:05 +02:00
namespace Content.Shared.Atmos.EntitySystems
2020-08-18 00:12:21 +10:00
{
public abstract class SharedGasTileOverlaySystem : EntitySystem
{
2025-09-03 23:17:39 -04:00
/// <summary>
/// The temperature at which the heat distortion effect starts to be applied.
/// </summary>
private float _tempAtMinHeatDistortion ;
/// <summary>
/// The temperature at which the heat distortion effect is at maximum strength.
/// </summary>
private float _tempAtMaxHeatDistortion ;
/// <summary>
/// Calculated linear slope and intercept to map temperature to a heat distortion strength from 0.0 to 1.0
/// </summary>
private float _heatDistortionSlope ;
private float _heatDistortionIntercept ;
2020-08-18 00:12:21 +10:00
public const byte ChunkSize = 8 ;
protected float AccumulatedFrameTime ;
2023-04-06 11:43:12 +12:00
protected bool PvsEnabled ;
2020-08-18 00:12:21 +10:00
2022-07-25 14:10:18 +12:00
[Dependency] protected readonly IPrototypeManager ProtoMan = default ! ;
2025-09-03 23:17:39 -04:00
[Dependency] protected readonly IConfigurationManager ConfMan = default ! ;
2020-08-25 16:14:26 +02:00
2022-07-25 14:10:18 +12:00
/// <summary>
/// array of the ids of all visible gases.
/// </summary>
public int [ ] VisibleGasId = default ! ;
public override void Initialize ( )
2020-08-18 00:12:21 +10:00
{
2022-07-25 14:10:18 +12:00
base . Initialize ( ) ;
2025-09-03 23:17:39 -04:00
// Make sure the heat distortion variables are updated if the CVars change
Subs . CVar ( ConfMan , CCVars . GasOverlayHeatMinimum , UpdateMinHeat , true ) ;
Subs . CVar ( ConfMan , CCVars . GasOverlayHeatMaximum , UpdateMaxHeat , true ) ;
2023-04-06 11:43:12 +12:00
SubscribeLocalEvent < GasTileOverlayComponent , ComponentGetState > ( OnGetState ) ;
2020-08-18 00:12:21 +10:00
2022-07-25 14:10:18 +12:00
List < int > visibleGases = new ( ) ;
2020-11-25 10:48:49 +01:00
2022-07-25 14:10:18 +12:00
for ( var i = 0 ; i < Atmospherics . TotalNumberOfGases ; i + + )
2020-11-25 10:48:49 +01:00
{
2022-07-25 14:10:18 +12:00
var gasPrototype = ProtoMan . Index < GasPrototype > ( i . ToString ( ) ) ;
if ( ! string . IsNullOrEmpty ( gasPrototype . GasOverlayTexture ) | | ! string . IsNullOrEmpty ( gasPrototype . GasOverlaySprite ) & & ! string . IsNullOrEmpty ( gasPrototype . GasOverlayState ) )
visibleGases . Add ( i ) ;
2020-11-25 10:48:49 +01:00
}
2022-07-25 14:10:18 +12:00
VisibleGasId = visibleGases . ToArray ( ) ;
}
2025-09-03 23:17:39 -04:00
private void UpdateMaxHeat ( float val )
{
_tempAtMaxHeatDistortion = val ;
UpdateHeatSlopeAndIntercept ( ) ;
}
private void UpdateMinHeat ( float val )
{
_tempAtMinHeatDistortion = val ;
UpdateHeatSlopeAndIntercept ( ) ;
}
private void UpdateHeatSlopeAndIntercept ( )
{
// Make sure to avoid invalid settings (min == max or min > max)
// I'm not sure if CVars can have constraints or if CVar subscribers can reject changes.
var diff = _tempAtMinHeatDistortion < _tempAtMaxHeatDistortion
? _tempAtMaxHeatDistortion - _tempAtMinHeatDistortion
: 0.001f ;
_heatDistortionSlope = 1.0f / diff ;
_heatDistortionIntercept = - _tempAtMinHeatDistortion * _heatDistortionSlope ;
}
2023-04-06 11:43:12 +12:00
private void OnGetState ( EntityUid uid , GasTileOverlayComponent component , ref ComponentGetState args )
{
if ( PvsEnabled & & ! args . ReplayState )
return ;
// Should this be a full component state or a delta-state?
if ( args . FromTick < = component . CreationTick | | args . FromTick < = component . ForceTick )
{
args . State = new GasTileOverlayState ( component . Chunks ) ;
return ;
}
var data = new Dictionary < Vector2i , GasOverlayChunk > ( ) ;
foreach ( var ( index , chunk ) in component . Chunks )
{
if ( chunk . LastUpdate > = args . FromTick )
data [ index ] = chunk ;
}
2024-05-24 16:08:23 +12:00
args . State = new GasTileOverlayDeltaState ( data , new ( component . Chunks . Keys ) ) ;
2023-04-06 11:43:12 +12:00
}
2022-07-25 14:10:18 +12:00
public static Vector2i GetGasChunkIndices ( Vector2i indices )
{
return new ( ( int ) MathF . Floor ( ( float ) indices . X / ChunkSize ) , ( int ) MathF . Floor ( ( float ) indices . Y / ChunkSize ) ) ;
2020-08-18 00:12:21 +10:00
}
[Serializable, NetSerializable]
public readonly struct GasOverlayData : IEquatable < GasOverlayData >
{
2024-03-24 03:34:56 +11:00
[ViewVariables]
2020-08-18 00:12:21 +10:00
public readonly byte FireState ;
2024-03-24 03:34:56 +11:00
[ViewVariables]
2022-07-25 14:10:18 +12:00
public readonly byte [ ] Opacity ;
2025-09-03 23:17:39 -04:00
/// <summary>
/// This temperature is currently only used by the GasTileHeatOverlay.
/// This value will only reflect the true temperature of the gas when the temperature is between
/// <see cref="SharedGasTileOverlaySystem._tempAtMinHeatDistortion"/> and <see cref="SharedGasTileOverlaySystem._tempAtMaxHeatDistortion"/> as these are the only
/// values at which the heat distortion varies.
/// Additionally, it will only update when the heat distortion strength changes by
/// <see cref="_heatDistortionStrengthChangeTolerance"/>. By default, this is 5%, which corresponds to
/// 20 steps from <see cref="SharedGasTileOverlaySystem._tempAtMinHeatDistortion"/> to <see cref="SharedGasTileOverlaySystem._tempAtMaxHeatDistortion"/>.
/// For 325K to 1000K with 5% tolerance, then this field will dirty only if it differs by 33.75K, or 20 steps.
/// </summary>
[ViewVariables]
public readonly float Temperature ;
2022-07-25 14:10:18 +12:00
// TODO change fire color based on temps
2020-08-18 00:12:21 +10:00
2025-09-03 23:17:39 -04:00
public GasOverlayData ( byte fireState , byte [ ] opacity , float temperature )
2020-08-18 00:12:21 +10:00
{
FireState = fireState ;
2022-07-25 14:10:18 +12:00
Opacity = opacity ;
2025-09-03 23:17:39 -04:00
Temperature = temperature ;
2022-07-25 14:10:18 +12:00
}
2020-11-25 10:48:49 +01:00
2022-07-25 14:10:18 +12:00
public bool Equals ( GasOverlayData other )
{
if ( FireState ! = other . FireState )
return false ;
2020-08-18 00:12:21 +10:00
2022-11-16 08:27:49 +01:00
if ( Opacity ? . Length ! = other . Opacity ? . Length )
return false ;
if ( Opacity ! = null & & other . Opacity ! = null )
2020-08-18 00:12:21 +10:00
{
2022-11-16 08:27:49 +01:00
for ( var i = 0 ; i < Opacity . Length ; i + + )
{
if ( Opacity [ i ] ! = other . Opacity [ i ] )
return false ;
}
2020-08-18 00:12:21 +10:00
}
2020-08-25 16:14:26 +02:00
2025-09-03 23:17:39 -04:00
// This is only checking if two datas are equal -- a different routine is used to check if the
// temperature differs enough to dirty the chunk using a much wider tolerance.
if ( ! MathHelper . CloseToPercent ( Temperature , other . Temperature ) )
return false ;
2022-07-25 14:10:18 +12:00
return true ;
2020-08-18 00:12:21 +10:00
}
}
2025-09-03 23:17:39 -04:00
/// <summary>
/// Calculate the heat distortion from a temperature.
/// Returns 0.0f below TempAtMinHeatDistortion and 1.0f above TempAtMaxHeatDistortion.
/// </summary>
/// <param name="temp"></param>
/// <returns></returns>
public float GetHeatDistortionStrength ( float temp )
{
return MathHelper . Clamp01 ( temp * _heatDistortionSlope + _heatDistortionIntercept ) ;
}
2020-08-18 00:12:21 +10:00
[Serializable, NetSerializable]
2022-07-25 14:10:18 +12:00
public sealed class GasOverlayUpdateEvent : EntityEventArgs
2020-08-18 00:12:21 +10:00
{
2023-09-11 09:42:41 +10:00
public Dictionary < NetEntity , List < GasOverlayChunk > > UpdatedChunks = new ( ) ;
public Dictionary < NetEntity , HashSet < Vector2i > > RemovedChunks = new ( ) ;
2020-08-18 00:12:21 +10:00
}
}
}