diff --git a/Content.Client/Atmos/EntitySystems/GasTileFireOverlaySystem.cs b/Content.Client/Atmos/EntitySystems/GasTileFireOverlaySystem.cs
new file mode 100644
index 0000000000..b1bfdb70ab
--- /dev/null
+++ b/Content.Client/Atmos/EntitySystems/GasTileFireOverlaySystem.cs
@@ -0,0 +1,30 @@
+using Content.Client.Atmos.Overlays;
+using JetBrains.Annotations;
+using Robust.Client.Graphics;
+
+namespace Content.Client.Atmos.EntitySystems;
+
+///
+/// System responsible for rendering atmos fire animations using .
+///
+[UsedImplicitly]
+public sealed class GasTileFireOverlaySystem : EntitySystem
+{
+ [Dependency] private readonly IOverlayManager _overlayMan = default!;
+
+ private GasTileFireOverlay _fireOverlay = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _fireOverlay = new GasTileFireOverlay();
+ _overlayMan.AddOverlay(_fireOverlay);
+ }
+
+ public override void Shutdown()
+ {
+ base.Shutdown();
+ _overlayMan.RemoveOverlay();
+ }
+}
diff --git a/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs b/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs
index ad26436946..8de649b81c 100644
--- a/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs
+++ b/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs
@@ -1,106 +1,85 @@
-using Content.Client.Atmos.Overlays;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using JetBrains.Annotations;
-using Robust.Client.GameObjects;
-using Robust.Client.Graphics;
-using Robust.Client.ResourceManagement;
using Robust.Shared.GameStates;
-namespace Content.Client.Atmos.EntitySystems
+namespace Content.Client.Atmos.EntitySystems;
+
+[UsedImplicitly]
+public sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem
{
- [UsedImplicitly]
- public sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem
+ public override void Initialize()
{
- [Dependency] private readonly IResourceCache _resourceCache = default!;
- [Dependency] private readonly IOverlayManager _overlayMan = default!;
- [Dependency] private readonly SpriteSystem _spriteSys = default!;
- [Dependency] private readonly SharedTransformSystem _xformSys = default!;
+ base.Initialize();
+ SubscribeNetworkEvent(HandleGasOverlayUpdate);
+ SubscribeLocalEvent(OnHandleState);
+ }
- private GasTileOverlay _overlay = default!;
+ private void OnHandleState(EntityUid gridUid, GasTileOverlayComponent comp, ref ComponentHandleState args)
+ {
+ Dictionary modifiedChunks;
- public override void Initialize()
+ switch (args.Current)
{
- base.Initialize();
- SubscribeNetworkEvent(HandleGasOverlayUpdate);
- SubscribeLocalEvent(OnHandleState);
-
- _overlay = new GasTileOverlay(this, EntityManager, _resourceCache, ProtoMan, _spriteSys, _xformSys);
- _overlayMan.AddOverlay(_overlay);
- }
-
- public override void Shutdown()
- {
- base.Shutdown();
- _overlayMan.RemoveOverlay();
- }
-
- private void OnHandleState(EntityUid gridUid, GasTileOverlayComponent comp, ref ComponentHandleState args)
- {
- Dictionary modifiedChunks;
-
- switch (args.Current)
+ // is this a delta or full state?
+ case GasTileOverlayDeltaState delta:
{
- // is this a delta or full state?
- case GasTileOverlayDeltaState delta:
+ modifiedChunks = delta.ModifiedChunks;
+ foreach (var index in comp.Chunks.Keys)
{
- modifiedChunks = delta.ModifiedChunks;
- foreach (var index in comp.Chunks.Keys)
- {
- if (!delta.AllChunks.Contains(index))
- comp.Chunks.Remove(index);
- }
-
- break;
+ if (!delta.AllChunks.Contains(index))
+ comp.Chunks.Remove(index);
}
- case GasTileOverlayState state:
- {
- modifiedChunks = state.Chunks;
- foreach (var index in comp.Chunks.Keys)
- {
- if (!state.Chunks.ContainsKey(index))
- comp.Chunks.Remove(index);
- }
- break;
- }
- default:
- return;
+ break;
}
-
- foreach (var (index, data) in modifiedChunks)
+ case GasTileOverlayState state:
{
- comp.Chunks[index] = data;
+ modifiedChunks = state.Chunks;
+ foreach (var index in comp.Chunks.Keys)
+ {
+ if (!state.Chunks.ContainsKey(index))
+ comp.Chunks.Remove(index);
+ }
+
+ break;
+ }
+ default:
+ return;
+ }
+
+ foreach (var (index, data) in modifiedChunks)
+ {
+ comp.Chunks[index] = data;
+ }
+ }
+
+ private void HandleGasOverlayUpdate(GasOverlayUpdateEvent ev)
+ {
+ foreach (var (nent, removedIndicies) in ev.RemovedChunks)
+ {
+ var grid = GetEntity(nent);
+
+ if (!TryComp(grid, out GasTileOverlayComponent? comp))
+ continue;
+
+ foreach (var index in removedIndicies)
+ {
+ comp.Chunks.Remove(index);
}
}
- private void HandleGasOverlayUpdate(GasOverlayUpdateEvent ev)
+ foreach (var (nent, gridData) in ev.UpdatedChunks)
{
- foreach (var (nent, removedIndicies) in ev.RemovedChunks)
+ var grid = GetEntity(nent);
+
+ if (!TryComp(grid, out GasTileOverlayComponent? comp))
+ continue;
+
+ foreach (var chunkData in gridData)
{
- var grid = GetEntity(nent);
-
- if (!TryComp(grid, out GasTileOverlayComponent? comp))
- continue;
-
- foreach (var index in removedIndicies)
- {
- comp.Chunks.Remove(index);
- }
- }
-
- foreach (var (nent, gridData) in ev.UpdatedChunks)
- {
- var grid = GetEntity(nent);
-
- if (!TryComp(grid, out GasTileOverlayComponent? comp))
- continue;
-
- foreach (var chunkData in gridData)
- {
- comp.Chunks[chunkData.Index] = chunkData;
- }
+ comp.Chunks[chunkData.Index] = chunkData;
}
}
}
diff --git a/Content.Client/Atmos/EntitySystems/GasTileVisibleGasOverlaySystem.cs b/Content.Client/Atmos/EntitySystems/GasTileVisibleGasOverlaySystem.cs
new file mode 100644
index 0000000000..3d7f26cc30
--- /dev/null
+++ b/Content.Client/Atmos/EntitySystems/GasTileVisibleGasOverlaySystem.cs
@@ -0,0 +1,31 @@
+using Content.Client.Atmos.Overlays;
+using JetBrains.Annotations;
+using Robust.Client.Graphics;
+
+namespace Content.Client.Atmos.EntitySystems;
+
+///
+/// System responsible for rendering visible atmos gasses (like plasma for example) using .
+///
+[UsedImplicitly]
+public sealed class GasTileVisibleGasOverlaySystem : EntitySystem
+{
+ [Dependency] private readonly IOverlayManager _overlayMan = default!;
+
+ private GasTileVisibleGasOverlay _visibleGasOverlay = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _visibleGasOverlay = new GasTileVisibleGasOverlay();
+ _overlayMan.AddOverlay(_visibleGasOverlay);
+ }
+
+ public override void Shutdown()
+ {
+ base.Shutdown();
+ _overlayMan.RemoveOverlay();
+ }
+
+}
diff --git a/Content.Client/Atmos/Overlays/GasTileFireOverlay.cs b/Content.Client/Atmos/Overlays/GasTileFireOverlay.cs
new file mode 100644
index 0000000000..310bac8669
--- /dev/null
+++ b/Content.Client/Atmos/Overlays/GasTileFireOverlay.cs
@@ -0,0 +1,172 @@
+using Content.Client.Atmos.EntitySystems;
+using Content.Shared.Atmos;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Species;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Shared.Enums;
+using Robust.Shared.Graphics.RSI;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+using System.Numerics;
+
+namespace Content.Client.Atmos.Overlays;
+
+///
+/// Overlay responsible for rendering atmos fire animation.
+///
+public sealed class GasTileFireOverlay : Overlay
+{
+ [Dependency] private readonly IPrototypeManager _protoMan = default!;
+ [Dependency] private readonly IResourceCache _resourceCache = default!;
+ [Dependency] private readonly IEntityManager _entManager = default!;
+ [Dependency] private readonly IMapManager _mapManager = default!;
+
+ public override OverlaySpace Space => OverlaySpace.WorldSpaceEntities | OverlaySpace.WorldSpaceBelowWorld;
+ private static readonly ProtoId UnshadedShader = "unshaded";
+
+ private readonly SharedTransformSystem _xformSys;
+ private readonly SharedMapSystem _mapSystem = default!;
+ private readonly ShaderInstance _shader;
+
+ private readonly float[] _timer;
+ private readonly float[][] _frameDelays;
+ private readonly int[] _frameCounter;
+
+ // TODO combine textures into a single texture atlas.
+ private readonly Texture[][] _frames;
+
+ private const int FireStates = 3;
+ private const string FireRsiPath = "/Textures/Effects/fire.rsi";
+
+ public const int GasOverlayZIndex = (int)Shared.DrawDepth.DrawDepth.Effects; // Under ghosts, above mostly everything else
+
+ public GasTileFireOverlay()
+ {
+ IoCManager.InjectDependencies(this);
+ _xformSys = _entManager.System();
+ _mapSystem = _entManager.System();
+ _shader = _protoMan.Index(UnshadedShader).Instance();
+ ZIndex = GasOverlayZIndex;
+
+ _timer = new float[FireStates];
+ _frameDelays = new float[FireStates][];
+ _frameCounter = new int[FireStates];
+ _frames = new Texture[FireStates][];
+
+ var fire = _resourceCache.GetResource(FireRsiPath).RSI;
+
+ for (var i = 0; i < FireStates; i++)
+ {
+ if (!fire.TryGetState((i + 1).ToString(), out var state))
+ throw new ArgumentOutOfRangeException($"Fire RSI doesn't have state \"{i}\"!");
+
+ _frames[i] = state.GetFrames(RsiDirection.South);
+ _frameDelays[i] = state.GetDelays();
+ _frameCounter[i] = 0;
+ }
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+
+ for (var i = 0; i < FireStates; i++)
+ {
+ var delays = _frameDelays[i];
+ if (delays.Length == 0)
+ continue;
+
+ var frameCount = _frameCounter[i];
+ _timer[i] += args.DeltaSeconds;
+ var time = delays[frameCount];
+
+ if (_timer[i] < time) continue;
+ _timer[i] -= time;
+ _frameCounter[i] = (frameCount + 1) % _frames[i].Length;
+ }
+ }
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ if (args.MapId == MapId.Nullspace)
+ return;
+
+ var drawHandle = args.WorldHandle;
+ var xformQuery = _entManager.GetEntityQuery();
+ var overlayQuery = _entManager.GetEntityQuery();
+ var gridState = (args.WorldBounds,
+ args.WorldHandle,
+ _frames,
+ _frameCounter,
+ _shader,
+ overlayQuery,
+ xformQuery,
+ _xformSys);
+
+ var mapUid = _mapSystem.GetMapOrInvalid(args.MapId);
+
+ if (args.Space != OverlaySpace.WorldSpaceEntities)
+ return;
+
+ // TODO: WorldBounds callback.
+ _mapManager.FindGridsIntersecting(args.MapId, args.WorldAABB, ref gridState,
+ static (EntityUid uid, MapGridComponent grid,
+ ref (Box2Rotated WorldBounds,
+ DrawingHandleWorld drawHandle,
+ Texture[][] frames,
+ int[] frameCounter,
+ ShaderInstance shader,
+ EntityQuery overlayQuery,
+ EntityQuery xformQuery,
+ SharedTransformSystem xformSys) state) =>
+ {
+ if (!state.overlayQuery.TryGetComponent(uid, out var comp) ||
+ !state.xformQuery.TryGetComponent(uid, out var gridXform))
+ {
+ return true;
+ }
+
+ var (_, _, worldMatrix, invMatrix) = state.xformSys.GetWorldPositionRotationMatrixWithInv(gridXform);
+ state.drawHandle.SetTransform(worldMatrix);
+ var floatBounds = invMatrix.TransformBox(state.WorldBounds).Enlarged(grid.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));
+
+ // Currently it would be faster to group drawing by gas rather than by chunk, but if the textures are
+ // ever moved to a single atlas, that should no longer be the case. So this is just grouping draw calls
+ // by chunk, even though its currently slower.
+
+ state.drawHandle.UseShader(state.shader);
+ foreach (var chunk in comp.Chunks.Values)
+ {
+ var enumerator = new GasChunkEnumerator(chunk);
+
+ while (enumerator.MoveNext(out var gas))
+ {
+ if (gas.FireState == 0)
+ continue;
+
+ var index = chunk.Origin + (enumerator.X, enumerator.Y);
+ if (!localBounds.Contains(index))
+ continue;
+
+ var fireState = gas.FireState - 1;
+ var texture = state.frames[fireState][state.frameCounter[fireState]];
+ state.drawHandle.DrawTexture(texture, index);
+ }
+ }
+
+ return true;
+ });
+
+ drawHandle.UseShader(null);
+ drawHandle.SetTransform(Matrix3x2.Identity);
+ }
+}
diff --git a/Content.Client/Atmos/Overlays/GasTileOverlay.cs b/Content.Client/Atmos/Overlays/GasTileOverlay.cs
deleted file mode 100644
index eeb10b54d0..0000000000
--- a/Content.Client/Atmos/Overlays/GasTileOverlay.cs
+++ /dev/null
@@ -1,302 +0,0 @@
-using System.Numerics;
-using Content.Client.Atmos.Components;
-using Content.Client.Atmos.EntitySystems;
-using Content.Shared.Atmos;
-using Content.Shared.Atmos.Components;
-using Content.Shared.Atmos.EntitySystems;
-using Content.Shared.Atmos.Prototypes;
-using Robust.Client.GameObjects;
-using Robust.Client.Graphics;
-using Robust.Client.ResourceManagement;
-using Robust.Shared.Enums;
-using Robust.Shared.Graphics.RSI;
-using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Timing;
-using Robust.Shared.Utility;
-
-namespace Content.Client.Atmos.Overlays
-{
- public sealed class GasTileOverlay : Overlay
- {
- private static readonly ProtoId UnshadedShader = "unshaded";
-
- private readonly IEntityManager _entManager;
- private readonly IMapManager _mapManager;
- private readonly SharedAtmosphereSystem _atmosphereSystem;
- private readonly SharedMapSystem _mapSystem;
- private readonly SharedTransformSystem _xformSys;
-
- public override OverlaySpace Space => OverlaySpace.WorldSpaceEntities | OverlaySpace.WorldSpaceBelowWorld;
- private readonly ShaderInstance _shader;
-
- // Gas overlays
- private readonly float[] _timer;
- private readonly float[][] _frameDelays;
- private readonly int[] _frameCounter;
-
- // TODO combine textures into a single texture atlas.
- private readonly Texture[][] _frames;
-
- // Fire overlays
- private const int FireStates = 3;
- private const string FireRsiPath = "/Textures/Effects/fire.rsi";
-
- private readonly float[] _fireTimer = new float[FireStates];
- private readonly float[][] _fireFrameDelays = new float[FireStates][];
- private readonly int[] _fireFrameCounter = new int[FireStates];
- private readonly Texture[][] _fireFrames = new Texture[FireStates][];
-
- private int _gasCount;
-
- public const int GasOverlayZIndex = (int) Shared.DrawDepth.DrawDepth.Effects; // Under ghosts, above mostly everything else
-
- public GasTileOverlay(GasTileOverlaySystem system, IEntityManager entManager, IResourceCache resourceCache, IPrototypeManager protoMan, SpriteSystem spriteSys, SharedTransformSystem xformSys)
- {
- _entManager = entManager;
- _mapManager = IoCManager.Resolve();
- _atmosphereSystem = entManager.System();
- _mapSystem = entManager.System();
- _xformSys = xformSys;
- _shader = protoMan.Index(UnshadedShader).Instance();
- ZIndex = GasOverlayZIndex;
-
- _gasCount = system.VisibleGasId.Length;
- _timer = new float[_gasCount];
- _frameDelays = new float[_gasCount][];
- _frameCounter = new int[_gasCount];
- _frames = new Texture[_gasCount][];
-
- for (var i = 0; i < _gasCount; i++)
- {
- var gasPrototype = _atmosphereSystem.GetGas(system.VisibleGasId[i]);
-
- SpriteSpecifier overlay;
-
- if (!string.IsNullOrEmpty(gasPrototype.GasOverlaySprite) && !string.IsNullOrEmpty(gasPrototype.GasOverlayState))
- overlay = new SpriteSpecifier.Rsi(new (gasPrototype.GasOverlaySprite), gasPrototype.GasOverlayState);
- else if (!string.IsNullOrEmpty(gasPrototype.GasOverlayTexture))
- overlay = new SpriteSpecifier.Texture(new (gasPrototype.GasOverlayTexture));
- else
- continue;
-
- switch (overlay)
- {
- case SpriteSpecifier.Rsi animated:
- var rsi = resourceCache.GetResource(animated.RsiPath).RSI;
- var stateId = animated.RsiState;
-
- if (!rsi.TryGetState(stateId, out var state))
- continue;
-
- _frames[i] = state.GetFrames(RsiDirection.South);
- _frameDelays[i] = state.GetDelays();
- _frameCounter[i] = 0;
- break;
- case SpriteSpecifier.Texture texture:
- _frames[i] = new[] { spriteSys.Frame0(texture) };
- _frameDelays[i] = Array.Empty();
- break;
- }
- }
-
- var fire = resourceCache.GetResource(FireRsiPath).RSI;
-
- for (var i = 0; i < FireStates; i++)
- {
- if (!fire.TryGetState((i + 1).ToString(), out var state))
- throw new ArgumentOutOfRangeException($"Fire RSI doesn't have state \"{i}\"!");
-
- _fireFrames[i] = state.GetFrames(RsiDirection.South);
- _fireFrameDelays[i] = state.GetDelays();
- _fireFrameCounter[i] = 0;
- }
- }
- protected override void FrameUpdate(FrameEventArgs args)
- {
- base.FrameUpdate(args);
-
- for (var i = 0; i < _gasCount; i++)
- {
- var delays = _frameDelays[i];
- if (delays.Length == 0)
- continue;
-
- var frameCount = _frameCounter[i];
- _timer[i] += args.DeltaSeconds;
- var time = delays[frameCount];
-
- if (_timer[i] < time)
- continue;
-
- _timer[i] -= time;
- _frameCounter[i] = (frameCount + 1) % _frames[i].Length;
- }
-
- for (var i = 0; i < FireStates; i++)
- {
- var delays = _fireFrameDelays[i];
- if (delays.Length == 0)
- continue;
-
- var frameCount = _fireFrameCounter[i];
- _fireTimer[i] += args.DeltaSeconds;
- var time = delays[frameCount];
-
- if (_fireTimer[i] < time) continue;
- _fireTimer[i] -= time;
- _fireFrameCounter[i] = (frameCount + 1) % _fireFrames[i].Length;
- }
- }
-
- protected override void Draw(in OverlayDrawArgs args)
- {
- if (args.MapId == MapId.Nullspace)
- return;
-
- var drawHandle = args.WorldHandle;
- var xformQuery = _entManager.GetEntityQuery();
- var overlayQuery = _entManager.GetEntityQuery();
- var gridState = (args.WorldBounds,
- args.WorldHandle,
- _gasCount,
- _frames,
- _frameCounter,
- _fireFrames,
- _fireFrameCounter,
- _shader,
- overlayQuery,
- xformQuery,
- _xformSys);
-
- var mapUid = _mapSystem.GetMapOrInvalid(args.MapId);
-
- if (_entManager.TryGetComponent(mapUid, out var atmos))
- DrawMapOverlay(drawHandle, args, mapUid, atmos);
-
- if (args.Space != OverlaySpace.WorldSpaceEntities)
- return;
-
- // TODO: WorldBounds callback.
- _mapManager.FindGridsIntersecting(args.MapId, args.WorldAABB, ref gridState,
- static (EntityUid uid, MapGridComponent grid,
- ref (Box2Rotated WorldBounds,
- DrawingHandleWorld drawHandle,
- int gasCount,
- Texture[][] frames,
- int[] frameCounter,
- Texture[][] fireFrames,
- int[] fireFrameCounter,
- ShaderInstance shader,
- EntityQuery overlayQuery,
- EntityQuery xformQuery,
- SharedTransformSystem xformSys) state) =>
- {
- if (!state.overlayQuery.TryGetComponent(uid, out var comp) ||
- !state.xformQuery.TryGetComponent(uid, out var gridXform))
- {
- return true;
- }
-
- var (_, _, worldMatrix, invMatrix) = state.xformSys.GetWorldPositionRotationMatrixWithInv(gridXform);
- state.drawHandle.SetTransform(worldMatrix);
- var floatBounds = invMatrix.TransformBox(state.WorldBounds).Enlarged(grid.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));
-
- // Currently it would be faster to group drawing by gas rather than by chunk, but if the textures are
- // ever moved to a single atlas, that should no longer be the case. So this is just grouping draw calls
- // by chunk, even though its currently slower.
-
- state.drawHandle.UseShader(null);
- foreach (var chunk in comp.Chunks.Values)
- {
- var enumerator = new GasChunkEnumerator(chunk);
-
- while (enumerator.MoveNext(out var gas))
- {
- if (gas.Opacity == null!)
- continue;
-
- var tilePosition = chunk.Origin + (enumerator.X, enumerator.Y);
- if (!localBounds.Contains(tilePosition))
- continue;
-
- for (var i = 0; i < state.gasCount; i++)
- {
- var opacity = gas.Opacity[i];
- if (opacity > 0)
- state.drawHandle.DrawTexture(state.frames[i][state.frameCounter[i]], tilePosition, Color.White.WithAlpha(opacity));
- }
- }
- }
-
- // And again for fire, with the unshaded shader
- state.drawHandle.UseShader(state.shader);
- foreach (var chunk in comp.Chunks.Values)
- {
- var enumerator = new GasChunkEnumerator(chunk);
-
- while (enumerator.MoveNext(out var gas))
- {
- if (gas.FireState == 0)
- continue;
-
- var index = chunk.Origin + (enumerator.X, enumerator.Y);
- if (!localBounds.Contains(index))
- continue;
-
- var fireState = gas.FireState - 1;
- var texture = state.fireFrames[fireState][state.fireFrameCounter[fireState]];
- state.drawHandle.DrawTexture(texture, index);
- }
- }
-
- return true;
- });
-
- drawHandle.UseShader(null);
- drawHandle.SetTransform(Matrix3x2.Identity);
- }
-
- private void DrawMapOverlay(
- DrawingHandleWorld handle,
- OverlayDrawArgs args,
- EntityUid map,
- MapAtmosphereComponent atmos)
- {
- var mapGrid = _entManager.HasComponent(map);
-
- // map-grid atmospheres get drawn above grids
- if (mapGrid && args.Space != OverlaySpace.WorldSpaceEntities)
- return;
-
- // Normal map atmospheres get drawn below grids
- if (!mapGrid && args.Space != OverlaySpace.WorldSpaceBelowWorld)
- return;
-
- var bottomLeft = args.WorldAABB.BottomLeft.Floored();
- var topRight = args.WorldAABB.TopRight.Ceiled();
-
- for (var x = bottomLeft.X; x <= topRight.X; x++)
- {
- for (var y = bottomLeft.Y; y <= topRight.Y; y++)
- {
- var tilePosition = new Vector2(x, y);
-
- for (var i = 0; i < atmos.OverlayData.Opacity.Length; i++)
- {
- var opacity = atmos.OverlayData.Opacity[i];
-
- if (opacity > 0)
- handle.DrawTexture(_frames[i][_frameCounter[i]], tilePosition, Color.White.WithAlpha(opacity));
- }
- }
- }
- }
- }
-}
diff --git a/Content.Client/Atmos/Overlays/GasTileVisibleGasOverlay.cs b/Content.Client/Atmos/Overlays/GasTileVisibleGasOverlay.cs
new file mode 100644
index 0000000000..37298b95fd
--- /dev/null
+++ b/Content.Client/Atmos/Overlays/GasTileVisibleGasOverlay.cs
@@ -0,0 +1,258 @@
+using Content.Client.Atmos.Components;
+using Content.Shared.Atmos;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.EntitySystems;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Shared.Enums;
+using Robust.Shared.Graphics.RSI;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+using System.Numerics;
+using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
+
+namespace Content.Client.Atmos.Overlays;
+
+///
+/// Overlay responsible for rendering visible atmos gasses (like plasma for example) usin.
+///
+public sealed class GasTileVisibleGasOverlay : Overlay
+{
+ [Dependency] private readonly IEntityManager _entManager = default!;
+ [Dependency] private readonly IResourceCache _resourceCache = default!;
+ [Dependency] private readonly IPrototypeManager _protoManager = default!;
+ [Dependency] private readonly IMapManager _mapManager = default!;
+
+ private static readonly ProtoId UnshadedShader = "unshaded";
+
+ private readonly SharedAtmosphereSystem _atmosphereSystem;
+ private readonly SharedMapSystem _mapSystem;
+ private readonly SharedTransformSystem _xformSys;
+ private readonly SharedGasTileOverlaySystem _gasTileOverlaySystem;
+ private readonly SpriteSystem _spriteSystem;
+
+ public override OverlaySpace Space => OverlaySpace.WorldSpaceEntities | OverlaySpace.WorldSpaceBelowWorld;
+ private readonly ShaderInstance _shader;
+
+ // Gas overlays
+ private readonly float[] _timer;
+ private readonly float[][] _frameDelays;
+ private readonly int[] _frameCounter;
+
+ // TODO combine textures into a single texture atlas.
+ private readonly Texture[][] _frames;
+
+ private readonly int _gasCount;
+
+ public const int GasOverlayZIndex = (int)DrawDepth.Gasses; // Under ghosts and fire, above mostly everything else
+
+ public GasTileVisibleGasOverlay()
+ {
+ IoCManager.InjectDependencies(this);
+ _atmosphereSystem = _entManager.System();
+ _mapSystem = _entManager.System();
+ _xformSys = _entManager.System();
+ _gasTileOverlaySystem = _entManager.System();
+ _spriteSystem = _entManager.System();
+
+ _shader = _protoManager.Index(UnshadedShader).Instance();
+ ZIndex = GasOverlayZIndex;
+
+ _gasCount = _gasTileOverlaySystem.VisibleGasId.Length;
+ _timer = new float[_gasCount];
+ _frameDelays = new float[_gasCount][];
+ _frameCounter = new int[_gasCount];
+ _frames = new Texture[_gasCount][];
+
+ for (var i = 0; i < _gasCount; i++)
+ {
+ var gasPrototype = _atmosphereSystem.GetGas(_gasTileOverlaySystem.VisibleGasId[i]);
+
+ SpriteSpecifier overlay;
+
+ if (!string.IsNullOrEmpty(gasPrototype.GasOverlaySprite) &&
+ !string.IsNullOrEmpty(gasPrototype.GasOverlayState))
+ overlay = new SpriteSpecifier.Rsi(new(gasPrototype.GasOverlaySprite), gasPrototype.GasOverlayState);
+ else if (!string.IsNullOrEmpty(gasPrototype.GasOverlayTexture))
+ overlay = new SpriteSpecifier.Texture(new(gasPrototype.GasOverlayTexture));
+ else
+ continue;
+
+ switch (overlay)
+ {
+ case SpriteSpecifier.Rsi animated:
+ var rsi = _resourceCache.GetResource(animated.RsiPath).RSI;
+ var stateId = animated.RsiState;
+
+ if (!rsi.TryGetState(stateId, out var state))
+ continue;
+
+ _frames[i] = state.GetFrames(RsiDirection.South);
+ _frameDelays[i] = state.GetDelays();
+ _frameCounter[i] = 0;
+ break;
+ case SpriteSpecifier.Texture texture:
+ _frames[i] = new[] { _spriteSystem.Frame0(texture) };
+ _frameDelays[i] = Array.Empty();
+ break;
+ }
+ }
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+
+ for (var i = 0; i < _gasCount; i++)
+ {
+ var delays = _frameDelays[i];
+ if (delays.Length == 0)
+ continue;
+
+ var frameCount = _frameCounter[i];
+ _timer[i] += args.DeltaSeconds;
+ var time = delays[frameCount];
+
+ if (_timer[i] < time)
+ continue;
+
+ _timer[i] -= time;
+ _frameCounter[i] = (frameCount + 1) % _frames[i].Length;
+ }
+ }
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ if (args.MapId == MapId.Nullspace)
+ return;
+
+ var drawHandle = args.WorldHandle;
+ var xformQuery = _entManager.GetEntityQuery();
+ var overlayQuery = _entManager.GetEntityQuery();
+ var gridState = (args.WorldBounds,
+ args.WorldHandle,
+ _gasCount,
+ _frames,
+ _frameCounter,
+ _shader,
+ overlayQuery,
+ xformQuery,
+ _xformSys);
+
+ var mapUid = _mapSystem.GetMapOrInvalid(args.MapId);
+
+ if (_entManager.TryGetComponent(mapUid, out var atmos))
+ DrawMapOverlay(drawHandle, args, mapUid, atmos);
+
+ if (args.Space != OverlaySpace.WorldSpaceEntities)
+ return;
+
+ // TODO: WorldBounds callback.
+ _mapManager.FindGridsIntersecting(args.MapId,
+ args.WorldAABB,
+ ref gridState,
+ static (EntityUid uid,
+ MapGridComponent grid,
+ ref (Box2Rotated WorldBounds,
+ DrawingHandleWorld drawHandle,
+ int gasCount,
+ Texture[][] frames,
+ int[] frameCounter,
+ ShaderInstance shader,
+ EntityQuery overlayQuery,
+ EntityQuery xformQuery,
+ SharedTransformSystem xformSys) state) =>
+ {
+ if (!state.overlayQuery.TryGetComponent(uid, out var comp) ||
+ !state.xformQuery.TryGetComponent(uid, out var gridXform))
+ {
+ return true;
+ }
+
+ var (_, _, worldMatrix, invMatrix) = state.xformSys.GetWorldPositionRotationMatrixWithInv(gridXform);
+ state.drawHandle.SetTransform(worldMatrix);
+ var floatBounds = invMatrix.TransformBox(state.WorldBounds).Enlarged(grid.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));
+
+ // Currently it would be faster to group drawing by gas rather than by chunk, but if the textures are
+ // ever moved to a single atlas, that should no longer be the case. So this is just grouping draw calls
+ // by chunk, even though its currently slower.
+
+ state.drawHandle.UseShader(null);
+ foreach (var chunk in comp.Chunks.Values)
+ {
+ var enumerator = new GasChunkEnumerator(chunk);
+
+ while (enumerator.MoveNext(out var gas))
+ {
+ if (gas.Opacity == null!)
+ continue;
+
+ var tilePosition = chunk.Origin + (enumerator.X, enumerator.Y);
+ if (!localBounds.Contains(tilePosition))
+ continue;
+
+ for (var i = 0; i < state.gasCount; i++)
+ {
+ var opacity = gas.Opacity[i];
+ if (opacity > 0)
+ {
+ state.drawHandle.DrawTexture(state.frames[i][state.frameCounter[i]],
+ tilePosition,
+ Color.White.WithAlpha(opacity));
+ }
+ }
+ }
+ }
+
+ return true;
+ });
+
+ drawHandle.UseShader(null);
+ drawHandle.SetTransform(Matrix3x2.Identity);
+ }
+
+ private void DrawMapOverlay(
+ DrawingHandleWorld handle,
+ OverlayDrawArgs args,
+ EntityUid map,
+ MapAtmosphereComponent atmos)
+ {
+ var mapGrid = _entManager.HasComponent(map);
+
+ // map-grid atmospheres get drawn above grids
+ if (mapGrid && args.Space != OverlaySpace.WorldSpaceEntities)
+ return;
+
+ // Normal map atmospheres get drawn below grids
+ if (!mapGrid && args.Space != OverlaySpace.WorldSpaceBelowWorld)
+ return;
+
+ var bottomLeft = args.WorldAABB.BottomLeft.Floored();
+ var topRight = args.WorldAABB.TopRight.Ceiled();
+
+ for (var x = bottomLeft.X; x <= topRight.X; x++)
+ {
+ for (var y = bottomLeft.Y; y <= topRight.Y; y++)
+ {
+ var tilePosition = new Vector2(x, y);
+
+ for (var i = 0; i < atmos.OverlayData.Opacity.Length; i++)
+ {
+ var opacity = atmos.OverlayData.Opacity[i];
+
+ if (opacity > 0)
+ handle.DrawTexture(_frames[i][_frameCounter[i]], tilePosition, Color.White.WithAlpha(opacity));
+ }
+ }
+ }
+ }
+}
diff --git a/Content.Shared/DrawDepth/DrawDepth.cs b/Content.Shared/DrawDepth/DrawDepth.cs
index 35fead5b3f..288602353c 100644
--- a/Content.Shared/DrawDepth/DrawDepth.cs
+++ b/Content.Shared/DrawDepth/DrawDepth.cs
@@ -112,17 +112,22 @@ namespace Content.Shared.DrawDepth
///
Overdoors = DrawDepthTag.Default + 10,
+ ///
+ /// Visible atmos gas.
+ ///
+ Gasses = DrawDepthTag.Default + 11,
+
///
/// Explosions, fire, melee swings. Whatever.
///
- Effects = DrawDepthTag.Default + 11,
+ Effects = DrawDepthTag.Default + 12,
- Ghosts = DrawDepthTag.Default + 12,
+ Ghosts = DrawDepthTag.Default + 13,
///
/// Use this selectively if it absolutely needs to be drawn above (almost) everything else. Examples include
/// the pointing arrow, the drag & drop ghost-entity, and some debug tools.
///
- Overlays = DrawDepthTag.Default + 13,
+ Overlays = DrawDepthTag.Default + 14,
}
}