using Content.Shared.Atmos.Components; using Robust.Shared.Configuration; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; namespace Content.Shared.Atmos.EntitySystems; public abstract class SharedGasTileOverlaySystem : EntitySystem { public const byte ChunkSize = 8; protected float AccumulatedFrameTime; protected bool PvsEnabled; [Dependency] protected readonly IPrototypeManager ProtoMan = default!; [Dependency] protected readonly IConfigurationManager ConfMan = default!; [Dependency] private readonly SharedAtmosphereSystem _atmosphere = default!; /// /// array of the ids of all visible gases. /// public int[] VisibleGasId = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnGetState); List visibleGases = new(); for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++) { var gasPrototype = _atmosphere.GetGas(i); if (gasPrototype.GasOverlaySprite != null) visibleGases.Add(i); } VisibleGasId = visibleGases.ToArray(); } 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(); foreach (var (index, chunk) in component.Chunks) { if (chunk.LastUpdate >= args.FromTick) data[index] = chunk; } args.State = new GasTileOverlayDeltaState(data, new(component.Chunks.Keys)); } public static Vector2i GetGasChunkIndices(Vector2i indices) { return new Vector2i((int)MathF.Floor((float)indices.X / ChunkSize), (int)MathF.Floor((float)indices.Y / ChunkSize)); } [Serializable, NetSerializable] public readonly struct GasOverlayData : IEquatable { [ViewVariables] public readonly byte FireState; [ViewVariables] public readonly byte[] Opacity; // TODO change fire color based on ByteTemp /// /// Network-synced air temperature, compressed to a single byte per tile for bandwidth optimization. /// Note: Values are approximate and may deviate even ~10°C from the precise server side only temperature. /// [ViewVariables] public readonly ThermalByte ByteGasTemperature; public GasOverlayData(byte fireState, byte[] opacity, ThermalByte byteTemp) { FireState = fireState; Opacity = opacity; ByteGasTemperature = byteTemp; } public bool Equals(GasOverlayData other) { if (FireState != other.FireState) return false; if (Opacity?.Length != other.Opacity?.Length) return false; if (Opacity != null && other.Opacity != null) { for (var i = 0; i < Opacity.Length; i++) { if (Opacity[i] != other.Opacity[i]) return false; } } if (ByteGasTemperature != other.ByteGasTemperature) return false; return true; } } [Serializable, NetSerializable] public sealed class GasOverlayUpdateEvent : EntityEventArgs { public Dictionary> UpdatedChunks = new(); public Dictionary> RemovedChunks = new(); } } /// /// Struct for networking gas temperatures to all clients using a single struct(byte) per tile. /// /// /// /// This struct compresses the gas temperature into a 1-byte value (0-255). /// It clamps the temperature to a maximum of 1000K and divides it by 4, creating a range of 0-250. /// This provides a resolution of 4 degrees Kelvin. /// /// /// The remaining bytes are used as special flags: /// /// 255: Represents a Wall (block cannot hold atmosphere). /// 254: Represents a Vacuum. /// 251-253: Reserved for future use. /// /// /// /// Dirtying Logic: The value is only dirtied and networked if the difference between the /// networked byte and the real atmosphere byte is greater than 1. This prevents network spam /// from minor temperature fluctuations (e.g., heating from 1K to 8K will not trigger an update, /// but hitting 9K moves the byte index enough to sync). /// /// /// Currently, the conversion is linear. Future improvements might involve a quadratic scale /// or pre-defined resolution points to offer higher precision at room temperatures /// and lower precision at extreme temperatures (1000K). /// /// [Serializable, NetSerializable] public struct ThermalByte : IEquatable { public const float TempMinimum = 0f; public const float TempMaximum = 1000f; public const int TempResolution = 250; public const byte ReservedFuture0 = 251; public const byte ReservedFuture1 = 252; public const byte ReservedFuture2 = 253; public const byte StateVacuum = 254; public const byte AtmosImpossible = 255; public const float TempDegreeResolution = (TempMaximum - TempMinimum) / TempResolution; public const float TempToByteFactor = TempResolution / (TempMaximum - TempMinimum); private byte _coreValue; public ThermalByte(float temperatureKelvin) { SetTemperature(temperatureKelvin); } public ThermalByte() { _coreValue = AtmosImpossible; } /// /// Set temperature of air in this in Kelvin. /// public void SetTemperature(float temperatureKelvin) { var clampedTemp = Math.Clamp(temperatureKelvin, TempMinimum, TempMaximum); _coreValue = (byte)((clampedTemp - TempMinimum) * TempResolution / (TempMaximum - TempMinimum)); } public void SetAtmosIsImpossible() { _coreValue = AtmosImpossible; } public void SetVacuum() { _coreValue = StateVacuum; } public bool IsAtmosImpossible => _coreValue == AtmosImpossible; // Cold space, solid walls public bool IsVacuum => _coreValue == StateVacuum; public byte Value => _coreValue; /// /// Attempts to get the air temperature in Kelvin. /// /// The temperature in Kelvin, if the tile has a valid temperature. /// /// If true and the tile is a vacuum, will be set to /// and the method will return . /// /// /// if the tile contains a valid temperature (including vacuum if is set); /// otherwise (e.g., walls). /// public readonly bool TryGetTemperature(out float temperature, bool onVacuumReturnTcmb = true) { switch (_coreValue) { case AtmosImpossible: temperature = 0f; return false; case StateVacuum when onVacuumReturnTcmb: temperature = Atmospherics.TCMB; return true; case StateVacuum: temperature = 0f; return false; default: temperature = (_coreValue * TempDegreeResolution) + TempMinimum; return true; } } public bool Equals(ThermalByte other) { return _coreValue == other._coreValue; } public static bool operator ==(ThermalByte left, ThermalByte right) { return left.Equals(right); } public static bool operator !=(ThermalByte left, ThermalByte right) { return !left.Equals(right); } public override bool Equals(object? obj) { return obj is ThermalByte other && Equals(other); } public override int GetHashCode() { return _coreValue.GetHashCode(); } }