Gas overlay chunking (#1678)

Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
This commit is contained in:
metalgearsloth
2020-08-18 00:12:21 +10:00
committed by GitHub
parent 0e6f55a23d
commit f54ba4b6d5
16 changed files with 786 additions and 306 deletions

View File

@@ -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))
{

View File

@@ -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;
}
}
}
}

View File

@@ -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<float, Color> _fireCache = new Dictionary<float, Color>();
@@ -37,18 +44,19 @@ namespace Content.Client.GameObjects.EntitySystems
private readonly int[] _fireFrameCounter = new int[FireStates];
private readonly Texture[][] _fireFrames = new Texture[FireStates][];
private Dictionary<GridId, Dictionary<MapIndices, GasOverlayData>> _overlay = new Dictionary<GridId, Dictionary<MapIndices, GasOverlayData>>();
private Dictionary<GridId, Dictionary<MapIndices, GasOverlayChunk>> _tileData =
new Dictionary<GridId, Dictionary<MapIndices, GasOverlayChunk>>();
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent(new EntityEventHandler<GasTileOverlayMessage>(OnTileOverlayMessage));
SubscribeNetworkEvent<GasOverlayMessage>(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<RSIResource>(animated.RsiPath).RSI;
@@ -82,11 +90,75 @@ namespace Content.Client.GameObjects.EntitySystems
_fireFrameDelays[i] = state.GetDelays();
_fireFrameCounter[i] = 0;
}
var overlayManager = IoCManager.Resolve<IOverlayManager>();
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<MapIndices, GasOverlayChunk>();
_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<IOverlayManager>();
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;
@@ -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<MapIndices, GasOverlayData>();
_overlay.Add(data.GridIndex, gridOverlays);
}
gridOverlays[data.GridIndices] = data.Data;
}
}
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);

View File

@@ -23,7 +23,7 @@ namespace Content.Server.Atmos
/// State for the fire sprite.
/// </summary>
[ViewVariables]
public int State;
public byte State;
public void Start()
{

View File

@@ -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)

View File

@@ -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!;
/// <summary>
/// The tiles that have had their atmos data updated since last tick
/// </summary>
private Dictionary<GridId, HashSet<MapIndices>> _invalidTiles = new Dictionary<GridId, HashSet<MapIndices>>();
private Dictionary<IPlayerSession, PlayerGasOverlay> _knownPlayerChunks =
new Dictionary<IPlayerSession, PlayerGasOverlay>();
/// <summary>
/// Gas data stored in chunks to make PVS / bubbling easier.
/// </summary>
private Dictionary<GridId, Dictionary<MapIndices, GasOverlayChunk>> _overlay =
new Dictionary<GridId, Dictionary<MapIndices, GasOverlayChunk>>();
/// <summary>
/// How far away do we update gas overlays (minimum; due to chunking further away tiles may also be updated).
/// </summary>
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;
/// <summary>
/// Overlay update ticks per second.
/// </summary>
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<MapIndices>();
_invalidTiles[gridIndex] = existing;
}
existing.Add(indices);
}
private GasOverlayChunk GetOrCreateChunk(GridId gridIndex, MapIndices indices)
{
if (!_overlay.TryGetValue(gridIndex, out var chunks))
{
chunks = new Dictionary<MapIndices, GasOverlayChunk>();
_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();
}
}
/// <summary>
/// Checks whether the overlay-relevant data for a gas tile has been updated.
/// </summary>
/// <param name="gam"></param>
/// <param name="oldTile"></param>
/// <param name="indices"></param>
/// <param name="overlayData"></param>
/// <returns>true if updated</returns>
private bool TryRefreshTile(GridAtmosphereComponent gam, GasOverlayData oldTile, MapIndices indices, out GasOverlayData overlayData)
{
var tile = gam.GetTile(indices);
var tileData = new List<GasData>();
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;
}
/// <summary>
/// Get every chunk in range of our entity that exists, including on other grids.
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
private List<GasOverlayChunk> GetChunksInRange(IEntity entity)
{
var inRange = new List<GasOverlayChunk>();
// 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<float>("net.gasoverlaytickrate");
if (AccumulatedFrameTime < _updateCooldown)
{
return;
}
_updateRange = _configManager.GetCVar<float>("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<GridId, GridAtmosphereComponent>();
var updatedTiles = new Dictionary<GasOverlayChunk, HashSet<MapIndices>>();
// 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<MapIndices>();
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<GasOverlayChunk>();
var chunksToAdd = new List<GasOverlayChunk>();
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<GridId, List<(MapIndices, GasOverlayData)>>();
// 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<GridId, Dictionary<MapIndices, GasOverlayChunk>> _data =
new Dictionary<GridId, Dictionary<MapIndices, GasOverlayChunk>>();
private readonly Dictionary<GasOverlayChunk, GameTick> _lastSent =
new Dictionary<GasOverlayChunk, GameTick>();
public GasOverlayMessage UpdateClient(GridId grid, List<(MapIndices, GasOverlayData)> data)
{
return new GasOverlayMessage(grid, data);
}
public void Reset()
{
_data.Clear();
_lastSent.Clear();
}
public List<GasOverlayChunk> GetKnownChunks()
{
var known = new List<GasOverlayChunk>();
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<MapIndices, GasOverlayChunk>();
_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);
}
}
/// <summary>
/// Retrieve a whole chunk as a message, only getting the relevant tiles for the gas overlay.
/// </summary>
/// <param name="chunk"></param>
/// <returns></returns>
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);
}
}
}
}

View File

@@ -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<GasTileOverlayData> _queue = new HashSet<GasTileOverlayData>();
private Dictionary<GridId, HashSet<MapIndices>> _invalid = new Dictionary<GridId, HashSet<MapIndices>>();
private Dictionary<GridId, Dictionary<MapIndices, GasOverlayData>> _overlay =
new Dictionary<GridId, Dictionary<MapIndices, GasOverlayData>>();
[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<MapIndices>();
_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<MapIndices, GasOverlayData>();
_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<GasTileOverlayData>();
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<IMapManager>();
var entityMan = IoCManager.Resolve<IEntityManager>();
var list = new List<GasData>();
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();
}
}
}

View File

@@ -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;
@@ -634,6 +635,7 @@ namespace Content.Server.GameTicking
_playerJoinLobby(player);
}
EntitySystem.Get<GasTileOverlaySystem>().ResettingCleanup();
EntitySystem.Get<PathfindingSystem>().ResettingCleanup();
EntitySystem.Get<AiReachableSystem>().ResettingCleanup();
EntitySystem.Get<WireHackingSystem>().ResetLayouts();

View File

@@ -19,7 +19,6 @@
"Marker",
"EmergencyLight",
"Clickable",
"CanSeeGases",
"RadiatingLight",
};

View File

@@ -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<IPrototypeManager>();
GasPrototypes = new GasPrototype[TotalNumberOfGases];
GasOverlays = new SpriteSpecifier[TotalNumberOfGases];
for (var i = 0; i < TotalNumberOfGases; i++)
{
GasPrototypes[i] = protoMan.Index<GasPrototype>(i.ToString());
var gasPrototype = protoMan.Index<GasPrototype>(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<GasPrototype> Gases => GasPrototypes;
private static readonly SpriteSpecifier[] GasOverlays;
public static SpriteSpecifier GetOverlay(int overlayId) => GasOverlays[overlayId];
#region ATMOS
/// <summary>
/// The universal gas constant, in kPa*L/(K*mol)
@@ -166,7 +179,7 @@ namespace Content.Shared.Atmos
/// <summary>
/// Total number of gases. Increase this if you want to add more!
/// </summary>
public const int TotalNumberOfGases = 6;
public const byte TotalNumberOfGases = 6;
/// <summary>
/// Amount of heat released per mole of burnt hydrogen or tritium (hydrogen isotope)

View File

@@ -45,23 +45,6 @@ namespace Content.Shared.Atmos
/// </summary>
public string GasOverlaySprite { get; set; }
/// <summary>
/// Sprite specifier for the gas overlay.
/// </summary>
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;
}
}
/// <summary>
/// Path to the tile overlay used when this gas appears visible.
/// </summary>

View File

@@ -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
{
/// <summary>
/// Grid for this chunk
/// </summary>
public GridId GridIndices { get; }
/// <summary>
/// Origin of this chunk
/// </summary>
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;
}
/// <summary>
/// Flags Dirty if the data is different.
/// </summary>
/// <param name="data"></param>
/// <param name="indices"></param>
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<SharedGasTileOverlaySystem.GasOverlayData> 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<MapIndices> indices)
{
foreach (var index in indices)
{
existingData.Add((index, GetData(index)));
}
}
public IEnumerable<MapIndices> 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;
}
}
}

View File

@@ -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<GasOverlayData>
{
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;
}
}
/// <summary>
/// Invalid tiles for the gas overlay.
/// No point re-sending every tile if only a subset might have been updated.
/// </summary>
[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;
}
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -22,7 +22,6 @@
DoRangeCheck: false
- type: IgnorePause
- type: Ghost
- type: CanSeeGases
- type: Sprite
netsync: false
drawdepth: Ghosts

View File

@@ -154,7 +154,6 @@
- type: Grammar
proper: true
- type: Pullable
- type: CanSeeGases
- type: DoAfter
- type: Strippable
- type: UserInterface