Atmos debug helpers (#2108)

* Atmos debug overlay

* Pressure direction information

* Atmos debug overlay: show excited tiles
This commit is contained in:
20kdc
2020-09-21 00:13:17 +01:00
committed by GitHub
parent 0ea8792501
commit da463097f0
7 changed files with 416 additions and 1 deletions

View File

@@ -0,0 +1,107 @@
using Content.Client.GameObjects.EntitySystems;
using Content.Shared.GameObjects.EntitySystems.Atmos;
using Content.Shared.Atmos;
using Robust.Client.Graphics.ClientEye;
using Robust.Client.Graphics.Drawing;
using Robust.Client.Graphics.Overlays;
using Robust.Client.Interfaces.Graphics;
using Robust.Client.Interfaces.Graphics.ClientEye;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
namespace Content.Client.Atmos
{
public class AtmosDebugOverlay : Overlay
{
private readonly AtmosDebugOverlaySystem _atmosDebugOverlaySystem;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IClyde _clyde = default!;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public AtmosDebugOverlay() : base(nameof(AtmosDebugOverlay))
{
IoCManager.InjectDependencies(this);
_atmosDebugOverlaySystem = EntitySystem.Get<AtmosDebugOverlaySystem>();
}
protected override void Draw(DrawingHandleBase handle, OverlaySpace overlay)
{
var drawHandle = (DrawingHandleWorld) handle;
var mapId = _eyeManager.CurrentMap;
var eye = _eyeManager.CurrentEye;
var worldBounds = Box2.CenteredAround(eye.Position.Position,
_clyde.ScreenSize / (float) EyeManager.PixelsPerMeter * eye.Zoom);
// IF YOU ARE ABOUT TO INTRODUCE CHUNKING OR SOME OTHER OPTIMIZATION INTO THIS CODE:
// -- THINK! --
// 1. "Is this going to make a critical atmos debugging tool harder to debug itself?"
// 2. "Is this going to do anything that could cause the atmos debugging tool to use resources, server-side or client-side, when nobody's using it?"
// 3. "Is this going to make it harder for atmos programmers to add data that may not be chunk-friendly into the atmos debugger?"
// Nanotrasen needs YOU! to avoid premature optimization in critical debugging tools - 20kdc
foreach (var mapGrid in _mapManager.FindGridsIntersecting(mapId, worldBounds))
{
if (!_atmosDebugOverlaySystem.HasData(mapGrid.Index))
continue;
var gridBounds = new Box2(mapGrid.WorldToLocal(worldBounds.BottomLeft), mapGrid.WorldToLocal(worldBounds.TopRight));
for (var pass = 0; pass < 3; pass++)
{
foreach (var tile in mapGrid.GetTilesIntersecting(gridBounds))
{
var dataMaybeNull = _atmosDebugOverlaySystem.GetData(mapGrid.Index, tile.GridIndices);
if (dataMaybeNull != null)
{
var data = (SharedAtmosDebugOverlaySystem.AtmosDebugOverlayData) dataMaybeNull!;
if (pass == 0)
{
float total = 0;
foreach (float f in data.Moles)
{
total += f;
}
var interp = total / (Atmospherics.MolesCellStandard * 2);
var res = Color.InterpolateBetween(Color.Red, Color.Green, interp).WithAlpha(0.75f);
drawHandle.DrawRect(Box2.FromDimensions(mapGrid.LocalToWorld(new Vector2(tile.X, tile.Y)), new Vector2(1, 1)), res);
}
else if (pass == 1)
{
if (data.PressureDirection != AtmosDirection.Invalid)
{
var atmosAngle = data.PressureDirection.ToAngle();
var atmosAngleOfs = atmosAngle.ToVec() * 0.4f;
var tileCentre = new Vector2(tile.X + 0.5f, tile.Y + 0.5f);
var basisA = mapGrid.LocalToWorld(tileCentre);
var basisB = mapGrid.LocalToWorld(tileCentre + atmosAngleOfs);
drawHandle.DrawLine(basisA, basisB, Color.Blue);
}
}
else if (pass == 2)
{
if (data.InExcitedGroup)
{
var tilePos = new Vector2(tile.X, tile.Y);
var basisA = mapGrid.LocalToWorld(tilePos);
var basisB = mapGrid.LocalToWorld(tilePos + new Vector2(1.0f, 1.0f));
var basisC = mapGrid.LocalToWorld(tilePos + new Vector2(0.0f, 1.0f));
var basisD = mapGrid.LocalToWorld(tilePos + new Vector2(1.0f, 0.0f));
drawHandle.DrawLine(basisA, basisB, Color.Cyan);
drawHandle.DrawLine(basisC, basisD, Color.Cyan);
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,84 @@
#nullable enable
using System;
using System.Collections.Generic;
using Content.Client.Atmos;
using Content.Shared.Atmos;
using Content.Shared.GameObjects.EntitySystems.Atmos;
using JetBrains.Annotations;
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.Interfaces.Map;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Content.Client.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class AtmosDebugOverlaySystem : SharedAtmosDebugOverlaySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
private Dictionary<GridId, AtmosDebugOverlayMessage> _tileData =
new Dictionary<GridId, AtmosDebugOverlayMessage>();
private AtmosphereSystem _atmosphereSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<AtmosDebugOverlayMessage>(HandleAtmosDebugOverlayMessage);
_mapManager.OnGridRemoved += OnGridRemoved;
_atmosphereSystem = Get<AtmosphereSystem>();
var overlayManager = IoCManager.Resolve<IOverlayManager>();
if(!overlayManager.HasOverlay(nameof(AtmosDebugOverlay)))
overlayManager.AddOverlay(new AtmosDebugOverlay());
}
private void HandleAtmosDebugOverlayMessage(AtmosDebugOverlayMessage message)
{
_tileData[message.GridId] = message;
}
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 AtmosDebugOverlayData? GetData(GridId gridIndex, MapIndices indices)
{
if (!_tileData.TryGetValue(gridIndex, out var srcMsg))
return null;
var relative = indices - srcMsg.BaseIdx;
if (relative.X < 0 || relative.Y < 0 || relative.X >= LocalViewRange || relative.Y >= LocalViewRange)
return null;
return srcMsg.OverlayData[relative.X + (relative.Y * LocalViewRange)];
}
}
}

View File

@@ -2,6 +2,7 @@
using System; using System;
using Content.Server.GameObjects.Components.Atmos; using Content.Server.GameObjects.Components.Atmos;
using Content.Server.GameObjects.EntitySystems; using Content.Server.GameObjects.EntitySystems;
using Content.Server.GameObjects.EntitySystems.Atmos;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
@@ -621,4 +622,26 @@ namespace Content.Server.Atmos
shell.SendText(player, $"Removed {moles} moles of gas {gas} from {tiles} tiles."); shell.SendText(player, $"Removed {moles} moles of gas {gas} from {tiles} tiles.");
} }
} }
}
public class ShowAtmos : IClientCommand
{
public string Command => "showatmos";
public string Description => "Toggles seeing atmos debug overlay";
public string Help => $"Usage: {Command}";
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
{
if (player == null) return;
var atmosDebug = EntitySystem.Get<AtmosDebugOverlaySystem>();
if (atmosDebug.PlayerObservers.Contains(player))
{
atmosDebug.PlayerObservers.Remove(player);
shell.SendText(player, $"Ok, disabled");
}
else
{
atmosDebug.PlayerObservers.Add(player);
shell.SendText(player, $"Ok, enabled");
}
}
}}

View File

@@ -93,6 +93,9 @@ namespace Content.Server.Atmos
private AtmosDirection _pressureDirection; private AtmosDirection _pressureDirection;
// I'm assuming there's a good reason the original variable was made private, but this information is also important.
public AtmosDirection PressureDirectionForDebugOverlay => _pressureDirection;
[ViewVariables, UsedImplicitly] [ViewVariables, UsedImplicitly]
private int PressureDirectionInt => (int)_pressureDirection; private int PressureDirectionInt => (int)_pressureDirection;

View File

@@ -0,0 +1,141 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Content.Server.GameObjects.Components.Atmos;
using Content.Server.Atmos;
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.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
namespace Content.Server.GameObjects.EntitySystems.Atmos
{
[UsedImplicitly]
public sealed class AtmosDebugOverlaySystem : SharedAtmosDebugOverlaySystem
{
[Robust.Shared.IoC.Dependency] private readonly IGameTiming _gameTiming = default!;
[Robust.Shared.IoC.Dependency] private readonly IPlayerManager _playerManager = default!;
[Robust.Shared.IoC.Dependency] private readonly IEntityManager _entityManager = default!;
[Robust.Shared.IoC.Dependency] private readonly IMapManager _mapManager = default!;
[Robust.Shared.IoC.Dependency] private readonly IConfigurationManager _configManager = default!;
/// <summary>
/// Players allowed to see the atmos debug overlay
/// </summary>
public HashSet<IPlayerSession> PlayerObservers = new HashSet<IPlayerSession>();
/// <summary>
/// Overlay update ticks per second.
/// </summary>
private float _updateCooldown;
private AtmosphereSystem _atmosphereSystem = default!;
public override void Initialize()
{
base.Initialize();
_atmosphereSystem = Get<AtmosphereSystem>();
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
_configManager.RegisterCVar("net.atmosdbgoverlaytickrate", 3.0f);
}
public override void Shutdown()
{
base.Shutdown();
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
}
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.NewStatus != SessionStatus.InGame)
{
if (PlayerObservers.Contains(e.Session))
{
PlayerObservers.Remove(e.Session);
}
return;
}
}
private AtmosDebugOverlayData ConvertTileToData(TileAtmosphere? tile)
{
var gases = new float[Atmospherics.TotalNumberOfGases];
if (tile?.Air == null)
{
return new AtmosDebugOverlayData(0, gases, AtmosDirection.Invalid, false);
}
else
{
for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
{
gases[i] = tile.Air.GetMoles(i);
}
return new AtmosDebugOverlayData(tile.Air.Temperature, gases, tile.PressureDirectionForDebugOverlay, tile.ExcitedGroup != null);
}
}
public override void Update(float frameTime)
{
AccumulatedFrameTime += frameTime;
_updateCooldown = 1 / _configManager.GetCVar<float>("net.atmosdbgoverlaytickrate");
if (AccumulatedFrameTime < _updateCooldown)
{
return;
}
// This is the timer from GasTileOverlaySystem
AccumulatedFrameTime -= _updateCooldown;
var currentTick = _gameTiming.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 in PlayerObservers)
{
if (session.AttachedEntity == null) continue;
var entity = session.AttachedEntity;
var worldBounds = Box2.CenteredAround(entity.Transform.WorldPosition,
new Vector2(LocalViewRange, LocalViewRange));
foreach (var grid in _mapManager.FindGridsIntersecting(entity.Transform.MapID, worldBounds))
{
if (!_entityManager.TryGetEntity(grid.GridEntityId, out var gridEnt)) continue;
if (!gridEnt.TryGetComponent<GridAtmosphereComponent>(out var gam)) continue;
var entityTile = grid.GetTileRef(entity.Transform.Coordinates).GridIndices;
var baseTile = new MapIndices(entityTile.X - (LocalViewRange / 2), entityTile.Y - (LocalViewRange / 2));
var debugOverlayContent = new AtmosDebugOverlayData[LocalViewRange * LocalViewRange];
var index = 0;
for (var y = 0; y < LocalViewRange; y++)
{
for (var x = 0; x < LocalViewRange; x++)
{
var mapIndices = new MapIndices(baseTile.X + x, baseTile.Y + y);
debugOverlayContent[index++] = ConvertTileToData(gam.GetTile(mapIndices));
}
}
RaiseNetworkEvent(new AtmosDebugOverlayMessage(grid.Index, baseTile, debugOverlayContent), session.ConnectedClient);
}
}
}
}
}

View File

@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Map;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using Content.Shared.Atmos;
namespace Content.Shared.GameObjects.EntitySystems.Atmos
{
public abstract class SharedAtmosDebugOverlaySystem : EntitySystem
{
// Keep in mind, this system is hilariously unoptimized. The goal here is to provide accurate debug data.
public const int LocalViewRange = 16;
protected float AccumulatedFrameTime;
[Serializable, NetSerializable]
public readonly struct AtmosDebugOverlayData
{
public readonly float Temperature;
public readonly float[] Moles;
public readonly AtmosDirection PressureDirection;
public readonly bool InExcitedGroup;
public AtmosDebugOverlayData(float temperature, float[] moles, AtmosDirection pressureDirection, bool inExcited)
{
Temperature = temperature;
Moles = moles;
PressureDirection = pressureDirection;
InExcitedGroup = inExcited;
}
}
/// <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 AtmosDebugOverlayMessage : EntitySystemMessage
{
public GridId GridId { get; }
public MapIndices BaseIdx { get; }
// LocalViewRange*LocalViewRange
public AtmosDebugOverlayData[] OverlayData { get; }
public AtmosDebugOverlayMessage(GridId gridIndices, MapIndices baseIdx, AtmosDebugOverlayData[] overlayData)
{
GridId = gridIndices;
BaseIdx = baseIdx;
OverlayData = overlayData;
}
}
}
}

View File

@@ -195,6 +195,7 @@
- settemp - settemp
- setatmostemp - setatmostemp
- deletegas - deletegas
- showatmos
- tilewalls - tilewalls
- events - events
- destroymechanism - destroymechanism