diff --git a/Content.Client/Atmos/EntitySystems/GasTileVacuumOverlaySystem.cs b/Content.Client/Atmos/EntitySystems/GasTileVacuumOverlaySystem.cs
new file mode 100644
index 0000000000..19dcd5bfec
--- /dev/null
+++ b/Content.Client/Atmos/EntitySystems/GasTileVacuumOverlaySystem.cs
@@ -0,0 +1,65 @@
+using Content.Client.Atmos.Overlays;
+using Content.Shared.CCVar;
+using JetBrains.Annotations;
+using Robust.Client.Graphics;
+using Robust.Shared.Configuration;
+
+namespace Content.Client.Atmos.EntitySystems;
+
+///
+/// System responsible for rendering vacuum effects using .
+///
+[UsedImplicitly]
+public sealed partial class GasTileVacuumOverlaySystem : EntitySystem
+{
+ [Dependency] private IOverlayManager _overlayMan = default!;
+ [Dependency] private IConfigurationManager _cfgManager = default!;
+
+ private GasTileVacuumOverlay? _gasTileVacuumOverlay;
+ private bool _vacuumOverlayEnabledDueToCvar; // due to CVar being enabled
+ private bool _vacuumOverlayEnabledIntensity; // due to non zero intensity in setting
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _cfgManager.OnValueChanged(CCVars.VacuumOverlay, OnVacuumOverlayChanged, true);
+ _cfgManager.OnValueChanged(CCVars.VacuumOverlayIntensity, OnVacuumOverlayIntensityChanged, true);
+ }
+
+ public override void Shutdown()
+ {
+ base.Shutdown();
+
+ _cfgManager.UnsubValueChanged(CCVars.VacuumOverlay, OnVacuumOverlayChanged);
+ _cfgManager.UnsubValueChanged(CCVars.VacuumOverlayIntensity, OnVacuumOverlayIntensityChanged);
+ _overlayMan.RemoveOverlay();
+ }
+
+ private void OnVacuumOverlayChanged(bool enabled)
+ {
+ _vacuumOverlayEnabledDueToCvar = enabled;
+ UpdateOverlay();
+ }
+
+ private void OnVacuumOverlayIntensityChanged(float intensity)
+ {
+ _vacuumOverlayEnabledIntensity = intensity > 0f;
+ UpdateOverlay();
+ }
+
+ private void UpdateOverlay()
+ {
+ if (_vacuumOverlayEnabledDueToCvar && _vacuumOverlayEnabledIntensity)
+ {
+ _gasTileVacuumOverlay ??= new GasTileVacuumOverlay();
+
+ if (!_overlayMan.HasOverlay())
+ _overlayMan.AddOverlay(_gasTileVacuumOverlay);
+ }
+ else
+ {
+ _overlayMan.RemoveOverlay();
+ }
+ }
+}
diff --git a/Content.Client/Atmos/Overlays/GasTileVacuumOverlay.cs b/Content.Client/Atmos/Overlays/GasTileVacuumOverlay.cs
new file mode 100644
index 0000000000..0ddcd64a27
--- /dev/null
+++ b/Content.Client/Atmos/Overlays/GasTileVacuumOverlay.cs
@@ -0,0 +1,196 @@
+using System.Numerics;
+using Content.Client.Graphics;
+using Content.Shared.Atmos;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.EntitySystems;
+using Content.Shared.CCVar;
+using Robust.Client.Graphics;
+using Robust.Shared.Configuration;
+using Robust.Shared.Enums;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Prototypes;
+using Color = Robust.Shared.Maths.Color;
+
+namespace Content.Client.Atmos.Overlays;
+
+///
+/// Overlay responsible for rendering vacuum overlay.
+///
+public sealed partial class GasTileVacuumOverlay : Overlay
+{
+ public override bool RequestScreenTexture { get; set; } = true;
+ private static readonly ProtoId UnshadedShader = "unshaded";
+ private static readonly ProtoId VacuumOverlayShader = "VacuumDesaturation";
+ private static readonly Color EmptyColor = new(0, 0, 0, 0);
+ private static readonly Color MarkerColor = new(255f, 0, 0);
+
+ [Dependency] private IEntityManager _entManager = default!;
+ [Dependency] private IPrototypeManager _proto = default!;
+ [Dependency] private IClyde _clyde = default!;
+ [Dependency] private IConfigurationManager _configManager = default!;
+
+ private readonly SharedMapSystem _maps;
+ private readonly SharedTransformSystem _xformSys;
+ private readonly ShaderInstance _unshader;
+ private readonly ShaderInstance _shader;
+
+ private List> _intersectingGrids = new();
+ private readonly OverlayResourceCache _resources = new();
+
+ // Overlay settings
+ private float _intensity; // overlay intensity. 0.0f = turned off, 1.0f = full grayscale
+
+ public override OverlaySpace Space => OverlaySpace.WorldSpace;
+
+ public GasTileVacuumOverlay()
+ {
+ IoCManager.InjectDependencies(this);
+ _maps = _entManager.System();
+ _xformSys = _entManager.System();
+
+ _unshader = _proto.Index(UnshadedShader).Instance();
+ _shader = _proto.Index(VacuumOverlayShader).InstanceUnique();
+ _configManager.OnValueChanged(CCVars.VacuumOverlayIntensity, SetVacuumOverlayIntensity, invokeImmediately: true);
+ }
+
+ private void SetVacuumOverlayIntensity(float intensity)
+ {
+ _intensity = MathHelper.Clamp(intensity, 0f, 1f);
+ _shader.SetParameter("intensity", _intensity);
+ }
+
+ protected override bool BeforeDraw(in OverlayDrawArgs args)
+ {
+ if (args.MapId == MapId.Nullspace)
+ return false;
+
+ var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
+
+ var target = args.Viewport.RenderTarget;
+
+ // Probably the resolution of the game window changed, remake the textures.
+ if (res.VacuumTarget?.Texture.Size != target.Size)
+ {
+ res.VacuumTarget?.Dispose();
+ res.VacuumTarget = _clyde.CreateRenderTarget(
+ target.Size,
+ new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
+ name: $"{nameof(GasTileVacuumOverlay)}-blur");
+ }
+
+ var overlayQuery = _entManager.GetEntityQuery();
+
+ args.WorldHandle.UseShader(_unshader);
+
+ var mapId = args.MapId;
+ var worldAABB = args.WorldAABB;
+ var worldBounds = args.WorldBounds;
+ var worldHandle = args.WorldHandle;
+ var worldToViewportLocal = args.Viewport.GetWorldToLocalMatrix();
+
+ // If there is no Vacuum after checking all visible tiles, we can bail early
+ var anyVacuum = false;
+
+ // We're rendering in the context of the vacuum target texture, which will encode data as to where vacuum effect will be
+ args.WorldHandle.RenderInRenderTarget(res.VacuumTarget,
+ () =>
+ {
+ _intersectingGrids.Clear();
+ _maps.FindGridsIntersecting(mapId, worldAABB, ref _intersectingGrids);
+ foreach (var grid in _intersectingGrids)
+ {
+ if (!overlayQuery.TryGetComponent(grid.Owner, out var comp))
+ continue;
+
+ var (_, _, gridEntToWorld, worldToGridLocal) = _xformSys.GetWorldPositionRotationMatrixWithInv(grid.Owner);
+ var gridEntToViewportLocal = gridEntToWorld * worldToViewportLocal;
+
+ if (!Matrix3x2.Invert(gridEntToViewportLocal, out _))
+ continue;
+
+ // Draw commands (like DrawRect) will be using grid coordinates from here
+ worldHandle.SetTransform(gridEntToViewportLocal);
+
+ // We only care about tiles that fit in these bounds
+ var floatBounds = worldToGridLocal.TransformBox(worldBounds).Enlarged(grid.Comp.TileSize);
+
+ var localBounds = new Box2i(
+ (int)MathF.Floor(floatBounds.Left),
+ (int)MathF.Floor(floatBounds.Bottom),
+ (int)MathF.Ceiling(floatBounds.Right),
+ (int)MathF.Ceiling(floatBounds.Top));
+
+ // for each tile and its gas --->
+ foreach (var chunk in comp.Chunks.Values)
+ {
+ var enumerator = new GasChunkEnumerator(chunk);
+
+ while (enumerator.MoveNext(out var tileGas))
+ {
+ // Check and make sure the tile is within the viewport/screen
+ var tilePosition = chunk.Origin + (enumerator.X, enumerator.Y);
+ if (!localBounds.Contains(tilePosition))
+ continue;
+
+ if (tileGas.ByteGasTemperature.Value != ThermalByte.StateVacuum)
+ continue;
+
+ // Encode the strength in the red channel
+ // alpha set to 1 as tile is active
+ worldHandle.DrawRect(Box2.CenteredAround(tilePosition + grid.Comp.TileSizeHalfVector,
+ grid.Comp.TileSizeVector),
+ MarkerColor);
+ anyVacuum = true;
+ }
+ }
+ }
+ },
+ // This clears the buffer to all zero first...
+ EmptyColor);
+
+ // no distortion, no need to render
+ if (!anyVacuum)
+ {
+ args.WorldHandle.UseShader(null);
+ args.WorldHandle.SetTransform(Matrix3x2.Identity);
+ return false;
+ }
+
+ return true;
+ }
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
+
+ if (ScreenTexture is null || res.VacuumTarget is null)
+ return;
+
+ _shader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
+
+ args.WorldHandle.UseShader(_shader);
+ args.WorldHandle.DrawTextureRect(res.VacuumTarget.Texture, args.WorldBounds);
+
+ args.WorldHandle.UseShader(null);
+ args.WorldHandle.SetTransform(Matrix3x2.Identity);
+ }
+
+ protected override void DisposeBehavior()
+ {
+ _resources.Dispose();
+
+ _configManager.UnsubValueChanged(CCVars.VacuumOverlayIntensity, SetVacuumOverlayIntensity);
+ base.DisposeBehavior();
+ }
+
+ internal sealed class CachedResources : IDisposable
+ {
+ public IRenderTexture? VacuumTarget;
+
+ public void Dispose()
+ {
+ VacuumTarget?.Dispose();
+ }
+ }
+}
diff --git a/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml b/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml
index 55c47957a0..65104d7e9c 100644
--- a/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml
+++ b/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml
@@ -44,6 +44,7 @@
+
_mapGridQuery;
+ [Robust.Shared.IoC.Dependency] private EntityQuery _gasTileOverlayQuery;
+
+ ///
+ /// Per-tick cache of sessions.
+ ///
+ private readonly List _sessions = new();
+ private UpdatePlayerJob _updateJob;
+
+ private readonly Dictionary>> _lastSentChunks = new();
+
+ // Oh look its more duplicated decal system code!
+ private readonly ObjectPool> _chunkIndexPool =
+ new DefaultObjectPool>(
+ new DefaultPooledObjectPolicy>(), 64);
+ private readonly ObjectPool>> _chunkViewerPool =
+ new DefaultObjectPool>>(
+ new DefaultPooledObjectPolicy>>(), 64);
+
+ private bool _doSessionUpdate;
+
+ ///
+ /// Overlay update interval, in seconds.
+ ///
+ private float _updateInterval;
+
+ private int _thresholds;
+
+ public override void Initialize()
{
- [Robust.Shared.IoC.Dependency] private IGameTiming _gameTiming = default!;
- [Robust.Shared.IoC.Dependency] private IPlayerManager _playerManager = default!;
- [Robust.Shared.IoC.Dependency] private IParallelManager _parMan = default!;
- [Robust.Shared.IoC.Dependency] private AtmosphereSystem _atmosphereSystem = default!;
- [Robust.Shared.IoC.Dependency] private ChunkingSystem _chunkingSys = default!;
+ base.Initialize();
- [Robust.Shared.IoC.Dependency] private EntityQuery _mapGridQuery = default!;
- [Robust.Shared.IoC.Dependency] private EntityQuery _gasTileOverlayQuery = default!;
-
- ///
- /// Per-tick cache of sessions.
- ///
- private readonly List _sessions = new();
- private UpdatePlayerJob _updateJob;
-
- private readonly Dictionary>> _lastSentChunks = new();
-
- // Oh look its more duplicated decal system code!
- private ObjectPool> _chunkIndexPool =
- new DefaultObjectPool>(
- new DefaultPooledObjectPolicy>(), 64);
- private ObjectPool>> _chunkViewerPool =
- new DefaultObjectPool>>(
- new DefaultPooledObjectPolicy>>(), 64);
-
- private bool _doSessionUpdate;
-
- ///
- /// Overlay update interval, in seconds.
- ///
- private float _updateInterval;
-
- private int _thresholds;
-
- public override void Initialize()
+ _updateJob = new UpdatePlayerJob()
{
- base.Initialize();
+ EntManager = EntityManager,
+ System = this,
+ ChunkIndexPool = _chunkIndexPool,
+ Sessions = _sessions,
+ ChunkingSys = _chunkingSys,
+ ChunkViewerPool = _chunkViewerPool,
+ LastSentChunks = _lastSentChunks,
+ GridQuery = _mapGridQuery,
+ };
- _updateJob = new UpdatePlayerJob()
+ _playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
+
+ InitializeCVars();
+
+ SubscribeLocalEvent(Reset);
+ SubscribeLocalEvent(OnStartup);
+ }
+
+ private void OnStartup(EntityUid uid, GasTileOverlayComponent component, ComponentStartup args)
+ {
+ // This **shouldn't** be required, but just in case we ever get entity prototypes that have gas overlays, we
+ // need to ensure that we send an initial full state to players.
+ Dirty(uid, component);
+ }
+
+ public override void Shutdown()
+ {
+ base.Shutdown();
+ _playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
+ }
+
+ private void OnPvsToggle(bool value)
+ {
+ if (value == PvsEnabled)
+ return;
+
+ PvsEnabled = value;
+
+ if (value)
+ return;
+
+ foreach (var lastSent in _lastSentChunks.Values)
+ {
+ foreach (var set in lastSent.Values)
{
- EntManager = EntityManager,
- System = this,
- ChunkIndexPool = _chunkIndexPool,
- Sessions = _sessions,
- ChunkingSys = _chunkingSys,
- ChunkViewerPool = _chunkViewerPool,
- LastSentChunks = _lastSentChunks,
- GridQuery = _mapGridQuery,
- };
-
- _playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
-
- InitializeCVars();
-
- SubscribeLocalEvent(Reset);
- SubscribeLocalEvent(OnStartup);
+ set.Clear();
+ _chunkIndexPool.Return(set);
+ }
+ lastSent.Clear();
}
- private void OnStartup(EntityUid uid, GasTileOverlayComponent component, ComponentStartup args)
+ // PVS was turned off, ensure data gets sent to all clients.
+ var query = AllEntityQuery();
+ while (query.MoveNext(out var uid, out var grid, out var meta))
{
- // This **shouldn't** be required, but just in case we ever get entity prototypes that have gas overlays, we
- // need to ensure that we send an initial full state to players.
- Dirty(uid, component);
+ grid.ForceTick = _gameTiming.CurTick;
+ Dirty(uid, grid, meta);
}
+ }
- public override void Shutdown()
+ private void UpdateTickRate(float value) => _updateInterval = value > 0.0f ? 1 / value : float.MaxValue;
+ private void UpdateThresholds(int value) => _thresholds = value;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Invalidate(Entity grid, Vector2i index)
+ {
+ if (_gasTileOverlayQuery.Resolve(grid.Owner, ref grid.Comp))
+ grid.Comp.InvalidTiles.Add(index);
+ }
+
+ private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
+ {
+ if (e.NewStatus != SessionStatus.InGame)
{
- base.Shutdown();
- _playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
- }
-
- private void OnPvsToggle(bool value)
- {
- if (value == PvsEnabled)
- return;
-
- PvsEnabled = value;
-
- if (value)
- return;
-
- foreach (var lastSent in _lastSentChunks.Values)
+ if (_lastSentChunks.Remove(e.Session, out var sets))
{
- foreach (var set in lastSent.Values)
+ foreach (var set in sets.Values)
{
set.Clear();
_chunkIndexPool.Return(set);
}
- lastSent.Clear();
- }
-
- // PVS was turned off, ensure data gets sent to all clients.
- var query = AllEntityQuery();
- while (query.MoveNext(out var uid, out var grid, out var meta))
- {
- grid.ForceTick = _gameTiming.CurTick;
- Dirty(uid, grid, meta);
}
}
- private void UpdateTickRate(float value) => _updateInterval = value > 0.0f ? 1 / value : float.MaxValue;
- private void UpdateThresholds(int value) => _thresholds = value;
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public void Invalidate(Entity grid, Vector2i index)
+ if (!_lastSentChunks.ContainsKey(e.Session))
{
- if (_gasTileOverlayQuery.Resolve(grid.Owner, ref grid.Comp))
- grid.Comp.InvalidTiles.Add(index);
+ _lastSentChunks[e.Session] = new();
}
+ }
- private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
+ private byte GetOpacity(float moles, float molesVisible, float molesVisibleMax)
+ {
+ return (byte) (ContentHelpers.RoundToLevels(
+ MathHelper.Clamp01((moles - molesVisible) /
+ (molesVisibleMax - molesVisible)) * 255, byte.MaxValue,
+ _thresholds) * 255 / (_thresholds - 1));
+ }
+
+ public GasOverlayData GetOverlayData(GasMixture? mixture)
+ {
+ ThermalByte byteTemp;
+ if (mixture == null)
{
- if (e.NewStatus != SessionStatus.InGame)
+ byteTemp = new();
+ byteTemp.SetVacuum();
+ }
+ else
+ byteTemp = new(mixture.Temperature);
+
+ var data = new GasOverlayData(0, new byte[VisibleGasId.Length], byteTemp);
+
+ for (var i = 0; i < VisibleGasId.Length; i++)
+ {
+ var id = VisibleGasId[i];
+ var gas = _atmosphereSystem.GetGas(id);
+ var moles = mixture?[id] ?? 0f;
+ ref var opacity = ref data.Opacity[i];
+
+ if (moles < gas.GasMolesVisible)
{
- if (_lastSentChunks.Remove(e.Session, out var sets))
- {
- foreach (var set in sets.Values)
- {
- set.Clear();
- _chunkIndexPool.Return(set);
- }
- }
+ continue;
}
- if (!_lastSentChunks.ContainsKey(e.Session))
- {
- _lastSentChunks[e.Session] = new();
- }
- }
-
- private byte GetOpacity(float moles, float molesVisible, float molesVisibleMax)
- {
- return (byte) (ContentHelpers.RoundToLevels(
- MathHelper.Clamp01((moles - molesVisible) /
- (molesVisibleMax - molesVisible)) * 255, byte.MaxValue,
+ opacity = (byte) (ContentHelpers.RoundToLevels(
+ MathHelper.Clamp01((moles - gas.GasMolesVisible) /
+ (gas.GasMolesVisibleMax - gas.GasMolesVisible)) * 255, byte.MaxValue,
_thresholds) * 255 / (_thresholds - 1));
}
- public GasOverlayData GetOverlayData(GasMixture? mixture)
+ return data;
+ }
+
+ ///
+ /// Updates the visuals for a tile on some grid chunk. Returns true if the visuals have changed.
+ ///
+ private bool UpdateChunkTile(GridAtmosphereComponent gridAtmosphere, GasOverlayChunk chunk, Vector2i index)
+ {
+ ref var oldData = ref chunk.TileData[chunk.GetDataIndex(index)];
+ if (!gridAtmosphere.Tiles.TryGetValue(index, out var tile))
{
- ThermalByte byteTemp;
- if (mixture == null)
- {
- byteTemp = new();
- byteTemp.SetVacuum();
- }
- else
- byteTemp = new(mixture.Temperature);
+ if (oldData.Equals(default))
+ return false;
- var data = new GasOverlayData(0, new byte[VisibleGasId.Length], byteTemp);
+ chunk.LastUpdate = _gameTiming.CurTick;
+ oldData = default;
+ return true;
+ }
+ var changed = false;
+
+ ThermalByte newByteTemp = new();
+
+ if (tile.Hotspot.Valid)
+ newByteTemp.SetTemperature(tile.Hotspot.Temperature);
+ else if (!tile.Space && tile.Air?.TotalMoles <= ThermalByte.MinimalPressureThreshold)
+ newByteTemp.SetVacuum();
+ else if (!tile.Space && tile.Air != null)
+ newByteTemp = new(tile.Air.Temperature);
+
+ if (oldData.Equals(default))
+ {
+ changed = true;
+ oldData = new GasOverlayData(tile.Hotspot.State, new byte[VisibleGasId.Length], newByteTemp);
+ }
+ else if (oldData.FireState != tile.Hotspot.State ||
+ Math.Abs(oldData.ByteGasTemperature.Value - newByteTemp.Value) > 1 || // Dirty Temperature when there is more then 1 byte difference. That should measure up to minimum 4 degreese difference, 6 degreese on average.
+ oldData.ByteGasTemperature.Value != newByteTemp.Value && newByteTemp.Value > ThermalByte.TempResolution) // change of special ThermalByte value
+ {
+ changed = true;
+ oldData = new GasOverlayData(tile.Hotspot.State, oldData.Opacity, newByteTemp);
+ }
+
+ if (tile is {Air: not null, NoGridTile: false})
+ {
for (var i = 0; i < VisibleGasId.Length; i++)
{
var id = VisibleGasId[i];
var gas = _atmosphereSystem.GetGas(id);
- var moles = mixture?[id] ?? 0f;
- ref var opacity = ref data.Opacity[i];
+ var moles = tile.Air[id];
+ ref var oldOpacity = ref oldData.Opacity[i];
if (moles < gas.GasMolesVisible)
{
+ if (oldOpacity != 0)
+ {
+ oldOpacity = 0;
+ changed = true;
+ }
+
continue;
}
- opacity = (byte) (ContentHelpers.RoundToLevels(
- MathHelper.Clamp01((moles - gas.GasMolesVisible) /
- (gas.GasMolesVisibleMax - gas.GasMolesVisible)) * 255, byte.MaxValue,
- _thresholds) * 255 / (_thresholds - 1));
- }
+ var opacity = GetOpacity(moles, gas.GasMolesVisible, gas.GasMolesVisibleMax);
- return data;
+ if (oldOpacity == opacity)
+ continue;
+
+ oldOpacity = opacity;
+ changed = true;
+ }
+ }
+ else
+ {
+ for (var i = 0; i < VisibleGasId.Length; i++)
+ {
+ changed |= oldData.Opacity[i] != 0;
+ oldData.Opacity[i] = 0;
+ }
}
- ///
- /// Updates the visuals for a tile on some grid chunk. Returns true if the visuals have changed.
- ///
- private bool UpdateChunkTile(GridAtmosphereComponent gridAtmosphere, GasOverlayChunk chunk, Vector2i index)
+ if (!changed)
+ return false;
+
+ chunk.LastUpdate = _gameTiming.CurTick;
+ return true;
+ }
+
+ private void UpdateOverlayData()
+ {
+ // TODO parallelize?
+ var query = AllEntityQuery();
+ while (query.MoveNext(out var uid, out var overlay, out var gam, out var meta))
{
- ref var oldData = ref chunk.TileData[chunk.GetDataIndex(index)];
- if (!gridAtmosphere.Tiles.TryGetValue(index, out var tile))
- {
- if (oldData.Equals(default))
- return false;
-
- chunk.LastUpdate = _gameTiming.CurTick;
- oldData = default;
- return true;
- }
-
var changed = false;
-
- ThermalByte newByteTemp = new();
-
- if (tile.Hotspot.Valid)
- newByteTemp.SetTemperature(tile.Hotspot.Temperature);
- else if (!tile.Space && tile.Air?.TotalMoles <= 5f)
- newByteTemp.SetVacuum();
- else if (!tile.Space && tile.Air != null)
- newByteTemp = new(tile.Air.Temperature);
-
- if (oldData.Equals(default))
+ foreach (var index in overlay.InvalidTiles)
{
- changed = true;
- oldData = new GasOverlayData(tile.Hotspot.State, new byte[VisibleGasId.Length], newByteTemp);
- }
- else if (oldData.FireState != tile.Hotspot.State ||
- Math.Abs(oldData.ByteGasTemperature.Value - newByteTemp.Value) > 1 || // Dirty Temperature when there is more then 1 byte difference. That should measure up to minimum 4 degreese difference, 6 degreese on average.
- (oldData.ByteGasTemperature.Value != newByteTemp.Value && newByteTemp.Value > ThermalByte.TempResolution)) // change of special ThermalByte value
- {
- changed = true;
- oldData = new GasOverlayData(tile.Hotspot.State, oldData.Opacity, newByteTemp);
+ var chunkIndex = GetGasChunkIndices(index);
+
+ if (!overlay.Chunks.TryGetValue(chunkIndex, out var chunk))
+ overlay.Chunks[chunkIndex] = chunk = new GasOverlayChunk(chunkIndex);
+
+ changed |= UpdateChunkTile(gam, chunk, index);
}
- if (tile is {Air: not null, NoGridTile: false})
- {
- for (var i = 0; i < VisibleGasId.Length; i++)
- {
- var id = VisibleGasId[i];
- var gas = _atmosphereSystem.GetGas(id);
- var moles = tile.Air[id];
- ref var oldOpacity = ref oldData.Opacity[i];
+ if (changed)
+ Dirty(uid, overlay, meta);
- if (moles < gas.GasMolesVisible)
- {
- if (oldOpacity != 0)
- {
- oldOpacity = 0;
- changed = true;
- }
-
- continue;
- }
-
- var opacity = GetOpacity(moles, gas.GasMolesVisible, gas.GasMolesVisibleMax);
-
- if (oldOpacity == opacity)
- continue;
-
- oldOpacity = opacity;
- changed = true;
- }
- }
- else
- {
- for (var i = 0; i < VisibleGasId.Length; i++)
- {
- changed |= oldData.Opacity[i] != 0;
- oldData.Opacity[i] = 0;
- }
- }
-
- if (!changed)
- return false;
-
- chunk.LastUpdate = _gameTiming.CurTick;
- return true;
- }
-
- private void UpdateOverlayData()
- {
- // TODO parallelize?
- var query = AllEntityQuery();
- while (query.MoveNext(out var uid, out var overlay, out var gam, out var meta))
- {
- var changed = false;
- foreach (var index in overlay.InvalidTiles)
- {
- var chunkIndex = GetGasChunkIndices(index);
-
- if (!overlay.Chunks.TryGetValue(chunkIndex, out var chunk))
- overlay.Chunks[chunkIndex] = chunk = new GasOverlayChunk(chunkIndex);
-
- changed |= UpdateChunkTile(gam, chunk, index);
- }
-
- if (changed)
- Dirty(uid, overlay, meta);
-
- overlay.InvalidTiles.Clear();
- }
- }
-
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
- AccumulatedFrameTime += frameTime;
-
- if (_doSessionUpdate)
- {
- UpdateSessions();
- return;
- }
-
- if (AccumulatedFrameTime < _updateInterval)
- return;
-
- AccumulatedFrameTime -= _updateInterval;
-
- // First, update per-chunk visual data for any invalidated tiles.
- UpdateOverlayData();
-
- // Then, next tick we send the data to players.
- // This is to avoid doing all the work in the same tick.
- _doSessionUpdate = true;
- }
-
- public void UpdateSessions()
- {
- _doSessionUpdate = false;
-
- if (!PvsEnabled)
- return;
-
- // Now we'll go through each player, then through each chunk in range of that player checking if the player is still in range
- // If they are, check if they need the new data to send (i.e. if there's an overlay for the gas).
- // Afterwards we reset all the chunk data for the next time we tick.
- _sessions.Clear();
-
- foreach (var player in _playerManager.Sessions)
- {
- if (player.Status != SessionStatus.InGame)
- continue;
-
- _sessions.Add(player);
- }
-
- if (_sessions.Count == 0)
- return;
-
- _parMan.ProcessNow(_updateJob, _sessions.Count);
- _updateJob.LastSessionUpdate = _gameTiming.CurTick;
- }
-
- public void Reset(RoundRestartCleanupEvent ev)
- {
- foreach (var data in _lastSentChunks.Values)
- {
- foreach (var previous in data.Values)
- {
- previous.Clear();
- _chunkIndexPool.Return(previous);
- }
-
- data.Clear();
- }
- }
-
- #region Jobs
-
- ///
- /// Updates per player gas overlay data.
- ///
- private record struct UpdatePlayerJob : IParallelRobustJob
- {
- public int BatchSize => 2;
-
- public IEntityManager EntManager;
- public ChunkingSystem ChunkingSys;
- public GasTileOverlaySystem System;
- public ObjectPool> ChunkIndexPool;
- public ObjectPool>> ChunkViewerPool;
-
- public GameTick LastSessionUpdate;
- public Dictionary>> LastSentChunks;
- public List Sessions;
-
- public EntityQuery GridQuery;
-
- public void Execute(int index)
- {
- var playerSession = Sessions[index];
- var chunksInRange = ChunkingSys.GetChunksForSession(playerSession, ChunkSize, ChunkIndexPool, ChunkViewerPool);
- var previouslySent = LastSentChunks[playerSession];
-
- var ev = new GasOverlayUpdateEvent();
-
- foreach (var (netGrid, oldIndices) in previouslySent)
- {
- // Mark the whole grid as stale and flag for removal.
- if (!chunksInRange.TryGetValue(netGrid, out var chunks))
- {
- previouslySent.Remove(netGrid);
-
- // If grid was deleted then don't worry about sending it to the client.
- if (!EntManager.TryGetEntity(netGrid, out var gridId) || GridQuery.HasComp(gridId.Value))
- ev.RemovedChunks[netGrid] = oldIndices;
- else
- {
- oldIndices.Clear();
- ChunkIndexPool.Return(oldIndices);
- }
-
- continue;
- }
-
- var old = ChunkIndexPool.Get();
- DebugTools.Assert(old.Count == 0);
- foreach (var chunk in oldIndices)
- {
- if (!chunks.Contains(chunk))
- old.Add(chunk);
- }
-
- if (old.Count == 0)
- ChunkIndexPool.Return(old);
- else
- ev.RemovedChunks.Add(netGrid, old);
- }
-
- foreach (var (netGrid, gridChunks) in chunksInRange)
- {
- // Not all grids have atmospheres.
- if (!EntManager.TryGetEntity(netGrid, out var grid) || !EntManager.TryGetComponent(grid, out GasTileOverlayComponent? overlay))
- continue;
-
- List dataToSend = new();
- ev.UpdatedChunks[netGrid] = dataToSend;
-
- previouslySent.TryGetValue(netGrid, out var previousChunks);
-
- foreach (var gIndex in gridChunks)
- {
- if (!overlay.Chunks.TryGetValue(gIndex, out var value))
- continue;
-
- // If the chunk was updated since we last sent it, send it again
- if (value.LastUpdate > LastSessionUpdate)
- {
- dataToSend.Add(value);
- continue;
- }
-
- // Always send it if we didn't previously send it
- if (previousChunks == null || !previousChunks.Contains(gIndex))
- dataToSend.Add(value);
- }
-
- previouslySent[netGrid] = gridChunks;
- if (previousChunks != null)
- {
- previousChunks.Clear();
- ChunkIndexPool.Return(previousChunks);
- }
- }
-
- if (ev.UpdatedChunks.Count != 0 || ev.RemovedChunks.Count != 0)
- System.RaiseNetworkEvent(ev, playerSession.Channel);
- }
- }
-
- #endregion
-
- private void InitializeCVars()
- {
- Subs.CVar(ConfMan, CCVars.NetGasOverlayTickRate, UpdateTickRate, true);
- Subs.CVar(ConfMan, CCVars.GasOverlayThresholds, UpdateThresholds, true);
- Subs.CVar(ConfMan, CVars.NetPVS, OnPvsToggle, true);
+ overlay.InvalidTiles.Clear();
}
}
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+ AccumulatedFrameTime += frameTime;
+
+ if (_doSessionUpdate)
+ {
+ UpdateSessions();
+ return;
+ }
+
+ if (AccumulatedFrameTime < _updateInterval)
+ return;
+
+ AccumulatedFrameTime -= _updateInterval;
+
+ // First, update per-chunk visual data for any invalidated tiles.
+ UpdateOverlayData();
+
+ // Then, next tick we send the data to players.
+ // This is to avoid doing all the work in the same tick.
+ _doSessionUpdate = true;
+ }
+
+ public void UpdateSessions()
+ {
+ _doSessionUpdate = false;
+
+ if (!PvsEnabled)
+ return;
+
+ // Now we'll go through each player, then through each chunk in range of that player checking if the player is still in range
+ // If they are, check if they need the new data to send (i.e. if there's an overlay for the gas).
+ // Afterwards we reset all the chunk data for the next time we tick.
+ _sessions.Clear();
+
+ foreach (var player in _playerManager.Sessions)
+ {
+ if (player.Status != SessionStatus.InGame)
+ continue;
+
+ _sessions.Add(player);
+ }
+
+ if (_sessions.Count == 0)
+ return;
+
+ _parMan.ProcessNow(_updateJob, _sessions.Count);
+ _updateJob.LastSessionUpdate = _gameTiming.CurTick;
+ }
+
+ public void Reset(RoundRestartCleanupEvent ev)
+ {
+ foreach (var data in _lastSentChunks.Values)
+ {
+ foreach (var previous in data.Values)
+ {
+ previous.Clear();
+ _chunkIndexPool.Return(previous);
+ }
+
+ data.Clear();
+ }
+ }
+
+ #region Jobs
+
+ ///
+ /// Updates per player gas overlay data.
+ ///
+ private record struct UpdatePlayerJob : IParallelRobustJob
+ {
+ public int BatchSize => 2;
+
+ public IEntityManager EntManager;
+ public ChunkingSystem ChunkingSys;
+ public GasTileOverlaySystem System;
+ public ObjectPool> ChunkIndexPool;
+ public ObjectPool>> ChunkViewerPool;
+
+ public GameTick LastSessionUpdate;
+ public Dictionary>> LastSentChunks;
+ public List Sessions;
+
+ public EntityQuery GridQuery;
+
+ public void Execute(int index)
+ {
+ var playerSession = Sessions[index];
+ var chunksInRange = ChunkingSys.GetChunksForSession(playerSession, ChunkSize, ChunkIndexPool, ChunkViewerPool);
+ var previouslySent = LastSentChunks[playerSession];
+
+ var ev = new GasOverlayUpdateEvent();
+
+ foreach (var (netGrid, oldIndices) in previouslySent)
+ {
+ // Mark the whole grid as stale and flag for removal.
+ if (!chunksInRange.TryGetValue(netGrid, out var chunks))
+ {
+ previouslySent.Remove(netGrid);
+
+ // If grid was deleted then don't worry about sending it to the client.
+ if (!EntManager.TryGetEntity(netGrid, out var gridId) || GridQuery.HasComp(gridId.Value))
+ ev.RemovedChunks[netGrid] = oldIndices;
+ else
+ {
+ oldIndices.Clear();
+ ChunkIndexPool.Return(oldIndices);
+ }
+
+ continue;
+ }
+
+ var old = ChunkIndexPool.Get();
+ DebugTools.Assert(old.Count == 0);
+ foreach (var chunk in oldIndices)
+ {
+ if (!chunks.Contains(chunk))
+ old.Add(chunk);
+ }
+
+ if (old.Count == 0)
+ ChunkIndexPool.Return(old);
+ else
+ ev.RemovedChunks.Add(netGrid, old);
+ }
+
+ foreach (var (netGrid, gridChunks) in chunksInRange)
+ {
+ // Not all grids have atmospheres.
+ if (!EntManager.TryGetEntity(netGrid, out var grid) || !EntManager.TryGetComponent(grid, out GasTileOverlayComponent? overlay))
+ continue;
+
+ List dataToSend = new();
+ ev.UpdatedChunks[netGrid] = dataToSend;
+
+ previouslySent.TryGetValue(netGrid, out var previousChunks);
+
+ foreach (var gIndex in gridChunks)
+ {
+ if (!overlay.Chunks.TryGetValue(gIndex, out var value))
+ continue;
+
+ // If the chunk was updated since we last sent it, send it again
+ if (value.LastUpdate > LastSessionUpdate)
+ {
+ dataToSend.Add(value);
+ continue;
+ }
+
+ // Always send it if we didn't previously send it
+ if (previousChunks == null || !previousChunks.Contains(gIndex))
+ dataToSend.Add(value);
+ }
+
+ previouslySent[netGrid] = gridChunks;
+ if (previousChunks != null)
+ {
+ previousChunks.Clear();
+ ChunkIndexPool.Return(previousChunks);
+ }
+ }
+
+ if (ev.UpdatedChunks.Count != 0 || ev.RemovedChunks.Count != 0)
+ System.RaiseNetworkEvent(ev, playerSession.Channel);
+ }
+ }
+
+ #endregion
+
+ private void InitializeCVars()
+ {
+ Subs.CVar(ConfMan, CCVars.NetGasOverlayTickRate, UpdateTickRate, true);
+ Subs.CVar(ConfMan, CCVars.GasOverlayThresholds, UpdateThresholds, true);
+ Subs.CVar(ConfMan, CVars.NetPVS, OnPvsToggle, true);
+ }
}
diff --git a/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs b/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs
index ad3dec38c7..d3e6c818a0 100644
--- a/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs
+++ b/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs
@@ -162,6 +162,11 @@ public struct ThermalByte : IEquatable
public const float TempDegreeResolution = (TempMaximum - TempMinimum) / TempResolution;
public const float TempToByteFactor = TempResolution / (TempMaximum - TempMinimum);
+ ///
+ /// The minimum pressure gas must have for tile temperature to be networked to clients. Below this threshold thermal byte is set to StateVacuum.
+ ///
+ public const float MinimalPressureThreshold = 20f;
+
private byte _coreValue;
public ThermalByte(float temperatureKelvin)
diff --git a/Content.Shared/CCVar/CCVars.Accessibility.Motion.cs b/Content.Shared/CCVar/CCVars.Accessibility.Motion.cs
index 65b33da7ce..cbc68788e3 100644
--- a/Content.Shared/CCVar/CCVars.Accessibility.Motion.cs
+++ b/Content.Shared/CCVar/CCVars.Accessibility.Motion.cs
@@ -43,6 +43,18 @@ public sealed partial class CCVars
public static readonly CVarDef DisableFlashEffect =
CVarDef.Create("accessibility.disable_flash_effect", false, CVar.CLIENTONLY | CVar.ARCHIVE);
+ ///
+ /// Decreases the intensity of the vacuum overlay 1.0f is maximum.
+ ///
+ public static readonly CVarDef VacuumOverlayIntensity =
+ CVarDef.Create("accessibility.vacuum_overlay_intensity", 0.5f, CVar.CLIENTONLY | CVar.ARCHIVE);
+
+ ///
+ /// Toggles vacuum overlay serverside. If you do not want it as an option for your players use this.
+ ///
+ public static readonly CVarDef VacuumOverlay =
+ CVarDef.Create("accessibility.vacuum_overlay", true, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
+
///
/// Decreases motion on the heat distortion shader.
///
diff --git a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl
index 5996e122b8..27a1cbfadd 100644
--- a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl
+++ b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl
@@ -403,6 +403,7 @@ ui-options-chat-window-opacity = Chat window opacity
ui-options-speech-bubble-text-opacity = Speech bubble text opacity
ui-options-speech-bubble-speaker-opacity = Speech bubble speaker opacity
ui-options-speech-bubble-background-opacity = Speech bubble background opacity
+ui-options-vacuum-overlay-intensity = Vacuum overlay intensity
ui-options-censor-nudity = Censor character nudity
diff --git a/Resources/Prototypes/Shaders/shaders.yml b/Resources/Prototypes/Shaders/shaders.yml
index 8758d96472..88bb31916e 100644
--- a/Resources/Prototypes/Shaders/shaders.yml
+++ b/Resources/Prototypes/Shaders/shaders.yml
@@ -140,3 +140,8 @@
id: Sharpness
kind: source
path: "/Textures/Shaders/sharpness.swsl"
+
+- type: shader
+ id: VacuumDesaturation
+ kind: source
+ path: "/Textures/Shaders/vacuum_desaturation.swsl"
diff --git a/Resources/Textures/Shaders/vacuum_desaturation.swsl b/Resources/Textures/Shaders/vacuum_desaturation.swsl
new file mode 100644
index 0000000000..bdcacd8460
--- /dev/null
+++ b/Resources/Textures/Shaders/vacuum_desaturation.swsl
@@ -0,0 +1,14 @@
+uniform sampler2D SCREEN_TEXTURE;
+uniform mediump float intensity;
+
+void fragment()
+{
+ if (texture2D(TEXTURE, UV).r < 0.1)
+ discard;
+
+ mediump vec4 screen_pixel = texture2D(SCREEN_TEXTURE, UV);
+ mediump float gray = dot(screen_pixel.rgb, vec3(0.299, 0.587, 0.114));
+
+ screen_pixel.rgb = mix(screen_pixel.rgb, vec3(gray), intensity);
+ COLOR = screen_pixel;
+}