using Content.Client.Atmos.Overlays; using Content.Shared.Atmos; using Content.Shared.Atmos.EntitySystems; using JetBrains.Annotations; using Robust.Client.Graphics; using Robust.Client.ResourceManagement; using Robust.Client.Utility; using Robust.Shared.Utility; namespace Content.Client.Atmos.EntitySystems { [UsedImplicitly] internal sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem { [Dependency] private readonly IResourceCache _resourceCache = default!; [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; // Gas overlays public readonly float[] Timer = new float[Atmospherics.TotalNumberOfGases]; public readonly float[][] FrameDelays = new float[Atmospherics.TotalNumberOfGases][]; public readonly int[] FrameCounter = new int[Atmospherics.TotalNumberOfGases]; public readonly Texture[][] Frames = new Texture[Atmospherics.TotalNumberOfGases][]; // Fire overlays public const int FireStates = 3; public const string FireRsiPath = "/Textures/Effects/fire.rsi"; public readonly float[] FireTimer = new float[FireStates]; public readonly float[][] FireFrameDelays = new float[FireStates][]; public readonly int[] FireFrameCounter = new int[FireStates]; public readonly Texture[][] FireFrames = new Texture[FireStates][]; private readonly Dictionary> _tileData = new(); public const int GasOverlayZIndex = 1; public override void Initialize() { base.Initialize(); SubscribeNetworkEvent(HandleGasOverlayMessage); SubscribeLocalEvent(OnGridRemoved); for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++) { var overlay = _atmosphereSystem.GetOverlay(i); 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(RSI.State.Direction.South); FrameDelays[i] = state.GetDelays(); FrameCounter[i] = 0; break; case SpriteSpecifier.Texture texture: Frames[i] = new[] {texture.Frame0()}; FrameDelays[i] = Array.Empty(); break; case null: Frames[i] = Array.Empty(); 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(RSI.State.Direction.South); FireFrameDelays[i] = state.GetDelays(); FireFrameCounter[i] = 0; } var overlayManager = IoCManager.Resolve(); overlayManager.AddOverlay(new GasTileOverlay()); overlayManager.AddOverlay(new FireTileOverlay()); } private void HandleGasOverlayMessage(GasOverlayMessage message) { foreach (var (indices, data) in message.OverlayData) { var chunk = GetOrCreateChunk(message.GridId, indices); chunk.Update(data, indices); } } // Slightly different to the server-side system version private GasOverlayChunk GetOrCreateChunk(EntityUid gridId, Vector2i indices) { if (!_tileData.TryGetValue(gridId, out var chunks)) { chunks = new Dictionary(); _tileData[gridId] = chunks; } var chunkIndices = GetGasChunkIndices(indices); if (!chunks.TryGetValue(chunkIndices, out var chunk)) { chunk = new GasOverlayChunk(gridId, chunkIndices); chunks[chunkIndices] = chunk; } return chunk; } public override void Shutdown() { base.Shutdown(); var overlayManager = IoCManager.Resolve(); overlayManager.RemoveOverlay(); overlayManager.RemoveOverlay(); } private void OnGridRemoved(GridRemovalEvent ev) { if (_tileData.ContainsKey(ev.GridId)) { _tileData.Remove(ev.GridId); } } public bool HasData(EntityUid gridId) { return _tileData.ContainsKey(gridId); } public GasOverlayEnumerator GetOverlays(EntityUid gridIndex, Vector2i indices) { if (!_tileData.TryGetValue(gridIndex, out var chunks)) return default; var chunkIndex = GetGasChunkIndices(indices); if (!chunks.TryGetValue(chunkIndex, out var chunk)) return default; var overlays = chunk.GetData(indices); return new GasOverlayEnumerator(overlays, this); } public FireOverlayEnumerator GetFireOverlays(EntityUid gridIndex, Vector2i indices) { if (!_tileData.TryGetValue(gridIndex, out var chunks)) return default; var chunkIndex = GetGasChunkIndices(indices); if (!chunks.TryGetValue(chunkIndex, out var chunk)) return default; var overlays = chunk.GetData(indices); return new FireOverlayEnumerator(overlays, this); } public override void FrameUpdate(float frameTime) { base.FrameUpdate(frameTime); for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++) { var delays = FrameDelays[i]; if (delays.Length == 0) continue; var frameCount = FrameCounter[i]; Timer[i] += frameTime; 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] += frameTime; var time = delays[frameCount]; if (FireTimer[i] < time) continue; FireTimer[i] -= time; FireFrameCounter[i] = (frameCount + 1) % FireFrames[i].Length; } } public struct GasOverlayEnumerator { private readonly GasTileOverlaySystem _system; private readonly GasData[]? _data; // TODO: Take Fire Temperature into account, when we code fire color private readonly int _length; // We cache the length so we can avoid a pointer dereference, for speed. Brrr. private int _current; public GasOverlayEnumerator(in GasOverlayData data, GasTileOverlaySystem system) { // Gas can't be null, as the caller to this constructor already ensured it wasn't. _data = data.Gas; _system = system; _length = _data?.Length ?? 0; _current = 0; } public bool MoveNext(out (Texture Texture, Color Color) overlay) { if (_current < _length) { // Data can't be null here unless length/current are incorrect var gas = _data![_current++]; var frames = _system.Frames[gas.Index]; overlay = (frames[_system.FrameCounter[gas.Index]], Color.White.WithAlpha(gas.Opacity)); return true; } overlay = default; return false; } } public struct FireOverlayEnumerator { private readonly GasTileOverlaySystem _system; private byte _fireState; // TODO: Take Fire Temperature into account, when we code fire color public FireOverlayEnumerator(in GasOverlayData data, GasTileOverlaySystem system) { _fireState = data.FireState; _system = system; } public bool MoveNext(out (Texture Texture, Color Color) overlay) { if (_fireState != 0) { var state = _fireState - 1; var frames = _system.FireFrames[state]; // TODO ATMOS Set color depending on temperature overlay = (frames[_system.FireFrameCounter[state]], Color.White); // Setting this to zero so we don't get stuck in an infinite loop. _fireState = 0; return true; } overlay = default; return false; } } } }