mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
335 lines
14 KiB
C#
335 lines
14 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using System.Text;
|
|
using Robust.Client.Graphics;
|
|
using Robust.Client.ResourceManagement;
|
|
using Robust.Shared.Enums;
|
|
using Robust.Shared.Console;
|
|
using Robust.Shared.GameObjects;
|
|
using Robust.Shared.IoC;
|
|
using Robust.Shared.Maths;
|
|
using Robust.Shared.Network;
|
|
using Robust.Shared.Timing;
|
|
using Robust.Client.Player;
|
|
using Robust.Shared.Log;
|
|
using Robust.Shared.Utility;
|
|
|
|
namespace Robust.Client.GameStates
|
|
{
|
|
/// <summary>
|
|
/// Visual debug overlay for the network diagnostic graph.
|
|
/// </summary>
|
|
internal sealed class NetGraphOverlay : Overlay
|
|
{
|
|
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
|
[Dependency] private readonly IClientNetManager _netManager = default!;
|
|
[Dependency] private readonly IClientGameStateManager _gameStateManager = default!;
|
|
[Dependency] private readonly IComponentFactory _componentFactory = default!;
|
|
[Dependency] private readonly IEntityManager _entManager = default!;
|
|
|
|
private const int HistorySize = 60 * 5; // number of ticks to keep in history.
|
|
private const int TargetPayloadBps = 56000 / 8; // Target Payload size in Bytes per second. A mind-numbing fifty-six thousand bits per second, who would ever need more?
|
|
private const int ExtremePayloadBps = 256000 / 8; // I know this is somewhat extreme, but I'm gonna make the graph go up to 256kbs. That's a whopping 32 KILO bytes per second.
|
|
private const int MidrangePayloadBps = 19200 / 8; // mid-range line
|
|
private const int BaselineBps = 8192 / 8; // mid-range line
|
|
private const int LowerGraphOffset = 100; // Offset on the Y axis in pixels of the lower lag/interp graph.
|
|
private const int LeftMargin = 500; // X offset, to avoid interfering with the f3 menu.
|
|
private const int MsPerPixel = 4; // Latency Milliseconds per pixel, for scaling the graph.
|
|
|
|
/// <inheritdoc />
|
|
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
|
|
|
|
private readonly Font _font;
|
|
|
|
private readonly List<(GameTick Tick, int Payload, int lag, int Buffer)> _history = new(HistorySize+10);
|
|
|
|
// sum of all data point sizes in bytes
|
|
private int _totalHistoryPayload;
|
|
private int _totalUncompressed;
|
|
|
|
public EntityUid WatchEntId { get; set; }
|
|
|
|
public NetGraphOverlay()
|
|
{
|
|
IoCManager.InjectDependencies(this);
|
|
var cache = IoCManager.Resolve<IResourceCache>();
|
|
_font = new VectorFont(cache.GetResource<FontResource>("/EngineFonts/NotoSans/NotoSans-Regular.ttf"), 10);
|
|
|
|
_gameStateManager.GameStateApplied += HandleGameStateApplied;
|
|
}
|
|
|
|
private void HandleGameStateApplied(GameStateAppliedArgs args)
|
|
{
|
|
var toSeq = args.AppliedState.ToSequence;
|
|
var sz = args.AppliedState.PayloadSize;
|
|
|
|
// calc lag
|
|
var lag = _netManager.ServerChannel!.Ping;
|
|
|
|
// calc interp info
|
|
var buffer = _gameStateManager.CurrentBufferSize;
|
|
|
|
_totalHistoryPayload += sz;
|
|
_history.Add((toSeq, sz, lag, buffer));
|
|
|
|
// not watching an ent
|
|
if(!WatchEntId.IsValid() || _entManager.IsClientSide(WatchEntId))
|
|
return;
|
|
|
|
string? entStateString = null;
|
|
string? entDelString = null;
|
|
var conShell = IoCManager.Resolve<IConsoleHost>().LocalShell;
|
|
|
|
var entStates = args.AppliedState.EntityStates;
|
|
if (entStates.HasContents)
|
|
{
|
|
var sb = new StringBuilder();
|
|
foreach (var entState in entStates.Span)
|
|
{
|
|
var uid = _entManager.GetEntity(entState.NetEntity);
|
|
|
|
if (uid != WatchEntId)
|
|
continue;
|
|
|
|
if (!entState.ComponentChanges.HasContents)
|
|
{
|
|
sb.Append("\n Entered PVS");
|
|
break;
|
|
}
|
|
|
|
sb.Append($"\n Changes:");
|
|
foreach (var compChange in entState.ComponentChanges.Span)
|
|
{
|
|
var registration = _componentFactory.GetRegistration(compChange.NetID);
|
|
sb.Append($"\n [{compChange.NetID}:{registration.Name}");
|
|
|
|
if (compChange.State is not null)
|
|
sb.Append($"\n STATE:{compChange.State.GetType().Name}");
|
|
}
|
|
|
|
// Note that component deletion is now implicit via the list of network comp ids. So it currently
|
|
// doesn't get logged here.
|
|
|
|
break;
|
|
}
|
|
entStateString = sb.ToString();
|
|
}
|
|
|
|
foreach (var ent in args.Detached)
|
|
{
|
|
var uid = _entManager.GetEntity(ent);
|
|
|
|
if (uid != WatchEntId)
|
|
continue;
|
|
|
|
conShell.WriteLine($"watchEnt: Left PVS at tick {args.AppliedState.ToSequence}, eid={WatchEntId}" + "\n");
|
|
}
|
|
|
|
var entDeletes = args.AppliedState.EntityDeletions;
|
|
if (entDeletes.HasContents)
|
|
{
|
|
foreach (var entDelete in entDeletes.Span)
|
|
{
|
|
var uid = _entManager.GetEntity(entDelete);
|
|
|
|
if (uid == WatchEntId)
|
|
entDelString = "\n Deleted";
|
|
}
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(entStateString) || !string.IsNullOrWhiteSpace(entDelString))
|
|
{
|
|
var fullString = $"watchEnt: from={args.AppliedState.FromSequence}, to={args.AppliedState.ToSequence}, eid={WatchEntId}";
|
|
if (!string.IsNullOrWhiteSpace(entStateString))
|
|
fullString += entStateString;
|
|
|
|
if (!string.IsNullOrWhiteSpace(entDelString))
|
|
fullString += entDelString;
|
|
|
|
conShell.WriteLine(fullString + "\n");
|
|
}
|
|
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected internal override void FrameUpdate(FrameEventArgs args)
|
|
{
|
|
base.FrameUpdate(args);
|
|
|
|
var over = _history.Count - HistorySize;
|
|
if (over <= 0)
|
|
return;
|
|
|
|
for (int i = 0; i < over; i++)
|
|
{
|
|
var point = _history[i];
|
|
_totalHistoryPayload -= point.Payload;
|
|
}
|
|
|
|
_history.RemoveRange(0, over);
|
|
}
|
|
|
|
private static int BytesToPixels(int bytes)
|
|
{
|
|
// the minimum size seems to be about 10 bytes.
|
|
// 10 bytes = 15 pixel, then doubling every 15 more pixels.
|
|
bytes = Math.Max(bytes - 10, 1);
|
|
return (int) Math.Round((1 + Math.Log2(bytes)) * 15);
|
|
}
|
|
|
|
protected internal override void Draw(in OverlayDrawArgs args)
|
|
{
|
|
// remember, 0,0 is top left of ui with +X right and +Y down
|
|
|
|
var highPayload = BytesToPixels(ExtremePayloadBps / _gameTiming.TickRate);
|
|
var targetPayload = BytesToPixels(TargetPayloadBps / _gameTiming.TickRate);
|
|
var midPayload = BytesToPixels(MidrangePayloadBps / _gameTiming.TickRate);
|
|
var minPayload = BytesToPixels(BaselineBps / _gameTiming.TickRate);
|
|
var width = HistorySize;
|
|
var height = 500;
|
|
var handle = args.ScreenHandle;
|
|
|
|
// bottom payload line
|
|
handle.DrawLine(new Vector2(LeftMargin, height), new Vector2(LeftMargin + width, height), Color.DarkGray.WithAlpha(0.8f));
|
|
|
|
// bottom lag line
|
|
handle.DrawLine(new Vector2(LeftMargin, height + LowerGraphOffset), new Vector2(LeftMargin + width, height + LowerGraphOffset), Color.DarkGray.WithAlpha(0.8f));
|
|
|
|
int lastLagY = -1;
|
|
int lastLagMs = -1;
|
|
// data points
|
|
for (var i = 0; i < _history.Count; i++)
|
|
{
|
|
var state = _history[i];
|
|
|
|
// draw the uncompressed size
|
|
var xOff = LeftMargin + i;
|
|
var yoff = height - BytesToPixels(state.Payload);
|
|
handle.DrawLine(new Vector2(xOff, height), new Vector2(xOff, yoff), Color.LimeGreen.WithAlpha(0.8f));
|
|
|
|
// second tick marks
|
|
if (state.Tick.Value % (_gameTiming.TickRate/5) == 0)
|
|
{
|
|
handle.DrawLine(new Vector2(xOff, height), new Vector2(xOff, height+2), Color.LightGray);
|
|
}
|
|
|
|
// lag data
|
|
var lagYoff = height + LowerGraphOffset - state.lag / MsPerPixel;
|
|
lastLagY = lagYoff - 1;
|
|
lastLagMs = state.lag;
|
|
handle.DrawLine(new Vector2(xOff, lagYoff - 2), new Vector2(xOff, lagYoff - 1), Color.Blue.WithAlpha(0.8f));
|
|
|
|
// interp data
|
|
Color interpColor;
|
|
if(state.Buffer < _gameStateManager.MinBufferSize)
|
|
interpColor = Color.Red;
|
|
else if(state.Buffer < _gameStateManager.TargetBufferSize)
|
|
interpColor = Color.Yellow;
|
|
else
|
|
interpColor = Color.Green;
|
|
|
|
var delta = (state.Buffer - _gameStateManager.MinBufferSize);
|
|
handle.DrawLine(new Vector2(xOff, height + LowerGraphOffset), new Vector2(xOff, height + LowerGraphOffset + delta * 6), interpColor.WithAlpha(0.8f));
|
|
}
|
|
|
|
// average payload line
|
|
var avg = height - BytesToPixels(_totalHistoryPayload/HistorySize);
|
|
var avgEnd = new Vector2(LeftMargin + width, avg) + new Vector2(70, 0);
|
|
handle.DrawLine(new Vector2(LeftMargin, avg), avgEnd, Color.DarkGray.WithAlpha(0.8f));
|
|
|
|
// top payload warning line
|
|
var warnYoff = height - targetPayload;
|
|
handle.DrawLine(new Vector2(LeftMargin, warnYoff), new Vector2(LeftMargin + width, warnYoff), Color.DarkGray.WithAlpha(0.8f));
|
|
|
|
// Extreme payload hazard line -- their modem will probably explode at this point.
|
|
var extremeYoff = height - highPayload;
|
|
handle.DrawLine(new Vector2(LeftMargin, warnYoff), new Vector2(LeftMargin + width, warnYoff), Color.DarkGray.WithAlpha(0.8f));
|
|
|
|
// mid payload line
|
|
var midYoff = height - midPayload;
|
|
handle.DrawLine(new Vector2(LeftMargin, midYoff), new Vector2(LeftMargin + width, midYoff), Color.DarkGray.WithAlpha(0.8f));
|
|
|
|
var minYoff = height - minPayload;
|
|
handle.DrawLine(new Vector2(LeftMargin, minYoff), new Vector2(LeftMargin + width, minYoff), Color.DarkGray.WithAlpha(0.8f));
|
|
|
|
// payload text
|
|
handle.DrawString(_font, new Vector2(LeftMargin + width, extremeYoff), "128Kbit/s");
|
|
handle.DrawString(_font, new Vector2(LeftMargin + width, warnYoff), "56Kbit/s");
|
|
handle.DrawString(_font, new Vector2(LeftMargin + width, midYoff), "33.6Kbit/s");
|
|
handle.DrawString(_font, new Vector2(LeftMargin + width, minYoff), "8.19Kbit/s");
|
|
|
|
// avg text
|
|
var lineHeight = _font.GetLineHeight(1) / 2f;
|
|
handle.DrawString(_font, avgEnd - new Vector2(lineHeight, lineHeight), "average");
|
|
|
|
// lag text info
|
|
if(lastLagY != -1)
|
|
handle.DrawString(_font, new Vector2(LeftMargin + width, lastLagY), $"{lastLagMs.ToString()}ms");
|
|
|
|
// buffer text
|
|
handle.DrawString(_font, new Vector2(LeftMargin, height + LowerGraphOffset), $"{_gameStateManager.CurrentBufferSize.ToString()} states");
|
|
}
|
|
|
|
protected override void DisposeBehavior()
|
|
{
|
|
_gameStateManager.GameStateApplied -= HandleGameStateApplied;
|
|
|
|
base.DisposeBehavior();
|
|
}
|
|
|
|
private sealed class NetShowGraphCommand : LocalizedCommands
|
|
{
|
|
public override string Command => "net_graph";
|
|
|
|
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
|
{
|
|
var overlayMan = IoCManager.Resolve<IOverlayManager>();
|
|
|
|
if(!overlayMan.HasOverlay(typeof(NetGraphOverlay)))
|
|
{
|
|
overlayMan.AddOverlay(new NetGraphOverlay());
|
|
shell.WriteLine("Enabled network overlay.");
|
|
}
|
|
else
|
|
{
|
|
overlayMan.RemoveOverlay(typeof(NetGraphOverlay));
|
|
shell.WriteLine("Disabled network overlay.");
|
|
}
|
|
}
|
|
}
|
|
|
|
private sealed class NetWatchEntCommand : LocalizedCommands
|
|
{
|
|
[Dependency] private readonly IEntityManager _entManager = default!;
|
|
[Dependency] private readonly IOverlayManager _overlayManager = default!;
|
|
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
|
|
|
public override string Command => "net_watchent";
|
|
|
|
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
|
{
|
|
EntityUid? entity;
|
|
|
|
if (args.Length == 0)
|
|
{
|
|
entity = _playerManager.LocalPlayer?.ControlledEntity ?? EntityUid.Invalid;
|
|
}
|
|
else if (!NetEntity.TryParse(args[0], out var netEntity) || !_entManager.TryGetEntity(netEntity, out entity))
|
|
{
|
|
shell.WriteError("Invalid argument: Needs to be 0 or an entityId.");
|
|
return;
|
|
}
|
|
|
|
if (!_overlayManager.TryGetOverlay(out NetGraphOverlay? overlay))
|
|
{
|
|
overlay = new NetGraphOverlay();
|
|
_overlayManager.AddOverlay(overlay);
|
|
}
|
|
|
|
overlay.WatchEntId = entity.Value;
|
|
}
|
|
}
|
|
}
|
|
}
|