diff --git a/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs b/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs index b45836a437..dc37dca7a3 100644 --- a/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs +++ b/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs @@ -1,264 +1,70 @@ using Content.Client.Atmos.Overlays; -using Content.Shared.Atmos; using Content.Shared.Atmos.EntitySystems; +using Content.Shared.GameTicking; using JetBrains.Annotations; +using Robust.Client.GameObjects; 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 + public sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem { [Dependency] private readonly IResourceCache _resourceCache = default!; - [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; + [Dependency] private readonly IOverlayManager _overlayMan = default!; + [Dependency] private readonly SpriteSystem _spriteSys = 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; + private GasTileOverlay _overlay = default!; public override void Initialize() { base.Initialize(); - SubscribeNetworkEvent(HandleGasOverlayMessage); + SubscribeNetworkEvent(HandleGasOverlayUpdate); 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()); + _overlay = new GasTileOverlay(this, _resourceCache, ProtoMan, _spriteSys); + _overlayMan.AddOverlay(_overlay); } - private void HandleGasOverlayMessage(GasOverlayMessage message) + public override void Reset(RoundRestartCleanupEvent ev) { - 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; + _overlay.TileData.Clear(); } public override void Shutdown() { base.Shutdown(); - var overlayManager = IoCManager.Resolve(); - overlayManager.RemoveOverlay(); - overlayManager.RemoveOverlay(); + _overlayMan.RemoveOverlay(_overlay); + } + + private void HandleGasOverlayUpdate(GasOverlayUpdateEvent ev) + { + foreach (var (grid, removedIndicies) in ev.RemovedChunks) + { + if (!_overlay.TileData.TryGetValue(grid, out var chunks)) + continue; + + foreach (var index in removedIndicies) + { + chunks.Remove(index); + } + } + + foreach (var (grid, gridData) in ev.UpdatedChunks) + { + var chunks = _overlay.TileData.GetOrNew(grid); + foreach (var chunkData in gridData) + { + chunks[chunkData.Index] = chunkData; + } + } } private void OnGridRemoved(GridRemovalEvent ev) { - _tileData.Remove(ev.EntityUid); - } - - 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; - } + _overlay.TileData.Remove(ev.EntityUid); } } } diff --git a/Content.Client/Atmos/Overlays/FireTileOverlay.cs b/Content.Client/Atmos/Overlays/FireTileOverlay.cs deleted file mode 100644 index b32f8699fc..0000000000 --- a/Content.Client/Atmos/Overlays/FireTileOverlay.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Content.Client.Atmos.EntitySystems; -using Robust.Client.Graphics; -using Robust.Shared.Enums; -using Robust.Shared.Prototypes; -using Robust.Shared.Map; - -namespace Content.Client.Atmos.Overlays -{ - public sealed class FireTileOverlay : Overlay - { - private readonly GasTileOverlaySystem _gasTileOverlaySystem; - - [Dependency] private readonly IMapManager _mapManager = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - - public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV; - private readonly ShaderInstance _shader; - - public FireTileOverlay() - { - IoCManager.InjectDependencies(this); - - _gasTileOverlaySystem = EntitySystem.Get(); - _shader = _prototypeManager.Index("unshaded").Instance().Duplicate(); - ZIndex = GasTileOverlaySystem.GasOverlayZIndex + 1; - } - - protected override void Draw(in OverlayDrawArgs args) - { - var drawHandle = args.WorldHandle; - - var mapId = args.Viewport.Eye!.Position.MapId; - var worldBounds = args.WorldBounds; - - drawHandle.UseShader(_shader); - - foreach (var mapGrid in _mapManager.FindGridsIntersecting(mapId, worldBounds)) - { - if (!_gasTileOverlaySystem.HasData(mapGrid.GridEntityId)) - continue; - - drawHandle.SetTransform(mapGrid.WorldMatrix); - - foreach (var tile in mapGrid.GetTilesIntersecting(worldBounds)) - { - var enumerator = _gasTileOverlaySystem.GetFireOverlays(mapGrid.GridEntityId, tile.GridIndices); - while (enumerator.MoveNext(out var tuple)) - { - drawHandle.DrawTexture(tuple.Texture, new Vector2(tile.X, tile.Y), tuple.Color); - } - } - } - - drawHandle.SetTransform(Matrix3.Identity); - } - } -} diff --git a/Content.Client/Atmos/Overlays/GasTileOverlay.cs b/Content.Client/Atmos/Overlays/GasTileOverlay.cs index b97eaa15dc..ed98936e91 100644 --- a/Content.Client/Atmos/Overlays/GasTileOverlay.cs +++ b/Content.Client/Atmos/Overlays/GasTileOverlay.cs @@ -1,54 +1,208 @@ using Content.Client.Atmos.EntitySystems; +using Content.Shared.Atmos; +using Content.Shared.Atmos.Prototypes; +using Robust.Client.GameObjects; using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; using Robust.Shared.Enums; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; using Robust.Shared.Map; -using Robust.Shared.Maths; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; +using Robust.Shared.Utility; namespace Content.Client.Atmos.Overlays { public sealed class GasTileOverlay : Overlay { - private readonly GasTileOverlaySystem _gasTileOverlaySystem; + private readonly GasTileOverlaySystem _system; + private readonly IMapManager _mapManager; + + public override OverlaySpace Space => OverlaySpace.WorldSpaceEntities; + private readonly ShaderInstance _shader; - [Dependency] private readonly IMapManager _mapManager = default!; + public readonly Dictionary> TileData = new(); - public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV; + // Gas overlays + private readonly float[] _timer; + private readonly float[][] _frameDelays; + private readonly int[] _frameCounter; - public GasTileOverlay() + // 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) Content.Shared.DrawDepth.DrawDepth.Effects; // Under ghosts, above mostly everything else + + public GasTileOverlay(GasTileOverlaySystem system, IResourceCache resourceCache, IPrototypeManager protoMan, SpriteSystem spriteSys) { - IoCManager.InjectDependencies(this); + _system = system; + _mapManager = IoCManager.Resolve(); + _shader = protoMan.Index("unshaded").Instance(); + ZIndex = GasOverlayZIndex; - _gasTileOverlaySystem = EntitySystem.Get(); - ZIndex = GasTileOverlaySystem.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 = protoMan.Index(_system.VisibleGasId[i].ToString()); + + SpriteSpecifier overlay; + + if (!string.IsNullOrEmpty(gasPrototype.GasOverlaySprite) && !string.IsNullOrEmpty(gasPrototype.GasOverlayState)) + overlay = new SpriteSpecifier.Rsi(new ResourcePath(gasPrototype.GasOverlaySprite), gasPrototype.GasOverlayState); + else if (!string.IsNullOrEmpty(gasPrototype.GasOverlayTexture)) + overlay = new SpriteSpecifier.Texture(new ResourcePath(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(RSI.State.Direction.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(RSI.State.Direction.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) { var drawHandle = args.WorldHandle; - var mapId = args.Viewport.Eye!.Position.MapId; - var worldBounds = args.WorldBounds; - - drawHandle.UseShader(null); - - foreach (var mapGrid in _mapManager.FindGridsIntersecting(mapId, worldBounds)) + foreach (var mapGrid in _mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds)) { - if (!_gasTileOverlaySystem.HasData(mapGrid.GridEntityId)) + if (!TileData.TryGetValue(mapGrid.GridEntityId, out var gridData)) continue; drawHandle.SetTransform(mapGrid.WorldMatrix); + var floatBounds = mapGrid.InvWorldMatrix.TransformBox(in args.WorldBounds); + var localBounds = new Box2i( + (int) MathF.Floor(floatBounds.Left), + (int) MathF.Floor(floatBounds.Bottom), + (int) MathF.Ceiling(floatBounds.Right), + (int) MathF.Ceiling(floatBounds.Top)); - foreach (var tile in mapGrid.GetTilesIntersecting(worldBounds)) + // 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. + + drawHandle.UseShader(null); + foreach (var chunk in gridData.Values) { - var enumerator = _gasTileOverlaySystem.GetOverlays(mapGrid.GridEntityId, tile.GridIndices); - while (enumerator.MoveNext(out var tuple)) + var enumerator = new GasChunkEnumerator(chunk); + + while (enumerator.MoveNext(out var gas)) { - drawHandle.DrawTexture(tuple.Texture, new Vector2(tile.X, tile.Y), tuple.Color); + if (gas.Value.Opacity == null) + continue; + + var tilePosition = chunk.Origin + (enumerator.X, enumerator.Y); + if (!localBounds.Contains(tilePosition)) + continue; + + for (var i = 0; i < _gasCount; i++) + { + var opacity = gas.Value.Opacity[i]; + if (opacity > 0) + drawHandle.DrawTexture(_frames[i][_frameCounter[i]], tilePosition, Color.White.WithAlpha(opacity)); + } + } + } + + // And again for fire, with the unshaded shader + drawHandle.UseShader(_shader); + foreach (var chunk in gridData.Values) + { + var enumerator = new GasChunkEnumerator(chunk); + + while (enumerator.MoveNext(out var gas)) + { + if (gas.Value.FireState == 0) + continue; + + var index = chunk.Origin + (enumerator.X, enumerator.Y); + if (!localBounds.Contains(index)) + continue; + + var state = gas.Value.FireState - 1; + var texture = _fireFrames[state][_fireFrameCounter[state]]; + drawHandle.DrawTexture(texture, index); } } } + + drawHandle.UseShader(null); + drawHandle.SetTransform(Matrix3.Identity); } } } diff --git a/Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs b/Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs index 118ba3822d..7332a5b64b 100644 --- a/Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs +++ b/Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs @@ -1,18 +1,21 @@ -using System.Linq; using System.Runtime.CompilerServices; using Content.Server.Atmos.Components; using Content.Shared.Atmos; using Content.Shared.Atmos.EntitySystems; using Content.Shared.CCVar; +using Content.Shared.Chunking; using Content.Shared.GameTicking; using Content.Shared.Rounding; using JetBrains.Annotations; +using Microsoft.Extensions.ObjectPool; using Robust.Server.Player; -using Robust.Shared; using Robust.Shared.Configuration; using Robust.Shared.Enums; using Robust.Shared.Map; +using Robust.Shared.Player; using Robust.Shared.Timing; +using Robust.Shared.Utility; +using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute; // ReSharper disable once RedundantUsingDirective @@ -21,38 +24,38 @@ namespace Content.Server.Atmos.EntitySystems [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 AtmosphereSystem _atmosphereSystem = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IConfigurationManager _confMan = default!; + [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; + [Dependency] private readonly ChunkingSystem _chunkingSys = default!; + + private readonly Dictionary>> _lastSentChunks = new(); /// /// The tiles that have had their atmos data updated since last tick /// private readonly Dictionary> _invalidTiles = new(); - private readonly Dictionary _knownPlayerChunks = - new(); - /// /// Gas data stored in chunks to make PVS / bubbling easier. /// private readonly Dictionary> _overlay = new(); - /// - /// 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; + // Oh look its more duplicated decal system code! + private ObjectPool> _chunkIndexPool = + new DefaultObjectPool>( + new DefaultPooledObjectPolicy>(), 64); + private ObjectPool>> _chunkViewerPool = + new DefaultObjectPool>>( + new DefaultPooledObjectPolicy>>(), 64); /// - /// Overlay update ticks per second. + /// Overlay update interval, in seconds. /// - private float _updateCooldown; + private float _updateInterval; private int _thresholds; @@ -60,51 +63,28 @@ namespace Content.Server.Atmos.EntitySystems { base.Initialize(); - SubscribeLocalEvent(Reset); SubscribeLocalEvent(OnGridRemoved); _playerManager.PlayerStatusChanged += OnPlayerStatusChanged; - var configManager = IoCManager.Resolve(); - configManager.OnValueChanged(CCVars.NetGasOverlayTickRate, value => _updateCooldown = value > 0.0f ? 1 / value : float.MaxValue, true); - configManager.OnValueChanged(CVars.NetMaxUpdateRange, value => _updateRange = value + RangeOffset, true); - configManager.OnValueChanged(CCVars.GasOverlayThresholds, value => _thresholds = value, true); + _confMan.OnValueChanged(CCVars.NetGasOverlayTickRate, UpdateTickRate, true); + _confMan.OnValueChanged(CCVars.GasOverlayThresholds, UpdateThresholds, true); } public override void Shutdown() { base.Shutdown(); _playerManager.PlayerStatusChanged -= OnPlayerStatusChanged; + _confMan.UnsubValueChanged(CCVars.NetGasOverlayTickRate, UpdateTickRate); + _confMan.UnsubValueChanged(CCVars.GasOverlayThresholds, UpdateThresholds); } + private void UpdateTickRate(float value) => _updateInterval = value > 0.0f ? 1 / value : float.MaxValue; + private void UpdateThresholds(int value) => _thresholds = value; + [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Invalidate(EntityUid gridIndex, Vector2i indices) + public void Invalidate(EntityUid grid, Vector2i index) { - if (!_invalidTiles.TryGetValue(gridIndex, out var existing)) - { - existing = new HashSet(); - _invalidTiles[gridIndex] = existing; - } - - existing.Add(indices); - } - - private GasOverlayChunk GetOrCreateChunk(EntityUid gridIndex, Vector2i 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; + _invalidTiles.GetOrNew(grid).Add(index); } private void OnGridRemoved(GridRemovalEvent ev) @@ -116,119 +96,95 @@ namespace Content.Server.Atmos.EntitySystems { if (e.NewStatus != SessionStatus.InGame) { - if (_knownPlayerChunks.ContainsKey(e.Session)) - { - _knownPlayerChunks.Remove(e.Session); - } - + if (_lastSentChunks.Remove(e.Session, out var set)) + ReturnToPool(set); return; } - if (!_knownPlayerChunks.ContainsKey(e.Session)) + if (!_lastSentChunks.ContainsKey(e.Session)) { - _knownPlayerChunks[e.Session] = new PlayerGasOverlay(); + _lastSentChunks[e.Session] = _chunkViewerPool.Get(); + DebugTools.Assert(_lastSentChunks[e.Session].Count == 0); } } - /// - /// Checks whether the overlay-relevant data for a gas tile has been updated. - /// - /// - /// - /// - /// - /// true if updated - private bool TryRefreshTile(GridAtmosphereComponent gridAtmosphere, GasOverlayData oldTile, Vector2i indices, out GasOverlayData overlayData) + private void ReturnToPool(Dictionary> chunks) { - if (!gridAtmosphere.Tiles.TryGetValue(indices, out var tile)) + foreach (var (_, previous) in chunks) { - overlayData = default; - return false; + previous.Clear(); + _chunkIndexPool.Return(previous); } - var tileData = new List(); + chunks.Clear(); + _chunkViewerPool.Return(chunks); + } - if(tile.Air != null) - for (byte i = 0; i < Atmospherics.TotalNumberOfGases; i++) + /// + /// Updates the visuals for a tile on some grid chunk. + /// + private void UpdateChunkTile(GridAtmosphereComponent gridAtmosphere, GasOverlayChunk chunk, Vector2i index, GameTick curTick) + { + var oldData = chunk.GetData(index); + if (!gridAtmosphere.Tiles.TryGetValue(index, out var tile)) + { + if (oldData == null) + return; + + chunk.LastUpdate = curTick; + chunk.SetData(index, null); + return; + } + + var opacity = new byte[VisibleGasId.Length]; + GasOverlayData newData = new(tile!.Hotspot.State, opacity); + if (tile.Air != null) + { + var i = 0; + foreach (var id in VisibleGasId) { - var gas = _atmosphereSystem.GetGas(i); - var overlay = _atmosphereSystem.GetOverlay(i); - if (overlay == null) continue; + var gas = _atmosphereSystem.GetGas(id); + var moles = tile.Air.Moles[id]; - var moles = tile.Air.Moles[i]; - - if (moles < gas.GasMolesVisible) continue; - - var opacity = (byte) (ContentHelpers.RoundToLevels(MathHelper.Clamp01(moles / gas.GasMolesVisibleMax) * 255, byte.MaxValue, _thresholds) * 255 / (_thresholds - 1)); - var data = new GasData(i, opacity); - tileData.Add(data); + if (moles >= gas.GasMolesVisible) + { + opacity[i] = (byte) (ContentHelpers.RoundToLevels( + MathHelper.Clamp01(moles / gas.GasMolesVisibleMax) * 255, byte.MaxValue, _thresholds) * 255 / (_thresholds - 1)); + } + i++; } - - overlayData = new GasOverlayData(tile!.Hotspot.State, tile.Hotspot.Temperature, tileData.Count == 0 ? Array.Empty() : tileData.ToArray()); - - if (overlayData.Equals(oldTile)) - { - return false; } - return true; + if (oldData != null && oldData.Value.Equals(newData)) + return; + + chunk.SetData(index, newData); + chunk.LastUpdate = curTick; } - /// - /// Get every chunk in range of our entity that exists, including on other grids. - /// - /// - /// - private List GetChunksInRange(EntityUid entity) + private void UpdateOverlayData(GameTick curTick) { - // 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); - - // Setting initial list size based on the theoretical max number of chunks from a single grid. For default - // parameters, this is currently 6^2 = 36. Unless a player is near more than one grid, this is will - // generally slightly over-estimate the actual list size, which will be either 25, 30, or 36 (assuming the - // player is not near the edge of a grid). - var initialListSize = (1 + (int) MathF.Ceiling(2 * _updateRange / ChunkSize)) * (1 + (int) MathF.Ceiling(2 * _updateRange / ChunkSize)); - - var inRange = new List(initialListSize); - - var xform = Transform(entity); - var worldPos = xform.MapPosition; - - var worldBounds = Box2.CenteredAround(worldPos.Position, new Vector2(_updateRange, _updateRange)); - - foreach (var grid in _mapManager.FindGridsIntersecting(xform.MapID, worldBounds)) + foreach (var (gridId, invalidIndices) in _invalidTiles) { - if (!_overlay.TryGetValue(grid.GridEntityId, out var chunks)) + if (!TryComp(gridId, out GridAtmosphereComponent? gam)) { + _overlay.Remove(gridId); continue; } - var entityTile = grid.GetTileRef(worldPos).GridIndices; + var chunks = _overlay.GetOrNew(gridId); - for (var x = -maxXDiff; x <= maxXDiff; x++) + foreach (var index in invalidIndices) { - for (var y = -maxYDiff; y <= maxYDiff; y++) - { - var chunkIndices = GetGasChunkIndices(new Vector2i(entityTile.X + x * ChunkSize, entityTile.Y + y * ChunkSize)); + var chunkIndex = GetGasChunkIndices(index); - if (!chunks.TryGetValue(chunkIndices, out var chunk)) continue; + if (!chunks.TryGetValue(chunkIndex, out var chunk)) + chunks[chunkIndex] = chunk = new GasOverlayChunk(chunkIndex); - // 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 > _updateRange || - yDiff > _updateRange || - xDiff < 0 && Math.Abs(xDiff + ChunkSize) > _updateRange || - yDiff < 0 && Math.Abs(yDiff + ChunkSize) > _updateRange) continue; - - inRange.Add(chunk); - } + UpdateChunkTile(gam, chunk, index, curTick); } } - - return inRange; + _invalidTiles.Clear(); } public override void Update(float frameTime) @@ -236,254 +192,118 @@ namespace Content.Server.Atmos.EntitySystems base.Update(frameTime); AccumulatedFrameTime += frameTime; - if (AccumulatedFrameTime < _updateCooldown) return; + if (AccumulatedFrameTime < _updateInterval) return; + AccumulatedFrameTime -= _updateInterval; - // 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. + var curTick = _gameTiming.CurTick; - // 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) - { - if (!_mapManager.TryGetGrid(gridId, out var grid)) - { - return; - } - - var gridEntityId = grid.GridEntityId; - - if (!EntityManager.TryGetComponent(gridEntityId, 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.ToArray()) - { - 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; - } - - tiles.Add(invalid); - chunk.Update(data, invalid); - } - } - - var currentTick = _gameTiming.CurTick; - - // Set the LastUpdate for chunks. - foreach (var (chunk, _) in updatedTiles) - { - chunk.Dirty(currentTick); - } + // First, update per-chunk visual data for any invalidated tiles. + UpdateOverlayData(curTick); // 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) + var xformQuery = GetEntityQuery(); + + foreach (var session in Filter.GetAllPlayers(_playerManager)) { - if (session.AttachedEntity is not {Valid: true} entity) continue; - - // Get chunks in range and update if we've moved around or the chunks have new overlay data - var chunksInRange = GetChunksInRange(entity); - 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<(Vector2i, GasOverlayData)>(); - clientInvalids[chunk.GridIndices] = existingData; - } - - chunk.GetData(existingData, invalids); - } - - foreach (var (grid, data) in clientInvalids) - { - RaiseNetworkEvent(overlay.UpdateClient(grid, data), session.ConnectedClient); - } + if (session is IPlayerSession { Status: SessionStatus.InGame } playerSession) + UpdatePlayer(playerSession, xformQuery, curTick); } - - // Cleanup - _invalidTiles.Clear(); } - private sealed class PlayerGasOverlay + + private void UpdatePlayer(IPlayerSession playerSession, EntityQuery xformQuery, GameTick curTick) { - private readonly Dictionary> _data = - new(); + var chunksInRange = _chunkingSys.GetChunksForSession(playerSession, ChunkSize, xformQuery, _chunkIndexPool, _chunkViewerPool); - private readonly Dictionary _lastSent = - new(); - - public GasOverlayMessage UpdateClient(EntityUid grid, List<(Vector2i, GasOverlayData)> data) + if (!_lastSentChunks.TryGetValue(playerSession, out var previouslySent)) { - return new(grid, data); + _lastSentChunks[playerSession] = previouslySent = _chunkViewerPool.Get(); + DebugTools.Assert(previouslySent.Count == 0); } - public void Reset() - { - _data.Clear(); - _lastSent.Clear(); - } + var ev = new GasOverlayUpdateEvent(); - public List GetKnownChunks() + foreach (var (grid, oldIndices) in previouslySent) { - var known = new List(); - - foreach (var (_, chunks) in _data) + // Mark the whole grid as stale and flag for removal. + if (!chunksInRange.TryGetValue(grid, out var chunks)) { - foreach (var (_, chunk) in chunks) + previouslySent.Remove(grid); + + // If grid was deleted then don't worry about sending it to the client. + if (_mapManager.IsGrid(grid)) + ev.RemovedChunks[grid] = oldIndices; + else { - known.Add(chunk); + oldIndices.Clear(); + _chunkIndexPool.Return(oldIndices); } + + continue; } - return known; + var old = _chunkIndexPool.Get(); + DebugTools.Assert(old.Count == 0); + foreach (var chunk in oldIndices) + { + if (!chunks.Contains(chunk)) + old.Add(chunk); + } + + if (old.Count == 0) + _chunkIndexPool.Return(old); + else + ev.RemovedChunks.Add(grid, old); } - public GasOverlayMessage? AddChunk(GameTick currentTick, GasOverlayChunk chunk) + foreach (var (grid, gridChunks) in chunksInRange) { - if (!_data.TryGetValue(chunk.GridIndices, out var chunks)) + // Not all grids have atmospheres. + if (!_overlay.TryGetValue(grid, out var gridData)) + continue; + + List dataToSend = new(); + ev.UpdatedChunks[grid] = dataToSend; + + previouslySent.TryGetValue(grid, out var previousChunks); + + foreach (var index in gridChunks) { - chunks = new Dictionary(); - _data[chunk.GridIndices] = chunks; + if (!gridData.TryGetValue(index, out var value)) + continue; + + if (previousChunks != null && + previousChunks.Contains(index) && + value.LastUpdate != curTick) + continue; + + dataToSend.Add(value); } - if (_lastSent.TryGetValue(chunk, out var last) && last >= chunk.LastUpdate) + previouslySent[grid] = gridChunks; + if (previousChunks != null) { - 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.Vector2i)) - { - chunks.Remove(chunk.Vector2i); + previousChunks.Clear(); + _chunkIndexPool.Return(previousChunks); } } - /// - /// 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<(Vector2i, 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 Vector2i(chunk.Vector2i.X + x, chunk.Vector2i.Y + y); - tileData.Add((indices, data)); - } - } - - if (tileData.Count == 0) - { - return null; - } - - return new GasOverlayMessage(chunk.GridIndices, tileData); - } + RaiseNetworkEvent(ev, playerSession.ConnectedClient); + ReturnToPool(ev.RemovedChunks); } - public void Reset(RoundRestartCleanupEvent ev) + public override void Reset(RoundRestartCleanupEvent ev) { _invalidTiles.Clear(); _overlay.Clear(); - foreach (var (_, data) in _knownPlayerChunks) + foreach (var data in _lastSentChunks.Values) { - data.Reset(); + ReturnToPool(data); } + + _lastSentChunks.Clear(); } } } diff --git a/Content.Server/Chunking/ChunkingSystem.cs b/Content.Server/Chunking/ChunkingSystem.cs new file mode 100644 index 0000000000..dc793a3884 --- /dev/null +++ b/Content.Server/Chunking/ChunkingSystem.cs @@ -0,0 +1,104 @@ +using Content.Shared.Decals; +using Microsoft.Extensions.ObjectPool; +using Robust.Server.Player; +using Robust.Shared; +using Robust.Shared.Configuration; +using Robust.Shared.Enums; +using Robust.Shared.Map; +using Robust.Shared.Utility; + +namespace Content.Shared.Chunking; + +/// +/// This system just exists to provide some utility functions for other systems that chunk data that needs to be +/// sent to players. In particular, see . +/// +public sealed class ChunkingSystem : EntitySystem +{ + [Dependency] private readonly IConfigurationManager _configurationManager = default!; + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + private Box2 _baseViewBounds; + + public override void Initialize() + { + base.Initialize(); + _configurationManager.OnValueChanged(CVars.NetMaxUpdateRange, OnPvsRangeChanged, true); + } + + public override void Shutdown() + { + base.Shutdown(); + _configurationManager.UnsubValueChanged(CVars.NetMaxUpdateRange, OnPvsRangeChanged); + } + + private void OnPvsRangeChanged(float value) => _baseViewBounds = Box2.UnitCentered.Scale(value); + + public Dictionary> GetChunksForSession( + IPlayerSession session, + int chunkSize, + EntityQuery xformQuery, + ObjectPool> indexPool, + ObjectPool>> viewerPool, + float? viewEnlargement = null) + { + var viewers = GetSessionViewers(session); + var chunks = GetChunksForViewers(viewers, chunkSize, indexPool, viewerPool, viewEnlargement ?? chunkSize, xformQuery); + return chunks; + } + + private HashSet GetSessionViewers(IPlayerSession session) + { + var viewers = new HashSet(); + if (session.Status != SessionStatus.InGame || session.AttachedEntity is null) + return viewers; + + viewers.Add(session.AttachedEntity.Value); + + foreach (var uid in session.ViewSubscriptions) + { + viewers.Add(uid); + } + + return viewers; + } + + private Dictionary> GetChunksForViewers( + HashSet viewers, + int chunkSize, + ObjectPool> indexPool, + ObjectPool>> viewerPool, + float viewEnlargement, + EntityQuery xformQuery) + { + Dictionary> chunks = viewerPool.Get(); + DebugTools.Assert(chunks.Count == 0); + + foreach (var viewerUid in viewers) + { + var xform = xformQuery.GetComponent(viewerUid); + var pos = _transform.GetWorldPosition(xform, xformQuery); + var bounds = _baseViewBounds.Translated(pos).Enlarged(viewEnlargement); + + foreach (var grid in _mapManager.FindGridsIntersecting(xform.MapID, bounds, true)) + { + if (!chunks.TryGetValue(grid.GridEntityId, out var set)) + { + chunks[grid.GridEntityId] = set = indexPool.Get(); + DebugTools.Assert(set.Count == 0); + } + + var enumerator = new ChunkIndicesEnumerator(_transform.GetInvWorldMatrix(grid.GridEntityId, xformQuery).TransformBox(bounds), chunkSize); + + while (enumerator.MoveNext(out var indices)) + { + set.Add(indices.Value); + } + } + } + + return chunks; + } +} + diff --git a/Content.Server/Decals/DecalSystem.cs b/Content.Server/Decals/DecalSystem.cs index b26a9323c5..ff5a00b704 100644 --- a/Content.Server/Decals/DecalSystem.cs +++ b/Content.Server/Decals/DecalSystem.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using Content.Server.Administration.Managers; using Content.Shared.Administration; +using Content.Shared.Chunking; using Content.Shared.Decals; using Content.Shared.Maps; using Microsoft.Extensions.ObjectPool; @@ -18,6 +19,7 @@ namespace Content.Server.Decals [Dependency] private readonly IAdminManager _adminManager = default!; [Dependency] private readonly ITileDefinitionManager _tileDefMan = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly ChunkingSystem _chunking = default!; private readonly Dictionary> _dirtyChunks = new(); private readonly Dictionary>> _previousSentChunks = new(); @@ -31,9 +33,6 @@ namespace Content.Server.Decals new DefaultObjectPool>>( new DefaultPooledObjectPolicy>>(), 64); - // Pool if we ever parallelise. - private HashSet _viewers = new(64); - public override void Initialize() { base.Initialize(); @@ -397,12 +396,14 @@ namespace Content.Server.Decals { base.Update(frameTime); + var xformQuery = GetEntityQuery(); + foreach (var session in Filter.GetAllPlayers(_playerManager)) { if (session is not IPlayerSession { Status: SessionStatus.InGame } playerSession) continue; - var chunksInRange = GetChunksForSession(playerSession); + var chunksInRange = _chunking.GetChunksForSession(playerSession, ChunkSize, xformQuery, _chunkIndexPool, _chunkViewerPool); var staleChunks = _chunkViewerPool.Get(); var previouslySent = _previousSentChunks[playerSession]; @@ -523,54 +524,5 @@ namespace Content.Server.Decals ReturnToPool(updatedChunks); ReturnToPool(staleChunks); } - - private HashSet GetSessionViewers(IPlayerSession session) - { - var viewers = _viewers; - if (session.Status != SessionStatus.InGame || session.AttachedEntity is null) - return viewers; - - viewers.Add(session.AttachedEntity.Value); - - foreach (var uid in session.ViewSubscriptions) - { - viewers.Add(uid); - } - - return viewers; - } - - private Dictionary> GetChunksForSession(IPlayerSession session) - { - var viewers = GetSessionViewers(session); - var chunks = GetChunksForViewers(viewers); - viewers.Clear(); - return chunks; - } - - private Dictionary> GetChunksForViewers(HashSet viewers) - { - var chunks = _chunkViewerPool.Get(); - var xformQuery = GetEntityQuery(); - - foreach (var viewerUid in viewers) - { - var (bounds, mapId) = CalcViewBounds(viewerUid, xformQuery.GetComponent(viewerUid)); - - foreach (var grid in MapManager.FindGridsIntersecting(mapId, bounds)) - { - if (!chunks.TryGetValue(grid.GridEntityId, out var chunk)) - chunks[grid.GridEntityId] = chunk = _chunkIndexPool.Get(); - - var enumerator = new ChunkIndicesEnumerator(_transform.GetInvWorldMatrix(grid.GridEntityId, xformQuery).TransformBox(bounds), ChunkSize); - - while (enumerator.MoveNext(out var indices)) - { - chunk.Add(indices.Value); - } - } - } - return chunks; - } } } diff --git a/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.cs b/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.cs index a7f2c8af33..ced9cdcfe8 100644 --- a/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.cs +++ b/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.cs @@ -1,4 +1,4 @@ -using Content.Shared.Atmos.Prototypes; +using Content.Shared.Atmos.Prototypes; using Robust.Shared.Prototypes; using Robust.Shared.Utility; @@ -10,22 +10,13 @@ namespace Content.Shared.Atmos.EntitySystems protected readonly GasPrototype[] GasPrototypes = new GasPrototype[Atmospherics.TotalNumberOfGases]; - private readonly SpriteSpecifier?[] _gasOverlays = new SpriteSpecifier[Atmospherics.TotalNumberOfGases]; - public override void Initialize() { base.Initialize(); for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++) { - var gasPrototype = _prototypeManager.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); + GasPrototypes[i] = _prototypeManager.Index(i.ToString()); } } @@ -34,7 +25,5 @@ namespace Content.Shared.Atmos.EntitySystems public GasPrototype GetGas(Gas gasId) => GasPrototypes[(int) gasId]; public IEnumerable Gases => GasPrototypes; - - public SpriteSpecifier? GetOverlay(int overlayId) => _gasOverlays[overlayId]; } } diff --git a/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs b/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs index b79fef0534..716520209b 100644 --- a/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs +++ b/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs @@ -1,4 +1,6 @@ -using Robust.Shared.Map; +using Content.Shared.Atmos.Prototypes; +using Content.Shared.GameTicking; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; namespace Content.Shared.Atmos.EntitySystems @@ -8,91 +10,74 @@ namespace Content.Shared.Atmos.EntitySystems public const byte ChunkSize = 8; protected float AccumulatedFrameTime; - public static Vector2i GetGasChunkIndices(Vector2i indices) + [Dependency] protected readonly IPrototypeManager ProtoMan = default!; + + /// + /// array of the ids of all visible gases. + /// + public int[] VisibleGasId = default!; + + public override void Initialize() { - return new((int) Math.Floor((float) indices.X / ChunkSize) * ChunkSize, (int) MathF.Floor((float) indices.Y / ChunkSize) * ChunkSize); + base.Initialize(); + + SubscribeLocalEvent(Reset); + + List visibleGases = new(); + + for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++) + { + var gasPrototype = ProtoMan.Index(i.ToString()); + if (!string.IsNullOrEmpty(gasPrototype.GasOverlayTexture) || !string.IsNullOrEmpty(gasPrototype.GasOverlaySprite) && !string.IsNullOrEmpty(gasPrototype.GasOverlayState)) + visibleGases.Add(i); + } + + VisibleGasId = visibleGases.ToArray(); } - [Serializable, NetSerializable] - public readonly struct GasData : IEquatable + public abstract void Reset(RoundRestartCleanupEvent ev); + + public static Vector2i GetGasChunkIndices(Vector2i indices) { - public readonly byte Index; - public readonly byte Opacity; - - public GasData(byte gasId, byte opacity) - { - Index = gasId; - Opacity = opacity; - } - - public override int GetHashCode() - { - return HashCode.Combine(Index, Opacity); - } - - public bool Equals(GasData other) - { - return other.Index == Index && other.Opacity == Opacity; - } + return new((int) MathF.Floor((float) indices.X / ChunkSize), (int) MathF.Floor((float) indices.Y / ChunkSize)); } [Serializable, NetSerializable] public readonly struct GasOverlayData : IEquatable { public readonly byte FireState; - public readonly float FireTemperature; - public readonly GasData[]? Gas; - public readonly int HashCode; + public readonly byte[] Opacity; - public GasOverlayData(byte fireState, float fireTemperature, GasData[] gas) + // TODO change fire color based on temps + // But also: dont dirty on a 0.01 kelvin change in temperatures. + // Either have a temp tolerance, or map temperature -> byte levels + + public GasOverlayData(byte fireState, byte[] opacity) { FireState = fireState; - FireTemperature = fireTemperature; - Gas = gas; - - Array.Sort(Gas, (a, b) => a.Index.CompareTo(b.Index)); - - var hash = new HashCode(); - hash.Add(FireState); - hash.Add(FireTemperature); - - foreach (var gasData in Gas) - { - hash.Add(gasData); - } - - HashCode = hash.ToHashCode(); - } - - public override int GetHashCode() - { - return HashCode; + Opacity = opacity; } public bool Equals(GasOverlayData other) { - // If you revert this then you need to make sure the hash comparison between - // our Gas[] and the other.Gas[] works. - return HashCode == other.HashCode; + if (FireState != other.FireState) + return false; + + for (var i = 0; i < Opacity.Length; i++) + { + if (Opacity[i] != other.Opacity[i]) + 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 : EntityEventArgs + public sealed class GasOverlayUpdateEvent : EntityEventArgs { - public EntityUid GridId { get; } - - public List<(Vector2i, GasOverlayData)> OverlayData { get; } - - public GasOverlayMessage(EntityUid gridIndices, List<(Vector2i,GasOverlayData)> overlayData) - { - GridId = gridIndices; - OverlayData = overlayData; - } + public Dictionary> UpdatedChunks = new(); + public Dictionary> RemovedChunks = new(); } } } diff --git a/Content.Shared/Atmos/GasOverlayChunk.cs b/Content.Shared/Atmos/GasOverlayChunk.cs index 6dbbdc26af..f5a8883e5e 100644 --- a/Content.Shared/Atmos/GasOverlayChunk.cs +++ b/Content.Shared/Atmos/GasOverlayChunk.cs @@ -1,99 +1,97 @@ using Content.Shared.Atmos.EntitySystems; -using Robust.Shared.Map; +using Robust.Shared.Serialization; using Robust.Shared.Timing; using Robust.Shared.Utility; +using System.Diagnostics.CodeAnalysis; +using static Content.Shared.Atmos.EntitySystems.SharedGasTileOverlaySystem; namespace Content.Shared.Atmos { + [Serializable, NetSerializable] + [Access(typeof(SharedGasTileOverlaySystem))] public sealed class GasOverlayChunk { /// - /// Grid for this chunk + /// The index of this chunk /// - public EntityUid GridIndices { get; } + public readonly Vector2i Index; + public readonly Vector2i Origin; - /// - /// Origin of this chunk - /// - public Vector2i Vector2i { get; } + public GasOverlayData?[][] TileData = new GasOverlayData?[ChunkSize][]; - public SharedGasTileOverlaySystem.GasOverlayData[,] TileData = new SharedGasTileOverlaySystem.GasOverlayData[SharedGasTileOverlaySystem.ChunkSize, SharedGasTileOverlaySystem.ChunkSize]; + [NonSerialized] + public GameTick LastUpdate; - public GameTick LastUpdate { get; private set; } - - public GasOverlayChunk(EntityUid gridIndices, Vector2i vector2i) + public GasOverlayChunk(Vector2i index) { - GridIndices = gridIndices; - Vector2i = vector2i; - } + Index = index; + Origin = Index * ChunkSize; - public void Dirty(GameTick currentTick) - { - LastUpdate = currentTick; - } - - /// - /// Flags Dirty if the data is different. - /// - /// - /// - public void Update(SharedGasTileOverlaySystem.GasOverlayData data, Vector2i indices) - { - DebugTools.Assert(InBounds(indices)); - var (offsetX, offsetY) = (indices.X - Vector2i.X, - indices.Y - Vector2i.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 whatever reason, net serialize does not like multi_D arrays. So Jagged it is. + for (var i = 0; i < ChunkSize; i++) { - for (var y = 0; y < SharedGasTileOverlaySystem.ChunkSize; y++) + TileData[i] = new GasOverlayData?[ChunkSize]; + } + } + + public GasOverlayData? GetData(Vector2i gridIndices) + { + DebugTools.Assert(InBounds(gridIndices)); + return TileData[gridIndices.X - Origin.X][gridIndices.Y - Origin.Y]; + } + + public GasOverlayData? SetData(Vector2i gridIndices, GasOverlayData? data) + { + DebugTools.Assert(InBounds(gridIndices)); + return TileData[gridIndices.X - Origin.X][gridIndices.Y - Origin.Y] = data; + } + + private bool InBounds(Vector2i gridIndices) + { + return gridIndices.X >= Origin.X && + gridIndices.Y >= Origin.Y && + gridIndices.X < Origin.X + ChunkSize && + gridIndices.Y < Origin.Y + ChunkSize; + } + } + + public struct GasChunkEnumerator + { + private GasOverlayChunk _chunk; + public int X = 0; + public int Y = -1; + private GasOverlayData?[] _column; + + + public GasChunkEnumerator(GasOverlayChunk chunk) + { + _chunk = chunk; + _column = _chunk.TileData[0]; + } + + public bool MoveNext([NotNullWhen(true)] out GasOverlayData? gas) + { + while (X < ChunkSize) + { + // We want to increment Y before returning, but we also want it to match the current Y coordinate for + // the returned gas, so using a slightly different logic for the Y loop. + while (Y < ChunkSize - 1) { - yield return TileData[x, y]; + Y++; + gas = _column[Y]; + + if (gas != null) + return true; } + + X++; + if (X < ChunkSize) + _column = _chunk.TileData[X]; + Y = -1; } - } - public void GetData(List<(Vector2i, 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 Vector2i(Vector2i.X + x, Vector2i.Y + y); - } - } - } - - public SharedGasTileOverlaySystem.GasOverlayData GetData(Vector2i indices) - { - DebugTools.Assert(InBounds(indices)); - return TileData[indices.X - Vector2i.X, indices.Y - Vector2i.Y]; - } - - private bool InBounds(Vector2i indices) - { - if (indices.X < Vector2i.X || indices.Y < Vector2i.Y) return false; - if (indices.X >= Vector2i.X + SharedGasTileOverlaySystem.ChunkSize || indices.Y >= Vector2i.Y + SharedGasTileOverlaySystem.ChunkSize) return false; - return true; + gas = null; + return false; } } } diff --git a/Content.Shared/Decals/SharedDecalSystem.cs b/Content.Shared/Decals/SharedDecalSystem.cs index c593b0fe9c..b0facbd052 100644 --- a/Content.Shared/Decals/SharedDecalSystem.cs +++ b/Content.Shared/Decals/SharedDecalSystem.cs @@ -94,14 +94,6 @@ namespace Content.Shared.Decals } protected virtual bool RemoveDecalHook(EntityUid gridId, uint uid) => true; - - protected (Box2 view, MapId mapId) CalcViewBounds(in EntityUid euid, TransformComponent xform) - { - var view = Box2.UnitCentered.Scale(_viewSize).Translated(xform.WorldPosition); - var map = xform.MapID; - - return (view, map); - } } // TODO: Pretty sure paul was moving this somewhere but just so people know