diff --git a/Content.Client/Atmos/GasTileOverlay.cs b/Content.Client/Atmos/GasTileOverlay.cs index 8f852d0ed1..16a55249e4 100644 --- a/Content.Client/Atmos/GasTileOverlay.cs +++ b/Content.Client/Atmos/GasTileOverlay.cs @@ -40,7 +40,12 @@ namespace Content.Client.Atmos foreach (var mapGrid in _mapManager.FindGridsIntersecting(mapId, worldBounds)) { - foreach (var tile in mapGrid.GetTilesIntersecting(worldBounds)) + if (!_gasTileOverlaySystem.HasData(mapGrid.Index)) + continue; + + var gridBounds = new Box2(mapGrid.WorldToLocal(worldBounds.BottomLeft), mapGrid.WorldToLocal(worldBounds.TopRight)); + + foreach (var tile in mapGrid.GetTilesIntersecting(gridBounds)) { foreach (var (texture, color) in _gasTileOverlaySystem.GetOverlays(mapGrid.Index, tile.GridIndices)) { diff --git a/Content.Client/GameObjects/Components/Atmos/CanSeeGasesComponent.cs b/Content.Client/GameObjects/Components/Atmos/CanSeeGasesComponent.cs deleted file mode 100644 index d95748f72b..0000000000 --- a/Content.Client/GameObjects/Components/Atmos/CanSeeGasesComponent.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Content.Client.Atmos; -using Robust.Client.GameObjects; -using Robust.Client.Interfaces.Graphics.Overlays; -using Robust.Shared.GameObjects; -using Robust.Shared.Interfaces.GameObjects; -using Robust.Shared.IoC; - -namespace Content.Client.GameObjects.Components.Atmos -{ - [RegisterComponent] - public class CanSeeGasesComponent : Component - { - [Dependency] private readonly IOverlayManager _overlayManager = default!; - - public override string Name => "CanSeeGases"; - - public override void HandleMessage(ComponentMessage message, IComponent component) - { - base.HandleMessage(message, component); - - switch (message) - { - case PlayerAttachedMsg _: - if(!_overlayManager.HasOverlay(nameof(GasTileOverlay))) - _overlayManager.AddOverlay(new GasTileOverlay()); - break; - - case PlayerDetachedMsg _: - if(!_overlayManager.HasOverlay(nameof(GasTileOverlay))) - _overlayManager.RemoveOverlay(nameof(GasTileOverlay)); - break; - } - } - } -} diff --git a/Content.Client/GameObjects/EntitySystems/GasTileOverlaySystem.cs b/Content.Client/GameObjects/EntitySystems/GasTileOverlaySystem.cs index a3eeadd0d3..253563f527 100644 --- a/Content.Client/GameObjects/EntitySystems/GasTileOverlaySystem.cs +++ b/Content.Client/GameObjects/EntitySystems/GasTileOverlaySystem.cs @@ -1,13 +1,19 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; +using Content.Client.Atmos; +using Content.Client.GameObjects.Components.Atmos; using Content.Shared.Atmos; -using Content.Shared.GameObjects.EntitySystems; +using Content.Shared.GameObjects.EntitySystems.Atmos; using JetBrains.Annotations; +using Robust.Client.GameObjects; +using Robust.Client.GameObjects.EntitySystems; using Robust.Client.Graphics; +using Robust.Client.Interfaces.Graphics.Overlays; using Robust.Client.Interfaces.ResourceManagement; using Robust.Client.ResourceManagement; using Robust.Client.Utility; -using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.Map; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; @@ -16,8 +22,9 @@ using Robust.Shared.Utility; namespace Content.Client.GameObjects.EntitySystems { [UsedImplicitly] - public sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem + internal sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem { + [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IResourceCache _resourceCache = default!; private readonly Dictionary _fireCache = new Dictionary(); @@ -36,19 +43,20 @@ namespace Content.Client.GameObjects.EntitySystems private readonly float[][] _fireFrameDelays = new float[FireStates][]; private readonly int[] _fireFrameCounter = new int[FireStates]; private readonly Texture[][] _fireFrames = new Texture[FireStates][]; - - private Dictionary> _overlay = new Dictionary>(); + + private Dictionary> _tileData = + new Dictionary>(); public override void Initialize() { base.Initialize(); - - SubscribeNetworkEvent(new EntityEventHandler(OnTileOverlayMessage)); + SubscribeNetworkEvent(HandleGasOverlayMessage); + _mapManager.OnGridRemoved += OnGridRemoved; for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++) { - var gas = Atmospherics.GetGas(i); - switch (gas.GasOverlay) + var overlay = Atmospherics.GetOverlay(i); + switch (overlay) { case SpriteSpecifier.Rsi animated: var rsi = _resourceCache.GetResource(animated.RsiPath).RSI; @@ -82,13 +90,77 @@ namespace Content.Client.GameObjects.EntitySystems _fireFrameDelays[i] = state.GetDelays(); _fireFrameCounter[i] = 0; } + + var overlayManager = IoCManager.Resolve(); + if(!overlayManager.HasOverlay(nameof(GasTileOverlay))) + overlayManager.AddOverlay(new GasTileOverlay()); + } + + 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(GridId gridId, MapIndices 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(); + _mapManager.OnGridRemoved -= OnGridRemoved; + var overlayManager = IoCManager.Resolve(); + if(!overlayManager.HasOverlay(nameof(GasTileOverlay))) + overlayManager.RemoveOverlay(nameof(GasTileOverlay)); + } + + private void OnGridRemoved(GridId gridId) + { + if (_tileData.ContainsKey(gridId)) + { + _tileData.Remove(gridId); + } + } + + public bool HasData(GridId gridId) + { + return _tileData.ContainsKey(gridId); } public (Texture, Color color)[] GetOverlays(GridId gridIndex, MapIndices indices) { - if (!_overlay.TryGetValue(gridIndex, out var tiles) || !tiles.TryGetValue(indices, out var overlays)) + if (!_tileData.TryGetValue(gridIndex, out var chunks)) return Array.Empty<(Texture, Color)>(); + + var chunkIndex = GetGasChunkIndices(indices); + if (!chunks.TryGetValue(chunkIndex, out var chunk)) + return Array.Empty<(Texture, Color)>(); + + var overlays = chunk.GetData(indices); + if (overlays.Gas == null) + return Array.Empty<(Texture, Color)>(); + var fire = overlays.FireState != 0; var length = overlays.Gas.Length + (fire ? 1 : 0); @@ -112,23 +184,6 @@ namespace Content.Client.GameObjects.EntitySystems return list; } - private void OnTileOverlayMessage(GasTileOverlayMessage ev) - { - if(ev.ClearAllOtherOverlays) - _overlay.Clear(); - - foreach (var data in ev.OverlayData) - { - if (!_overlay.TryGetValue(data.GridIndex, out var gridOverlays)) - { - gridOverlays = new Dictionary(); - _overlay.Add(data.GridIndex, gridOverlays); - } - - gridOverlays[data.GridIndices] = data.Data; - } - } - public override void FrameUpdate(float frameTime) { base.FrameUpdate(frameTime); diff --git a/Content.Server/Atmos/Hotspot.cs b/Content.Server/Atmos/Hotspot.cs index 2178a6bea8..4597813c6f 100644 --- a/Content.Server/Atmos/Hotspot.cs +++ b/Content.Server/Atmos/Hotspot.cs @@ -23,7 +23,7 @@ namespace Content.Server.Atmos /// State for the fire sprite. /// [ViewVariables] - public int State; + public byte State; public void Start() { diff --git a/Content.Server/Atmos/TileAtmosphere.cs b/Content.Server/Atmos/TileAtmosphere.cs index cddfc3ee96..b1ad2df3cf 100644 --- a/Content.Server/Atmos/TileAtmosphere.cs +++ b/Content.Server/Atmos/TileAtmosphere.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; using Content.Server.Atmos.Reactions; using Content.Server.GameObjects.Components.Atmos; using Content.Server.GameObjects.EntitySystems; +using Content.Server.GameObjects.EntitySystems.Atmos; using Content.Server.Interfaces; using Content.Shared.Atmos; using Content.Shared.Audio; @@ -745,7 +746,7 @@ namespace Content.Server.Atmos } else { - Hotspot.State = Hotspot.Volume > Atmospherics.CellVolume * 0.4f ? 2 : 1; + Hotspot.State = (byte) (Hotspot.Volume > Atmospherics.CellVolume * 0.4f ? 2 : 1); } if (Hotspot.Temperature > MaxFireTemperatureSustained) diff --git a/Content.Server/GameObjects/EntitySystems/Atmos/GasTileOverlaySystem.cs b/Content.Server/GameObjects/EntitySystems/Atmos/GasTileOverlaySystem.cs new file mode 100644 index 0000000000..456bb14293 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/Atmos/GasTileOverlaySystem.cs @@ -0,0 +1,475 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using Content.Server.GameObjects.Components.Atmos; +using Content.Server.Interfaces.GameTicking; +using Content.Shared.Atmos; +using Content.Shared.GameObjects.EntitySystems.Atmos; +using JetBrains.Annotations; +using Robust.Server.Interfaces.Player; +using Robust.Server.Player; +using Robust.Shared.Enums; +using Robust.Shared.Interfaces.Configuration; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Map; +using Robust.Shared.Interfaces.Timing; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Timing; + +namespace Content.Server.GameObjects.EntitySystems.Atmos +{ + [UsedImplicitly] + internal sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem + { + [Robust.Shared.IoC.Dependency] private readonly IGameTiming _gameTiming = default!; + [Robust.Shared.IoC.Dependency] private readonly IPlayerManager _playerManager = default!; + [Robust.Shared.IoC.Dependency] private readonly IMapManager _mapManager = default!; + [Robust.Shared.IoC.Dependency] private readonly IConfigurationManager _configManager = default!; + + /// + /// The tiles that have had their atmos data updated since last tick + /// + private Dictionary> _invalidTiles = new Dictionary>(); + + private Dictionary _knownPlayerChunks = + new Dictionary(); + + /// + /// Gas data stored in chunks to make PVS / bubbling easier. + /// + private Dictionary> _overlay = + new Dictionary>(); + + /// + /// How far away do we update gas overlays (minimum; due to chunking further away tiles may also be updated). + /// + private float _updateRange; + // Because the gas overlay updates aren't run every tick we need to avoid the pop-in that might occur with + // the regular PVS range. + private const float RangeOffset = 6.0f; + + /// + /// Overlay update ticks per second. + /// + private float _updateCooldown; + + public override void Initialize() + { + base.Initialize(); + + _playerManager.PlayerStatusChanged += OnPlayerStatusChanged; + _mapManager.OnGridRemoved += OnGridRemoved; + _configManager.RegisterCVar("net.gasoverlaytickrate", 3.0f); + } + + public override void Shutdown() + { + base.Shutdown(); + _playerManager.PlayerStatusChanged -= OnPlayerStatusChanged; + _mapManager.OnGridRemoved -= OnGridRemoved; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Invalidate(GridId gridIndex, MapIndices indices) + { + if (!_invalidTiles.TryGetValue(gridIndex, out var existing)) + { + existing = new HashSet(); + _invalidTiles[gridIndex] = existing; + } + + existing.Add(indices); + } + + private GasOverlayChunk GetOrCreateChunk(GridId gridIndex, MapIndices indices) + { + if (!_overlay.TryGetValue(gridIndex, out var chunks)) + { + chunks = new Dictionary(); + _overlay[gridIndex] = chunks; + } + + var chunkIndices = GetGasChunkIndices(indices); + + if (!chunks.TryGetValue(chunkIndices, out var chunk)) + { + chunk = new GasOverlayChunk(gridIndex, chunkIndices); + chunks[chunkIndices] = chunk; + } + + return chunk; + } + + private void OnGridRemoved(GridId gridId) + { + if (_overlay.ContainsKey(gridId)) + { + _overlay.Remove(gridId); + } + } + + public void ResettingCleanup() + { + _invalidTiles.Clear(); + _overlay.Clear(); + + foreach (var (_, data) in _knownPlayerChunks) + { + data.Reset(); + } + } + + private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e) + { + if (e.NewStatus != SessionStatus.InGame) + { + if (_knownPlayerChunks.ContainsKey(e.Session)) + { + _knownPlayerChunks.Remove(e.Session); + } + + return; + } + + if (!_knownPlayerChunks.ContainsKey(e.Session)) + { + _knownPlayerChunks[e.Session] = new PlayerGasOverlay(); + } + } + + /// + /// Checks whether the overlay-relevant data for a gas tile has been updated. + /// + /// + /// + /// + /// + /// true if updated + private bool TryRefreshTile(GridAtmosphereComponent gam, GasOverlayData oldTile, MapIndices indices, out GasOverlayData overlayData) + { + var tile = gam.GetTile(indices); + var tileData = new List(); + + for (byte i = 0; i < Atmospherics.TotalNumberOfGases; i++) + { + var gas = Atmospherics.GetGas(i); + var overlay = Atmospherics.GetOverlay(i); + if (overlay == null || tile.Air == null) continue; + + var moles = tile.Air.Gases[i]; + + if (moles < gas.GasMolesVisible) continue; + + var data = new GasData(i, (byte) (FloatMath.Clamp01(moles / gas.GasMolesVisibleMax) * 255)); + tileData.Add(data); + } + + overlayData = new GasOverlayData(tile.Hotspot.State, tile.Hotspot.Temperature, tileData.Count == 0 ? null : tileData.ToArray()); + + if (overlayData.Equals(oldTile)) + { + return false; + } + + return true; + } + + /// + /// Get every chunk in range of our entity that exists, including on other grids. + /// + /// + /// + private List GetChunksInRange(IEntity entity) + { + var inRange = new List(); + + // This is the max in any direction that we can get a chunk (e.g. max 2 chunks away of data). + var (maxXDiff, maxYDiff) = ((int) (_updateRange / ChunkSize) + 1, (int) (_updateRange / ChunkSize) + 1); + + var worldBounds = Box2.CenteredAround(entity.Transform.WorldPosition, + new Vector2(_updateRange, _updateRange)); + + foreach (var grid in _mapManager.FindGridsIntersecting(entity.Transform.MapID, worldBounds)) + { + if (!_overlay.TryGetValue(grid.Index, out var chunks)) + { + continue; + } + + var entityTile = grid.GetTileRef(entity.Transform.GridPosition).GridIndices; + + for (var x = -maxXDiff; x <= maxXDiff; x++) + { + for (var y = -maxYDiff; y <= maxYDiff; y++) + { + var chunkIndices = GetGasChunkIndices(new MapIndices(entityTile.X + x * ChunkSize, entityTile.Y + y * ChunkSize)); + + if (!chunks.TryGetValue(chunkIndices, out var chunk)) continue; + + // Now we'll check if it's in range and relevant for us + // (e.g. if we're on the very edge of a chunk we may need more chunks). + + var (xDiff, yDiff) = (chunkIndices.X - entityTile.X, chunkIndices.Y - entityTile.Y); + if (xDiff > 0 && xDiff > _updateRange || + yDiff > 0 && yDiff > _updateRange || + xDiff < 0 && Math.Abs(xDiff + ChunkSize) > _updateRange || + yDiff < 0 && Math.Abs(yDiff + ChunkSize) > _updateRange) continue; + + inRange.Add(chunk); + } + } + } + + return inRange; + } + + public override void Update(float frameTime) + { + AccumulatedFrameTime += frameTime; + _updateCooldown = 1 / _configManager.GetCVar("net.gasoverlaytickrate"); + + if (AccumulatedFrameTime < _updateCooldown) + { + return; + } + + _updateRange = _configManager.GetCVar("net.maxupdaterange") + RangeOffset; + + // TODO: So in the worst case scenario we still have to send a LOT of tile data per tick if there's a fire. + // If we go with say 15 tile radius then we have up to 900 tiles to update per tick. + // In a saltern fire the worst you'll normally see is around 650 at the moment. + // Need a way to fake this more because sending almost 2,000 tile updates per second to even 50 players is... yikes + // I mean that's as big as it gets so larger maps will have the same but still, that's a lot of data. + + // Some ways to do this are potentially: splitting fire and gas update data so they don't update at the same time + // (gives the illusion of more updates happening), e.g. if gas updates are 3 times a second and fires are 1.6 times a second or something. + // Could also look at updating tiles close to us more frequently (e.g. within 1 chunk every tick). + // Stuff just out of our viewport we need so when we move it doesn't pop in but it doesn't mean we need to update it every tick. + + AccumulatedFrameTime -= _updateCooldown; + + var gridAtmosComponents = new Dictionary(); + var updatedTiles = new Dictionary>(); + + // So up to this point we've been caching the updated tiles for multiple ticks. + // Now we'll go through and check whether the update actually matters for the overlay or not, + // and if not then we won't bother sending the data. + foreach (var (gridId, indices) in _invalidTiles) + { + var gridEntityId = _mapManager.GetGrid(gridId).GridEntityId; + + if (!EntityManager.GetEntity(gridEntityId).TryGetComponent(out GridAtmosphereComponent gam)) + { + continue; + } + + // If it's being invalidated it should have this right? + // At any rate we'll cache it for here + the AddChunk + if (!gridAtmosComponents.ContainsKey(gridId)) + { + gridAtmosComponents[gridId] = gam; + } + + foreach (var invalid in indices) + { + var chunk = GetOrCreateChunk(gridId, invalid); + + if (!TryRefreshTile(gam, chunk.GetData(invalid), invalid, out var data)) continue; + + if (!updatedTiles.TryGetValue(chunk, out var tiles)) + { + tiles = new HashSet(); + updatedTiles[chunk] = tiles; + } + + updatedTiles[chunk].Add(invalid); + chunk.Update(data, invalid); + } + } + + var currentTick = _gameTiming.CurTick; + + // Set the LastUpdate for chunks. + foreach (var (chunk, _) in updatedTiles) + { + chunk.Dirty(currentTick); + } + + // 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. + foreach (var (session, overlay) in _knownPlayerChunks) + { + if (session.AttachedEntity == null) continue; + + // Get chunks in range and update if we've moved around or the chunks have new overlay data + var chunksInRange = GetChunksInRange(session.AttachedEntity); + var knownChunks = overlay.GetKnownChunks(); + var chunksToRemove = new List(); + var chunksToAdd = new List(); + + foreach (var chunk in chunksInRange) + { + if (!knownChunks.Contains(chunk)) + { + chunksToAdd.Add(chunk); + } + } + + foreach (var chunk in knownChunks) + { + if (!chunksInRange.Contains(chunk)) + { + chunksToRemove.Add(chunk); + } + } + + foreach (var chunk in chunksToAdd) + { + var message = overlay.AddChunk(currentTick, chunk); + if (message != null) + { + RaiseNetworkEvent(message, session.ConnectedClient); + } + } + + foreach (var chunk in chunksToRemove) + { + overlay.RemoveChunk(chunk); + } + + var clientInvalids = new Dictionary>(); + + // Check for any dirty chunks in range and bundle the data to send to the client. + foreach (var chunk in chunksInRange) + { + if (!updatedTiles.TryGetValue(chunk, out var invalids)) continue; + + if (!clientInvalids.TryGetValue(chunk.GridIndices, out var existingData)) + { + existingData = new List<(MapIndices, GasOverlayData)>(); + clientInvalids[chunk.GridIndices] = existingData; + } + + chunk.GetData(existingData, invalids); + } + + foreach (var (grid, data) in clientInvalids) + { + RaiseNetworkEvent(overlay.UpdateClient(grid, data), session.ConnectedClient); + } + } + + // Cleanup + _invalidTiles.Clear(); + } + private sealed class PlayerGasOverlay + { + private readonly Dictionary> _data = + new Dictionary>(); + + private readonly Dictionary _lastSent = + new Dictionary(); + + public GasOverlayMessage UpdateClient(GridId grid, List<(MapIndices, GasOverlayData)> data) + { + return new GasOverlayMessage(grid, data); + } + + public void Reset() + { + _data.Clear(); + _lastSent.Clear(); + } + + public List GetKnownChunks() + { + var known = new List(); + + foreach (var (_, chunks) in _data) + { + foreach (var (_, chunk) in chunks) + { + known.Add(chunk); + } + } + + return known; + } + + public GasOverlayMessage? AddChunk(GameTick currentTick, GasOverlayChunk chunk) + { + if (!_data.TryGetValue(chunk.GridIndices, out var chunks)) + { + chunks = new Dictionary(); + _data[chunk.GridIndices] = chunks; + } + + if (_lastSent.TryGetValue(chunk, out var last) && last >= chunk.LastUpdate) + { + return null; + } + + _lastSent[chunk] = currentTick; + var message = ChunkToMessage(chunk); + + return message; + } + + public void RemoveChunk(GasOverlayChunk chunk) + { + // Don't need to sync to client as they can manage it themself. + if (!_data.TryGetValue(chunk.GridIndices, out var chunks)) + { + return; + } + + if (chunks.ContainsKey(chunk.MapIndices)) + { + chunks.Remove(chunk.MapIndices); + } + } + + /// + /// Retrieve a whole chunk as a message, only getting the relevant tiles for the gas overlay. + /// + /// + /// + private GasOverlayMessage? ChunkToMessage(GasOverlayChunk chunk) + { + // Chunk data should already be up to date. + // Only send relevant tiles to client. + + var tileData = new List<(MapIndices, GasOverlayData)>(); + + for (var x = 0; x < ChunkSize; x++) + { + for (var y = 0; y < ChunkSize; y++) + { + // TODO: Check could be more robust I think. + var data = chunk.TileData[x, y]; + if ((data.Gas == null || data.Gas.Length == 0) && data.FireState == 0 && data.FireTemperature == 0.0f) + { + continue; + } + + var indices = new MapIndices(chunk.MapIndices.X + x, chunk.MapIndices.Y + y); + tileData.Add((indices, data)); + } + } + + if (tileData.Count == 0) + { + return null; + } + + return new GasOverlayMessage(chunk.GridIndices, tileData); + } + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/GasTileOverlaySystem.cs b/Content.Server/GameObjects/EntitySystems/GasTileOverlaySystem.cs deleted file mode 100644 index 493ef068ed..0000000000 --- a/Content.Server/GameObjects/EntitySystems/GasTileOverlaySystem.cs +++ /dev/null @@ -1,145 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using Content.Server.GameObjects.Components.Atmos; -using Content.Shared.Atmos; -using Content.Shared.GameObjects.EntitySystems; -using JetBrains.Annotations; -using Robust.Server.Interfaces.Player; -using Robust.Server.Player; -using Robust.Shared.Enums; -using Robust.Shared.Interfaces.GameObjects; -using Robust.Shared.Interfaces.Map; -using Robust.Shared.IoC; -using Robust.Shared.Map; - -namespace Content.Server.GameObjects.EntitySystems -{ - [UsedImplicitly] - public sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem - { - private int _tickTimer = 0; - private HashSet _queue = new HashSet(); - private Dictionary> _invalid = new Dictionary>(); - - private Dictionary> _overlay = - new Dictionary>(); - - [Robust.Shared.IoC.Dependency] private IPlayerManager _playerManager = default!; - - public override void Initialize() - { - base.Initialize(); - - _playerManager.PlayerStatusChanged += OnPlayerStatusChanged; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Invalidate(GridId gridIndex, MapIndices indices) - { - if (!_invalid.TryGetValue(gridIndex, out var set) || set == null) - { - set = new HashSet(); - _invalid.Add(gridIndex, set); - } - - set.Add(indices); - } - - public void SetTileOverlay(GridId gridIndex, MapIndices indices, GasData[] gasData, int fireState = 0, float fireTemperature = 0f) - { - if(!_overlay.TryGetValue(gridIndex, out var _)) - _overlay[gridIndex] = new Dictionary(); - - _overlay[gridIndex][indices] = new GasOverlayData(fireState, fireTemperature, gasData); - _queue.Add(GetData(gridIndex, indices)); - } - - private void OnPlayerStatusChanged(object sender, SessionStatusEventArgs e) - { - if (e.NewStatus != SessionStatus.InGame) return; - - RaiseNetworkEvent(new GasTileOverlayMessage(GetData(), true), e.Session.ConnectedClient); - } - - private GasTileOverlayData[] GetData() - { - var list = new List(); - - foreach (var (gridId, tiles) in _overlay) - { - foreach (var (indices, _) in tiles) - { - var data = GetData(gridId, indices); - if(data.Data.Gas.Length > 0) - list.Add(data); - } - } - - return list.ToArray(); - } - - private GasTileOverlayData GetData(GridId gridIndex, MapIndices indices) - { - return new GasTileOverlayData(gridIndex, indices, _overlay[gridIndex][indices]); - } - - private void Revalidate() - { - var mapMan = IoCManager.Resolve(); - var entityMan = IoCManager.Resolve(); - var list = new List(); - - foreach (var (gridId, indices) in _invalid) - { - if (!mapMan.GridExists(gridId)) - { - _invalid.Remove(gridId); - return; - } - var grid = entityMan.GetEntity(mapMan.GetGrid(gridId).GridEntityId); - if (!grid.TryGetComponent(out GridAtmosphereComponent gam)) continue; - - foreach (var index in indices) - { - var tile = gam.GetTile(index); - - if (tile?.Air == null) continue; - - list.Clear(); - - for(var i = 0; i < Atmospherics.TotalNumberOfGases; i++) - { - var gas = Atmospherics.GetGas(i); - var overlay = gas.GasOverlay; - if (overlay == null) continue; - var moles = tile.Air.Gases[i]; - if(moles == 0f || moles < gas.GasMolesVisible) continue; - list.Add(new GasData(i, MathF.Max(MathF.Min(1, moles / gas.GasMolesVisibleMax), 0f))); - } - - if (list.Count == 0) continue; - - SetTileOverlay(gridId, index, list.ToArray(), tile.Hotspot.State, tile.Hotspot.Temperature); - } - - indices.Clear(); - } - } - - public override void Update(float frameTime) - { - _tickTimer++; - - Revalidate(); - - if (_tickTimer < 10) return; - - _tickTimer = 0; - if(_queue.Count > 0) - RaiseNetworkEvent(new GasTileOverlayMessage(_queue.ToArray())); - _queue.Clear(); - } - } -} diff --git a/Content.Server/GameTicking/GameTicker.cs b/Content.Server/GameTicking/GameTicker.cs index 42bd24d44c..839844f61f 100644 --- a/Content.Server/GameTicking/GameTicker.cs +++ b/Content.Server/GameTicking/GameTicker.cs @@ -13,6 +13,7 @@ using Content.Server.GameObjects.Components.PDA; using Content.Server.GameObjects.EntitySystems; using Content.Server.GameObjects.EntitySystems.AI.Pathfinding; using Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible; +using Content.Server.GameObjects.EntitySystems.Atmos; using Content.Server.GameObjects.EntitySystems.StationEvents; using Content.Server.GameTicking.GamePresets; using Content.Server.Interfaces; @@ -633,7 +634,8 @@ namespace Content.Server.GameTicking _playerJoinLobby(player); } - + + EntitySystem.Get().ResettingCleanup(); EntitySystem.Get().ResettingCleanup(); EntitySystem.Get().ResettingCleanup(); EntitySystem.Get().ResetLayouts(); diff --git a/Content.Server/IgnoredComponents.cs b/Content.Server/IgnoredComponents.cs index 4aa536c30d..cd9713f35d 100644 --- a/Content.Server/IgnoredComponents.cs +++ b/Content.Server/IgnoredComponents.cs @@ -19,7 +19,6 @@ "Marker", "EmergencyLight", "Clickable", - "CanSeeGases", "RadiatingLight", }; diff --git a/Content.Shared/Atmos/Atmospherics.cs b/Content.Shared/Atmos/Atmospherics.cs index 6872999f85..c39b6b7d5d 100644 --- a/Content.Shared/Atmos/Atmospherics.cs +++ b/Content.Shared/Atmos/Atmospherics.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using Robust.Shared.IoC; using Robust.Shared.Prototypes; +using Robust.Shared.Utility; namespace Content.Shared.Atmos { @@ -14,10 +15,18 @@ namespace Content.Shared.Atmos var protoMan = IoCManager.Resolve(); GasPrototypes = new GasPrototype[TotalNumberOfGases]; + GasOverlays = new SpriteSpecifier[TotalNumberOfGases]; for (var i = 0; i < TotalNumberOfGases; i++) { - GasPrototypes[i] = protoMan.Index(i.ToString()); + var gasPrototype = protoMan.Index(i.ToString()); + GasPrototypes[i] = gasPrototype; + + if(string.IsNullOrEmpty(gasPrototype.GasOverlaySprite) && !string.IsNullOrEmpty(gasPrototype.GasOverlayTexture)) + GasOverlays[i] = new SpriteSpecifier.Texture(new ResourcePath(gasPrototype.GasOverlayTexture)); + + if(!string.IsNullOrEmpty(gasPrototype.GasOverlaySprite) && !string.IsNullOrEmpty(gasPrototype.GasOverlayState)) + GasOverlays[i] = new SpriteSpecifier.Rsi(new ResourcePath(gasPrototype.GasOverlaySprite), gasPrototype.GasOverlayState); } } @@ -27,6 +36,10 @@ namespace Content.Shared.Atmos public static GasPrototype GetGas(Gas gasId) => GasPrototypes[(int) gasId]; public static IEnumerable Gases => GasPrototypes; + private static readonly SpriteSpecifier[] GasOverlays; + + public static SpriteSpecifier GetOverlay(int overlayId) => GasOverlays[overlayId]; + #region ATMOS /// /// The universal gas constant, in kPa*L/(K*mol) @@ -166,7 +179,7 @@ namespace Content.Shared.Atmos /// /// Total number of gases. Increase this if you want to add more! /// - public const int TotalNumberOfGases = 6; + public const byte TotalNumberOfGases = 6; /// /// Amount of heat released per mole of burnt hydrogen or tritium (hydrogen isotope) diff --git a/Content.Shared/Atmos/GasPrototype.cs b/Content.Shared/Atmos/GasPrototype.cs index 8a71811760..31827d2447 100644 --- a/Content.Shared/Atmos/GasPrototype.cs +++ b/Content.Shared/Atmos/GasPrototype.cs @@ -45,23 +45,6 @@ namespace Content.Shared.Atmos /// public string GasOverlaySprite { get; set; } - /// - /// Sprite specifier for the gas overlay. - /// - public SpriteSpecifier GasOverlay - { - get - { - if(string.IsNullOrEmpty(GasOverlaySprite) && !string.IsNullOrEmpty(GasOverlayTexture)) - return new SpriteSpecifier.Texture(new ResourcePath(GasOverlayTexture)); - - if(!string.IsNullOrEmpty(GasOverlaySprite) && !string.IsNullOrEmpty(GasOverlayState)) - return new SpriteSpecifier.Rsi(new ResourcePath(GasOverlaySprite), GasOverlayState); - - return null; - } - } - /// /// Path to the tile overlay used when this gas appears visible. /// diff --git a/Content.Shared/GameObjects/EntitySystems/Atmos/GasOverlayChunk.cs b/Content.Shared/GameObjects/EntitySystems/Atmos/GasOverlayChunk.cs new file mode 100644 index 0000000000..cc4ce2988c --- /dev/null +++ b/Content.Shared/GameObjects/EntitySystems/Atmos/GasOverlayChunk.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Robust.Shared.Interfaces.Timing; +using Robust.Shared.IoC; +using Robust.Shared.Map; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Shared.GameObjects.EntitySystems.Atmos +{ + public sealed class GasOverlayChunk + { + /// + /// Grid for this chunk + /// + public GridId GridIndices { get; } + + /// + /// Origin of this chunk + /// + public MapIndices MapIndices { get; } + + public SharedGasTileOverlaySystem.GasOverlayData[,] TileData = new SharedGasTileOverlaySystem.GasOverlayData[SharedGasTileOverlaySystem.ChunkSize, SharedGasTileOverlaySystem.ChunkSize]; + + public GameTick LastUpdate { get; private set; } + + public GasOverlayChunk(GridId gridIndices, MapIndices mapIndices) + { + GridIndices = gridIndices; + MapIndices = mapIndices; + } + + public void Dirty(GameTick currentTick) + { + LastUpdate = currentTick; + } + + /// + /// Flags Dirty if the data is different. + /// + /// + /// + public void Update(SharedGasTileOverlaySystem.GasOverlayData data, MapIndices indices) + { + DebugTools.Assert(InBounds(indices)); + var (offsetX, offsetY) = (indices.X - MapIndices.X, + indices.Y - MapIndices.Y); + + TileData[offsetX, offsetY] = data; + } + + public void Update(SharedGasTileOverlaySystem.GasOverlayData data, byte x, byte y) + { + DebugTools.Assert(x < SharedGasTileOverlaySystem.ChunkSize && y < SharedGasTileOverlaySystem.ChunkSize); + + TileData[x, y] = data; + } + + public IEnumerable GetAllData() + { + for (var x = 0; x < SharedGasTileOverlaySystem.ChunkSize; x++) + { + for (var y = 0; y < SharedGasTileOverlaySystem.ChunkSize; y++) + { + yield return TileData[x, y]; + } + } + } + + public void GetData(List<(MapIndices, SharedGasTileOverlaySystem.GasOverlayData)> existingData, HashSet indices) + { + foreach (var index in indices) + { + existingData.Add((index, GetData(index))); + } + } + + public IEnumerable GetAllIndices() + { + for (var x = 0; x < SharedGasTileOverlaySystem.ChunkSize; x++) + { + for (var y = 0; y < SharedGasTileOverlaySystem.ChunkSize; y++) + { + yield return new MapIndices(MapIndices.X + x, MapIndices.Y + y); + } + } + } + + public SharedGasTileOverlaySystem.GasOverlayData GetData(MapIndices indices) + { + DebugTools.Assert(InBounds(indices)); + return TileData[indices.X - MapIndices.X, indices.Y - MapIndices.Y]; + } + + private bool InBounds(MapIndices indices) + { + if (indices.X < MapIndices.X || indices.Y < MapIndices.Y) return false; + if (indices.X >= MapIndices.X + SharedGasTileOverlaySystem.ChunkSize || indices.Y >= MapIndices.Y + SharedGasTileOverlaySystem.ChunkSize) return false; + return true; + } + } +} \ No newline at end of file diff --git a/Content.Shared/GameObjects/EntitySystems/Atmos/SharedGasTileOverlaySystem.cs b/Content.Shared/GameObjects/EntitySystems/Atmos/SharedGasTileOverlaySystem.cs new file mode 100644 index 0000000000..6ff74584d0 --- /dev/null +++ b/Content.Shared/GameObjects/EntitySystems/Atmos/SharedGasTileOverlaySystem.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Map; +using Robust.Shared.Serialization; +using Robust.Shared.Utility; + +namespace Content.Shared.GameObjects.EntitySystems.Atmos +{ + public abstract class SharedGasTileOverlaySystem : EntitySystem + { + public const byte ChunkSize = 8; + protected float AccumulatedFrameTime; + + public static MapIndices GetGasChunkIndices(MapIndices indices) + { + return new MapIndices((int) Math.Floor((float) indices.X / ChunkSize) * ChunkSize, (int) MathF.Floor((float) indices.Y / ChunkSize) * ChunkSize); + } + + [Serializable, NetSerializable] + public struct GasData + { + public byte Index { get; set; } + public byte Opacity { get; set; } + + public GasData(byte gasId, byte opacity) + { + Index = gasId; + Opacity = opacity; + } + } + + [Serializable, NetSerializable] + public readonly struct GasOverlayData : IEquatable + { + public readonly byte FireState; + public readonly float FireTemperature; + public readonly GasData[] Gas; + + public GasOverlayData(byte fireState, float fireTemperature, GasData[] gas) + { + FireState = fireState; + FireTemperature = fireTemperature; + Gas = gas; + } + + public bool Equals(GasOverlayData other) + { + // TODO: Moony had a suggestion on how to do this faster with the hash + // https://discordapp.com/channels/310555209753690112/310555209753690112/744080145219846204 + // Aside from that I can't really see any low-hanging fruit CPU perf wise. + if (Gas?.Length != other.Gas?.Length) return false; + if (FireState != other.FireState) return false; + if (FireTemperature != other.FireTemperature) return false; + + if (Gas == null) + { + return true; + } + + DebugTools.Assert(other.Gas != null); + + for (var i = 0; i < Gas.Length; i++) + { + var thisGas = Gas[i]; + var otherGas = other.Gas[i]; + + if (!thisGas.Equals(otherGas)) + { + return false; + } + } + + return true; + } + } + + /// + /// Invalid tiles for the gas overlay. + /// No point re-sending every tile if only a subset might have been updated. + /// + [Serializable, NetSerializable] + public sealed class GasOverlayMessage : EntitySystemMessage + { + public GridId GridId { get; } + + public List<(MapIndices, GasOverlayData)> OverlayData { get; } + + public GasOverlayMessage(GridId gridIndices, List<(MapIndices,GasOverlayData)> overlayData) + { + GridId = gridIndices; + OverlayData = overlayData; + } + } + } +} diff --git a/Content.Shared/GameObjects/EntitySystems/SharedGasTileOverlaySystem.cs b/Content.Shared/GameObjects/EntitySystems/SharedGasTileOverlaySystem.cs deleted file mode 100644 index 346f44d733..0000000000 --- a/Content.Shared/GameObjects/EntitySystems/SharedGasTileOverlaySystem.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using Robust.Shared.GameObjects; -using Robust.Shared.GameObjects.Systems; -using Robust.Shared.Map; -using Robust.Shared.Serialization; - -namespace Content.Shared.GameObjects.EntitySystems -{ - public abstract class SharedGasTileOverlaySystem : EntitySystem - { - [Serializable, NetSerializable] - public struct GasData - { - public int Index { get; set; } - public float Opacity { get; set; } - - public GasData(int gasId, float opacity) - { - Index = gasId; - Opacity = opacity; - } - } - - [Serializable, NetSerializable] - public readonly struct GasOverlayData - { - public readonly int FireState; - public readonly float FireTemperature; - public readonly GasData[] Gas; - - public GasOverlayData(int fireState, float fireTemperature, GasData[] gas) - { - FireState = fireState; - FireTemperature = fireTemperature; - Gas = gas; - } - } - - [Serializable, NetSerializable] - public readonly struct GasTileOverlayData - { - public readonly GridId GridIndex; - public readonly MapIndices GridIndices; - public readonly GasOverlayData Data; - - public GasTileOverlayData(GridId gridIndex, MapIndices gridIndices, GasOverlayData data) - { - GridIndex = gridIndex; - GridIndices = gridIndices; - Data = data; - } - - public override int GetHashCode() - { - return GridIndex.GetHashCode() ^ GridIndices.GetHashCode() ^ Data.GetHashCode(); - } - } - - [Serializable, NetSerializable] - public class GasTileOverlayMessage : EntitySystemMessage - { - public GasTileOverlayData[] OverlayData { get; } - public bool ClearAllOtherOverlays { get; } - - public GasTileOverlayMessage(GasTileOverlayData[] overlayData, bool clearAllOtherOverlays = false) - { - OverlayData = overlayData; - ClearAllOtherOverlays = clearAllOtherOverlays; - } - } - } -} diff --git a/Resources/Prototypes/Entities/Mobs/Player/observer.yml b/Resources/Prototypes/Entities/Mobs/Player/observer.yml index 4b6fea353d..ef8f085f0c 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/observer.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/observer.yml @@ -22,7 +22,6 @@ DoRangeCheck: false - type: IgnorePause - type: Ghost - - type: CanSeeGases - type: Sprite netsync: false drawdepth: Ghosts diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml index 040ff8e983..d12d5fd9f0 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml @@ -154,7 +154,6 @@ - type: Grammar proper: true - type: Pullable - - type: CanSeeGases - type: DoAfter - type: Strippable - type: UserInterface