From d5ed6aa462549ceaaf154707f112656592bf95dd Mon Sep 17 00:00:00 2001 From: InsoPL Date: Sun, 15 Mar 2026 08:20:06 +0100 Subject: [PATCH] Refactor: Gas tile overlay split (#42881) * yeah * split2 * well * cleanup * docs fix * Dependency cleanup * Update Content.Client/Atmos/EntitySystems/GasTileFireOverlaySystem.cs Co-authored-by: Pok <113675512+Pok27@users.noreply.github.com> * Update Content.Client/Atmos/Overlays/GasTileVisibleGasOverlay.cs Co-authored-by: Pok <113675512+Pok27@users.noreply.github.com> * Update Content.Client/Atmos/Overlays/GasTileVisibleGasOverlay.cs Co-authored-by: Pok <113675512+Pok27@users.noreply.github.com> * yup * modifying a lot anyways * other cleanup --------- Co-authored-by: Pok <113675512+Pok27@users.noreply.github.com> Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> --- .../EntitySystems/GasTileFireOverlaySystem.cs | 30 ++ .../EntitySystems/GasTileOverlaySystem.cs | 141 ++++---- .../GasTileVisibleGasOverlaySystem.cs | 31 ++ .../Atmos/Overlays/GasTileFireOverlay.cs | 172 ++++++++++ .../Atmos/Overlays/GasTileOverlay.cs | 302 ------------------ .../Overlays/GasTileVisibleGasOverlay.cs | 258 +++++++++++++++ Content.Shared/DrawDepth/DrawDepth.cs | 11 +- 7 files changed, 559 insertions(+), 386 deletions(-) create mode 100644 Content.Client/Atmos/EntitySystems/GasTileFireOverlaySystem.cs create mode 100644 Content.Client/Atmos/EntitySystems/GasTileVisibleGasOverlaySystem.cs create mode 100644 Content.Client/Atmos/Overlays/GasTileFireOverlay.cs delete mode 100644 Content.Client/Atmos/Overlays/GasTileOverlay.cs create mode 100644 Content.Client/Atmos/Overlays/GasTileVisibleGasOverlay.cs 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, } }