GasTileOverlaySystem.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. using System.Linq;
  2. using System.Runtime.CompilerServices;
  3. using System.Threading.Tasks;
  4. using Content.Server.Atmos.Components;
  5. using Content.Shared.Atmos;
  6. using Content.Shared.Atmos.Components;
  7. using Content.Shared.Atmos.EntitySystems;
  8. using Content.Shared.CCVar;
  9. using Content.Shared.Chunking;
  10. using Content.Shared.GameTicking;
  11. using Content.Shared.Rounding;
  12. using JetBrains.Annotations;
  13. using Microsoft.Extensions.ObjectPool;
  14. using Robust.Server.Player;
  15. using Robust.Shared;
  16. using Robust.Shared.Configuration;
  17. using Robust.Shared.Enums;
  18. using Robust.Shared.Map;
  19. using Robust.Shared.Player;
  20. using Robust.Shared.Threading;
  21. using Robust.Shared.Timing;
  22. using Robust.Shared.Utility;
  23. // ReSharper disable once RedundantUsingDirective
  24. namespace Content.Server.Atmos.EntitySystems
  25. {
  26. [UsedImplicitly]
  27. public sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem
  28. {
  29. [Robust.Shared.IoC.Dependency] private readonly IGameTiming _gameTiming = default!;
  30. [Robust.Shared.IoC.Dependency] private readonly IPlayerManager _playerManager = default!;
  31. [Robust.Shared.IoC.Dependency] private readonly IMapManager _mapManager = default!;
  32. [Robust.Shared.IoC.Dependency] private readonly IConfigurationManager _confMan = default!;
  33. [Robust.Shared.IoC.Dependency] private readonly IParallelManager _parMan = default!;
  34. [Robust.Shared.IoC.Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
  35. [Robust.Shared.IoC.Dependency] private readonly ChunkingSystem _chunkingSys = default!;
  36. /// <summary>
  37. /// Per-tick cache of sessions.
  38. /// </summary>
  39. private readonly List<ICommonSession> _sessions = new();
  40. private UpdatePlayerJob _updateJob;
  41. private readonly Dictionary<ICommonSession, Dictionary<NetEntity, HashSet<Vector2i>>> _lastSentChunks = new();
  42. // Oh look its more duplicated decal system code!
  43. private ObjectPool<HashSet<Vector2i>> _chunkIndexPool =
  44. new DefaultObjectPool<HashSet<Vector2i>>(
  45. new DefaultPooledObjectPolicy<HashSet<Vector2i>>(), 64);
  46. private ObjectPool<Dictionary<NetEntity, HashSet<Vector2i>>> _chunkViewerPool =
  47. new DefaultObjectPool<Dictionary<NetEntity, HashSet<Vector2i>>>(
  48. new DefaultPooledObjectPolicy<Dictionary<NetEntity, HashSet<Vector2i>>>(), 64);
  49. private bool _doSessionUpdate;
  50. /// <summary>
  51. /// Overlay update interval, in seconds.
  52. /// </summary>
  53. private float _updateInterval;
  54. private int _thresholds;
  55. private EntityQuery<GasTileOverlayComponent> _query;
  56. public override void Initialize()
  57. {
  58. base.Initialize();
  59. _updateJob = new UpdatePlayerJob()
  60. {
  61. EntManager = EntityManager,
  62. System = this,
  63. ChunkIndexPool = _chunkIndexPool,
  64. Sessions = _sessions,
  65. ChunkingSys = _chunkingSys,
  66. MapManager = _mapManager,
  67. ChunkViewerPool = _chunkViewerPool,
  68. LastSentChunks = _lastSentChunks,
  69. };
  70. _playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
  71. Subs.CVar(_confMan, CCVars.NetGasOverlayTickRate, UpdateTickRate, true);
  72. Subs.CVar(_confMan, CCVars.GasOverlayThresholds, UpdateThresholds, true);
  73. Subs.CVar(_confMan, CVars.NetPVS, OnPvsToggle, true);
  74. SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
  75. SubscribeLocalEvent<GasTileOverlayComponent, ComponentStartup>(OnStartup);
  76. _query = GetEntityQuery<GasTileOverlayComponent>();
  77. }
  78. private void OnStartup(EntityUid uid, GasTileOverlayComponent component, ComponentStartup args)
  79. {
  80. // This **shouldn't** be required, but just in case we ever get entity prototypes that have gas overlays, we
  81. // need to ensure that we send an initial full state to players.
  82. Dirty(uid, component);
  83. }
  84. public override void Shutdown()
  85. {
  86. base.Shutdown();
  87. _playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
  88. }
  89. private void OnPvsToggle(bool value)
  90. {
  91. if (value == PvsEnabled)
  92. return;
  93. PvsEnabled = value;
  94. if (value)
  95. return;
  96. foreach (var lastSent in _lastSentChunks.Values)
  97. {
  98. foreach (var set in lastSent.Values)
  99. {
  100. set.Clear();
  101. _chunkIndexPool.Return(set);
  102. }
  103. lastSent.Clear();
  104. }
  105. // PVS was turned off, ensure data gets sent to all clients.
  106. var query = AllEntityQuery<GasTileOverlayComponent, MetaDataComponent>();
  107. while (query.MoveNext(out var uid, out var grid, out var meta))
  108. {
  109. grid.ForceTick = _gameTiming.CurTick;
  110. Dirty(uid, grid, meta);
  111. }
  112. }
  113. private void UpdateTickRate(float value) => _updateInterval = value > 0.0f ? 1 / value : float.MaxValue;
  114. private void UpdateThresholds(int value) => _thresholds = value;
  115. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  116. public void Invalidate(Entity<GasTileOverlayComponent?> grid, Vector2i index)
  117. {
  118. if (_query.Resolve(grid.Owner, ref grid.Comp))
  119. grid.Comp.InvalidTiles.Add(index);
  120. }
  121. private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
  122. {
  123. if (e.NewStatus != SessionStatus.InGame)
  124. {
  125. if (_lastSentChunks.Remove(e.Session, out var sets))
  126. {
  127. foreach (var set in sets.Values)
  128. {
  129. set.Clear();
  130. _chunkIndexPool.Return(set);
  131. }
  132. }
  133. }
  134. if (!_lastSentChunks.ContainsKey(e.Session))
  135. {
  136. _lastSentChunks[e.Session] = new();
  137. }
  138. }
  139. private byte GetOpacity(float moles, float molesVisible, float molesVisibleMax)
  140. {
  141. return (byte) (ContentHelpers.RoundToLevels(
  142. MathHelper.Clamp01((moles - molesVisible) /
  143. (molesVisibleMax - molesVisible)) * 255, byte.MaxValue,
  144. _thresholds) * 255 / (_thresholds - 1));
  145. }
  146. public GasOverlayData GetOverlayData(GasMixture? mixture)
  147. {
  148. var data = new GasOverlayData(0, new byte[VisibleGasId.Length]);
  149. for (var i = 0; i < VisibleGasId.Length; i++)
  150. {
  151. var id = VisibleGasId[i];
  152. var gas = _atmosphereSystem.GetGas(id);
  153. var moles = mixture?[id] ?? 0f;
  154. ref var opacity = ref data.Opacity[i];
  155. if (moles < gas.GasMolesVisible)
  156. {
  157. continue;
  158. }
  159. opacity = (byte) (ContentHelpers.RoundToLevels(
  160. MathHelper.Clamp01((moles - gas.GasMolesVisible) /
  161. (gas.GasMolesVisibleMax - gas.GasMolesVisible)) * 255, byte.MaxValue,
  162. _thresholds) * 255 / (_thresholds - 1));
  163. }
  164. return data;
  165. }
  166. /// <summary>
  167. /// Updates the visuals for a tile on some grid chunk. Returns true if the visuals have changed.
  168. /// </summary>
  169. private bool UpdateChunkTile(GridAtmosphereComponent gridAtmosphere, GasOverlayChunk chunk, Vector2i index)
  170. {
  171. ref var oldData = ref chunk.TileData[chunk.GetDataIndex(index)];
  172. if (!gridAtmosphere.Tiles.TryGetValue(index, out var tile))
  173. {
  174. if (oldData.Equals(default))
  175. return false;
  176. chunk.LastUpdate = _gameTiming.CurTick;
  177. oldData = default;
  178. return true;
  179. }
  180. var changed = false;
  181. if (oldData.Equals(default))
  182. {
  183. changed = true;
  184. oldData = new GasOverlayData(tile.Hotspot.State, new byte[VisibleGasId.Length]);
  185. }
  186. else if (oldData.FireState != tile.Hotspot.State)
  187. {
  188. changed = true;
  189. oldData = new GasOverlayData(tile.Hotspot.State, oldData.Opacity);
  190. }
  191. if (tile is {Air: not null, NoGridTile: false})
  192. {
  193. for (var i = 0; i < VisibleGasId.Length; i++)
  194. {
  195. var id = VisibleGasId[i];
  196. var gas = _atmosphereSystem.GetGas(id);
  197. var moles = tile.Air[id];
  198. ref var oldOpacity = ref oldData.Opacity[i];
  199. if (moles < gas.GasMolesVisible)
  200. {
  201. if (oldOpacity != 0)
  202. {
  203. oldOpacity = 0;
  204. changed = true;
  205. }
  206. continue;
  207. }
  208. var opacity = GetOpacity(moles, gas.GasMolesVisible, gas.GasMolesVisibleMax);
  209. if (oldOpacity == opacity)
  210. continue;
  211. oldOpacity = opacity;
  212. changed = true;
  213. }
  214. }
  215. else
  216. {
  217. for (var i = 0; i < VisibleGasId.Length; i++)
  218. {
  219. changed |= oldData.Opacity[i] != 0;
  220. oldData.Opacity[i] = 0;
  221. }
  222. }
  223. if (!changed)
  224. return false;
  225. chunk.LastUpdate = _gameTiming.CurTick;
  226. return true;
  227. }
  228. private void UpdateOverlayData()
  229. {
  230. // TODO parallelize?
  231. var query = AllEntityQuery<GasTileOverlayComponent, GridAtmosphereComponent, MetaDataComponent>();
  232. while (query.MoveNext(out var uid, out var overlay, out var gam, out var meta))
  233. {
  234. var changed = false;
  235. foreach (var index in overlay.InvalidTiles)
  236. {
  237. var chunkIndex = GetGasChunkIndices(index);
  238. if (!overlay.Chunks.TryGetValue(chunkIndex, out var chunk))
  239. overlay.Chunks[chunkIndex] = chunk = new GasOverlayChunk(chunkIndex);
  240. changed |= UpdateChunkTile(gam, chunk, index);
  241. }
  242. if (changed)
  243. Dirty(uid, overlay, meta);
  244. overlay.InvalidTiles.Clear();
  245. }
  246. }
  247. public override void Update(float frameTime)
  248. {
  249. base.Update(frameTime);
  250. AccumulatedFrameTime += frameTime;
  251. if (_doSessionUpdate)
  252. {
  253. UpdateSessions();
  254. return;
  255. }
  256. if (AccumulatedFrameTime < _updateInterval)
  257. return;
  258. AccumulatedFrameTime -= _updateInterval;
  259. // First, update per-chunk visual data for any invalidated tiles.
  260. UpdateOverlayData();
  261. // Then, next tick we send the data to players.
  262. // This is to avoid doing all the work in the same tick.
  263. _doSessionUpdate = true;
  264. }
  265. public void UpdateSessions()
  266. {
  267. _doSessionUpdate = false;
  268. if (!PvsEnabled)
  269. return;
  270. // Now we'll go through each player, then through each chunk in range of that player checking if the player is still in range
  271. // If they are, check if they need the new data to send (i.e. if there's an overlay for the gas).
  272. // Afterwards we reset all the chunk data for the next time we tick.
  273. _sessions.Clear();
  274. foreach (var player in _playerManager.Sessions)
  275. {
  276. if (player.Status != SessionStatus.InGame)
  277. continue;
  278. _sessions.Add(player);
  279. }
  280. if (_sessions.Count == 0)
  281. return;
  282. _parMan.ProcessNow(_updateJob, _sessions.Count);
  283. _updateJob.LastSessionUpdate = _gameTiming.CurTick;
  284. }
  285. public void Reset(RoundRestartCleanupEvent ev)
  286. {
  287. foreach (var data in _lastSentChunks.Values)
  288. {
  289. foreach (var previous in data.Values)
  290. {
  291. previous.Clear();
  292. _chunkIndexPool.Return(previous);
  293. }
  294. data.Clear();
  295. }
  296. }
  297. #region Jobs
  298. /// <summary>
  299. /// Updates per player gas overlay data.
  300. /// </summary>
  301. private record struct UpdatePlayerJob : IParallelRobustJob
  302. {
  303. public int BatchSize => 2;
  304. public IEntityManager EntManager;
  305. public IMapManager MapManager;
  306. public ChunkingSystem ChunkingSys;
  307. public GasTileOverlaySystem System;
  308. public ObjectPool<HashSet<Vector2i>> ChunkIndexPool;
  309. public ObjectPool<Dictionary<NetEntity, HashSet<Vector2i>>> ChunkViewerPool;
  310. public GameTick LastSessionUpdate;
  311. public Dictionary<ICommonSession, Dictionary<NetEntity, HashSet<Vector2i>>> LastSentChunks;
  312. public List<ICommonSession> Sessions;
  313. public void Execute(int index)
  314. {
  315. var playerSession = Sessions[index];
  316. var chunksInRange = ChunkingSys.GetChunksForSession(playerSession, ChunkSize, ChunkIndexPool, ChunkViewerPool);
  317. var previouslySent = LastSentChunks[playerSession];
  318. var ev = new GasOverlayUpdateEvent();
  319. foreach (var (netGrid, oldIndices) in previouslySent)
  320. {
  321. // Mark the whole grid as stale and flag for removal.
  322. if (!chunksInRange.TryGetValue(netGrid, out var chunks))
  323. {
  324. previouslySent.Remove(netGrid);
  325. // If grid was deleted then don't worry about sending it to the client.
  326. if (!EntManager.TryGetEntity(netGrid, out var gridId) || !MapManager.IsGrid(gridId.Value))
  327. ev.RemovedChunks[netGrid] = oldIndices;
  328. else
  329. {
  330. oldIndices.Clear();
  331. ChunkIndexPool.Return(oldIndices);
  332. }
  333. continue;
  334. }
  335. var old = ChunkIndexPool.Get();
  336. DebugTools.Assert(old.Count == 0);
  337. foreach (var chunk in oldIndices)
  338. {
  339. if (!chunks.Contains(chunk))
  340. old.Add(chunk);
  341. }
  342. if (old.Count == 0)
  343. ChunkIndexPool.Return(old);
  344. else
  345. ev.RemovedChunks.Add(netGrid, old);
  346. }
  347. foreach (var (netGrid, gridChunks) in chunksInRange)
  348. {
  349. // Not all grids have atmospheres.
  350. if (!EntManager.TryGetEntity(netGrid, out var grid) || !EntManager.TryGetComponent(grid, out GasTileOverlayComponent? overlay))
  351. continue;
  352. List<GasOverlayChunk> dataToSend = new();
  353. ev.UpdatedChunks[netGrid] = dataToSend;
  354. previouslySent.TryGetValue(netGrid, out var previousChunks);
  355. foreach (var gIndex in gridChunks)
  356. {
  357. if (!overlay.Chunks.TryGetValue(gIndex, out var value))
  358. continue;
  359. // If the chunk was updated since we last sent it, send it again
  360. if (value.LastUpdate > LastSessionUpdate)
  361. {
  362. dataToSend.Add(value);
  363. continue;
  364. }
  365. // Always send it if we didn't previously send it
  366. if (previousChunks == null || !previousChunks.Contains(gIndex))
  367. dataToSend.Add(value);
  368. }
  369. previouslySent[netGrid] = gridChunks;
  370. if (previousChunks != null)
  371. {
  372. previousChunks.Clear();
  373. ChunkIndexPool.Return(previousChunks);
  374. }
  375. }
  376. if (ev.UpdatedChunks.Count != 0 || ev.RemovedChunks.Count != 0)
  377. System.RaiseNetworkEvent(ev, playerSession.Channel);
  378. }
  379. }
  380. #endregion
  381. }
  382. }