using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Threading;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Mirror;
using Nessie.ATLYSS.EasySettings;
using ServX;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("ServX")]
[assembly: AssemblyConfiguration("Debug")]
[assembly: AssemblyFileVersion("1.3.0.0")]
[assembly: AssemblyInformationalVersion("1.3.0")]
[assembly: AssemblyProduct("ServX")]
[assembly: AssemblyTitle("ServX")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.3.0.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
[CompilerGenerated]
[Microsoft.CodeAnalysis.Embedded]
internal sealed class EmbeddedAttribute : Attribute
{
}
}
namespace System.Runtime.CompilerServices
{
[CompilerGenerated]
[Microsoft.CodeAnalysis.Embedded]
[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
internal sealed class RefSafetyRulesAttribute : Attribute
{
public readonly int Version;
public RefSafetyRulesAttribute(int P_0)
{
Version = P_0;
}
}
}
public class PingGraph : MonoBehaviour
{
private const float GraphMaxValue = 500f;
private const int SampleCapacity = 200;
private GameObject graphCanvas;
private RectTransform graphContainer;
private Text maxPingText;
private Text avgPingText;
private Text minPingText;
private Text changeText;
private Text timerText;
private bool isInitialized;
private bool sessionSummaryDisplayed;
private bool manualHidden;
private bool wasConnected;
private float tickTimer;
private readonly List<GameObject> linePool = new List<GameObject>();
private PingGraphPopoutWindow popoutWindow;
private readonly object popoutLock = new object();
private volatile bool popoutUserHideRequested;
private static bool popoutSupportWarningLogged;
internal static PingGraph Instance { get; private set; }
private static bool PopoutSupported => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
private void Awake()
{
Instance = this;
}
private void Start()
{
ApplyDisplayMode();
}
private void OnDestroy()
{
if ((Object)(object)Instance == (Object)(object)this)
{
Instance = null;
}
ClosePopoutWindow(waitForThread: true);
DestroyGraphCanvas();
}
public static void RefreshDisplayMode()
{
Instance?.ApplyDisplayMode();
}
private bool UsingPopout()
{
ConfigEntry<bool> usePopoutGraph = Plugin.usePopoutGraph;
if (usePopoutGraph != null && usePopoutGraph.Value && !PingTracker.DisableGraph && IsMultiplayerActive())
{
if (!PopoutSupported)
{
if (!popoutSupportWarningLogged)
{
Plugin.Logger.LogWarning((object)"Pop-out ping graph is currently supported on Windows only.");
popoutSupportWarningLogged = true;
}
return false;
}
return true;
}
return false;
}
private static bool IsMultiplayerActive()
{
return NetworkClient.isConnected && !(AtlyssNetworkManager._current?._soloMode ?? true);
}
private bool ShouldDisplayPopout()
{
return wasConnected && PingTracker.AveragePings.Count > 0;
}
private void ApplyDisplayMode()
{
if (UsingPopout())
{
if (!manualHidden && ShouldDisplayPopout())
{
EnsurePopoutWindow();
HideOverlay();
PushPopoutSnapshot();
SetPopoutVisibility(visible: true);
}
else
{
SetPopoutVisibility(visible: false);
}
return;
}
ClosePopoutWindow();
if (!isInitialized && !PingTracker.DisableGraph)
{
InitializeGraph();
}
if ((Object)(object)graphCanvas != (Object)null)
{
graphCanvas.SetActive(!manualHidden && !PingTracker.DisableGraph);
}
}
private void Update()
{
if (popoutUserHideRequested)
{
popoutUserHideRequested = false;
manualHidden = true;
}
bool flag = UsingPopout();
bool disableSummary = PingTracker.DisableSummary;
bool hasData = PingTracker.AveragePings.Count > 0;
bool flag2 = NetworkClient.isConnected && !(AtlyssNetworkManager._current?._soloMode ?? true);
if (PingTracker.DisableGraph)
{
HandleGraphDisabled(disableSummary, hasData);
return;
}
if (!flag2)
{
HandleDisconnected(disableSummary, hasData);
return;
}
if (!wasConnected)
{
PingTracker.ResetSessionMetrics();
sessionSummaryDisplayed = false;
wasConnected = true;
manualHidden = false;
}
PingTracker.Ticks++;
TrySampleLatency();
if (!flag && !isInitialized)
{
InitializeGraph();
}
HandleInput(flag, disableSummary, hasData);
UpdateVisuals(flag);
PingTracker.Timer += Time.deltaTime;
UpdateTpsHistory();
}
private void HandleGraphDisabled(bool disableSummary, bool hasData)
{
if (!disableSummary && !sessionSummaryDisplayed && hasData)
{
PingTracker.LogSessionSummaryIfAvailable();
}
if (hasData)
{
PingTracker.ResetSessionMetrics();
}
manualHidden = false;
sessionSummaryDisplayed = !disableSummary && hasData;
wasConnected = false;
HideOverlay();
ClosePopoutWindow();
if (isInitialized)
{
DestroyGraphCanvas();
}
}
private void HandleDisconnected(bool disableSummary, bool hasData)
{
if (wasConnected && !disableSummary && hasData)
{
PingTracker.LogSessionSummaryIfAvailable();
}
if (hasData)
{
PingTracker.ResetSessionMetrics();
}
wasConnected = false;
manualHidden = false;
sessionSummaryDisplayed = false;
HideOverlay();
ClosePopoutWindow();
if (isInitialized)
{
DestroyGraphCanvas();
}
}
private void HandleInput(bool popoutMode, bool disableSummary, bool hasData)
{
//IL_002b: Unknown result type (might be due to invalid IL or missing references)
//IL_0030: Unknown result type (might be due to invalid IL or missing references)
//IL_0031: Unknown result type (might be due to invalid IL or missing references)
//IL_0034: Unknown result type (might be due to invalid IL or missing references)
//IL_007f: Unknown result type (might be due to invalid IL or missing references)
//IL_0084: Unknown result type (might be due to invalid IL or missing references)
//IL_0085: Unknown result type (might be due to invalid IL or missing references)
//IL_0088: Unknown result type (might be due to invalid IL or missing references)
ConfigEntry<bool> enablePingGraph = Plugin.enablePingGraph;
if (enablePingGraph == null || !enablePingGraph.Value)
{
return;
}
ConfigEntry<KeyCode> toggleGraphKey = Plugin.toggleGraphKey;
KeyCode val = (KeyCode)((toggleGraphKey != null) ? ((int)toggleGraphKey.Value) : 0);
if ((int)val != 0 && Input.GetKeyDown(val))
{
manualHidden = !manualHidden;
if (popoutMode)
{
SetPopoutVisibility(!manualHidden && ShouldDisplayPopout());
}
}
ConfigEntry<KeyCode> resetGraphKey = Plugin.resetGraphKey;
KeyCode val2 = (KeyCode)((resetGraphKey != null) ? ((int)resetGraphKey.Value) : 0);
if ((int)val2 != 0 && Input.GetKeyDown(val2))
{
if (!disableSummary && hasData)
{
PingTracker.LogSessionSummaryIfAvailable();
}
PingTracker.ResetSessionMetrics();
sessionSummaryDisplayed = false;
ClearGraphVisuals();
if (popoutMode)
{
PushPopoutSnapshot();
}
}
}
private void UpdateVisuals(bool popoutMode)
{
if (popoutMode)
{
if (!manualHidden && ShouldDisplayPopout())
{
HideOverlay();
EnsurePopoutWindow();
PushPopoutSnapshot();
SetPopoutVisibility(visible: true);
}
else
{
SetPopoutVisibility(visible: false);
}
return;
}
if ((Object)(object)graphCanvas != (Object)null)
{
graphCanvas.SetActive(!manualHidden);
}
UpdateGraph();
UpdateTextValues();
if ((Object)(object)graphCanvas != (Object)null && manualHidden)
{
graphCanvas.SetActive(false);
}
}
private void UpdateTpsHistory()
{
tickTimer += Time.deltaTime;
if (tickTimer >= 1f)
{
PingTracker.LastTPS = PingTracker.Ticks;
PingTracker.TPSHistory.Add(PingTracker.LastTPS);
PingTracker.Ticks = 0;
tickTimer = 0f;
}
}
private void HideOverlay()
{
if ((Object)(object)graphCanvas != (Object)null && graphCanvas.activeSelf)
{
graphCanvas.SetActive(false);
}
}
private void TrySampleLatency()
{
if (Time.time - PingTracker.LastSampleTime < 1f || !PingTracker.TryCaptureSnapshot(out var avg, out var max, out var min, out var playerPing))
{
return;
}
PingTracker.LastSampleTime = Time.time;
PingTracker.AveragePings.Add(avg);
PingTracker.MaxPings.Add(max);
PingTracker.MinPings.Add(min);
PingTracker.PlayerPings.Add(playerPing);
if (PingTracker.AveragePings.Count > 200)
{
PingTracker.AveragePings.RemoveAt(0);
if (PingTracker.MaxPings.Count > 200)
{
PingTracker.MaxPings.RemoveAt(0);
}
if (PingTracker.MinPings.Count > 200)
{
PingTracker.MinPings.RemoveAt(0);
}
}
if (PingTracker.PlayerPings.Count > 200)
{
PingTracker.PlayerPings.RemoveAt(0);
}
if (!PingTracker.HighestPing.HasValue || max > PingTracker.HighestPing)
{
PingTracker.HighestPing = max;
}
if (!PingTracker.LowestPing.HasValue || min < PingTracker.LowestPing)
{
PingTracker.LowestPing = min;
}
float valueOrDefault = PingTracker.FirstAveragePing.GetValueOrDefault();
if (!PingTracker.FirstAveragePing.HasValue)
{
valueOrDefault = avg;
PingTracker.FirstAveragePing = valueOrDefault;
}
}
private void InitializeGraph()
{
CreateGraphCanvas();
CreateTextElements();
isInitialized = true;
}
private void CreateGraphCanvas()
{
//IL_001f: Unknown result type (might be due to invalid IL or missing references)
//IL_0029: Expected O, but got Unknown
//IL_005a: Unknown result type (might be due to invalid IL or missing references)
//IL_0060: Expected O, but got Unknown
//IL_0093: Unknown result type (might be due to invalid IL or missing references)
//IL_00ae: Unknown result type (might be due to invalid IL or missing references)
//IL_00c9: Unknown result type (might be due to invalid IL or missing references)
//IL_00e4: Unknown result type (might be due to invalid IL or missing references)
//IL_00ff: Unknown result type (might be due to invalid IL or missing references)
//IL_010f: Unknown result type (might be due to invalid IL or missing references)
//IL_0115: Expected O, but got Unknown
//IL_0149: Unknown result type (might be due to invalid IL or missing references)
//IL_0168: Unknown result type (might be due to invalid IL or missing references)
//IL_017f: Unknown result type (might be due to invalid IL or missing references)
//IL_0196: Unknown result type (might be due to invalid IL or missing references)
//IL_01ad: Unknown result type (might be due to invalid IL or missing references)
//IL_01ba: Unknown result type (might be due to invalid IL or missing references)
if (!((Object)(object)graphCanvas != (Object)null))
{
graphCanvas = new GameObject("PingGraphCanvas");
Canvas val = graphCanvas.AddComponent<Canvas>();
val.renderMode = (RenderMode)0;
graphCanvas.AddComponent<CanvasScaler>();
graphCanvas.AddComponent<GraphicRaycaster>();
GameObject val2 = new GameObject("GraphContainer");
val2.transform.SetParent(graphCanvas.transform);
graphContainer = val2.AddComponent<RectTransform>();
graphContainer.sizeDelta = new Vector2(300f, 150f);
graphContainer.anchorMin = new Vector2(1f, 1f);
graphContainer.anchorMax = new Vector2(1f, 1f);
graphContainer.pivot = new Vector2(1f, 1f);
graphContainer.anchoredPosition = new Vector2(-10f, -20f);
GameObject val3 = new GameObject("GraphBackground");
val3.transform.SetParent(((Component)graphContainer).transform, false);
Image val4 = val3.AddComponent<Image>();
((Graphic)val4).color = new Color(0.1f, 0.1f, 0.1f, 0.8f);
RectTransform rectTransform = ((Graphic)val4).rectTransform;
rectTransform.sizeDelta = new Vector2(500f, 150f);
rectTransform.anchorMin = new Vector2(0f, 0f);
rectTransform.anchorMax = new Vector2(1f, 1f);
rectTransform.offsetMin = new Vector2(-120f, 0f);
rectTransform.offsetMax = Vector2.zero;
graphCanvas.SetActive(false);
}
}
private void CreateTextElements()
{
//IL_0012: Unknown result type (might be due to invalid IL or missing references)
//IL_0017: Unknown result type (might be due to invalid IL or missing references)
//IL_0037: Unknown result type (might be due to invalid IL or missing references)
//IL_003c: Unknown result type (might be due to invalid IL or missing references)
//IL_005c: Unknown result type (might be due to invalid IL or missing references)
//IL_0061: Unknown result type (might be due to invalid IL or missing references)
//IL_0081: Unknown result type (might be due to invalid IL or missing references)
//IL_0086: Unknown result type (might be due to invalid IL or missing references)
//IL_00a6: Unknown result type (might be due to invalid IL or missing references)
//IL_00ab: Unknown result type (might be due to invalid IL or missing references)
maxPingText = CreateText("MaxPingText", new Vector2(-320f, -10f), Color.red);
avgPingText = CreateText("AvgPingText", new Vector2(-320f, -30f), Color.green);
minPingText = CreateText("MinPingText", new Vector2(-320f, -50f), Color.cyan);
changeText = CreateText("ChangePingText", new Vector2(-320f, -70f), Color.yellow);
timerText = CreateText("TimerText", new Vector2(-320f, -90f), Color.white);
}
private Text CreateText(string name, Vector2 position, Color color)
{
//IL_0002: Unknown result type (might be due to invalid IL or missing references)
//IL_0008: Expected O, but got Unknown
//IL_0041: Unknown result type (might be due to invalid IL or missing references)
//IL_0072: Unknown result type (might be due to invalid IL or missing references)
//IL_0088: Unknown result type (might be due to invalid IL or missing references)
//IL_009e: Unknown result type (might be due to invalid IL or missing references)
//IL_00aa: Unknown result type (might be due to invalid IL or missing references)
GameObject val = new GameObject(name);
val.transform.SetParent(graphCanvas.transform);
Text val2 = val.AddComponent<Text>();
val2.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
val2.fontSize = 14;
((Graphic)val2).color = color;
val2.alignment = (TextAnchor)3;
val2.horizontalOverflow = (HorizontalWrapMode)1;
val2.verticalOverflow = (VerticalWrapMode)1;
RectTransform rectTransform = ((Graphic)val2).rectTransform;
rectTransform.anchorMin = new Vector2(1f, 1f);
rectTransform.anchorMax = new Vector2(1f, 1f);
rectTransform.pivot = new Vector2(1f, 1f);
rectTransform.anchoredPosition = position;
return val2;
}
private void UpdateGraph()
{
//IL_0049: Unknown result type (might be due to invalid IL or missing references)
//IL_005a: Unknown result type (might be due to invalid IL or missing references)
//IL_00a7: Unknown result type (might be due to invalid IL or missing references)
//IL_00d8: Unknown result type (might be due to invalid IL or missing references)
//IL_0109: Unknown result type (might be due to invalid IL or missing references)
if ((Object)(object)graphCanvas == (Object)null || (Object)(object)graphContainer == (Object)null || PingTracker.AveragePings.Count == 0)
{
return;
}
float y = graphContainer.sizeDelta.y;
float x = graphContainer.sizeDelta.x;
float step = x / (float)Mathf.Max(PingTracker.AveragePings.Count - 1, 1);
int num = 0;
for (int i = 1; i < PingTracker.AveragePings.Count; i++)
{
CreateGraphLine(PingTracker.AveragePings[i - 1], PingTracker.AveragePings[i], i - 1, step, y, Color.green, num++);
CreateGraphLine(PingTracker.MaxPings[i - 1], PingTracker.MaxPings[i], i - 1, step, y, Color.red, num++);
CreateGraphLine(PingTracker.MinPings[i - 1], PingTracker.MinPings[i], i - 1, step, y, Color.cyan, num++);
}
for (int j = num; j < linePool.Count; j++)
{
if ((Object)(object)linePool[j] != (Object)null)
{
linePool[j].SetActive(false);
}
}
graphCanvas.SetActive(!manualHidden);
}
private void CreateGraphLine(float value1, float value2, int index, float step, float graphHeight, Color color, int poolIndex)
{
//IL_0096: Unknown result type (might be due to invalid IL or missing references)
//IL_009d: Expected O, but got Unknown
//IL_00d2: Unknown result type (might be due to invalid IL or missing references)
//IL_00f1: Unknown result type (might be due to invalid IL or missing references)
//IL_00f9: Unknown result type (might be due to invalid IL or missing references)
//IL_010e: Unknown result type (might be due to invalid IL or missing references)
//IL_0125: Unknown result type (might be due to invalid IL or missing references)
//IL_013c: Unknown result type (might be due to invalid IL or missing references)
//IL_0153: Unknown result type (might be due to invalid IL or missing references)
//IL_0163: Unknown result type (might be due to invalid IL or missing references)
//IL_0191: Unknown result type (might be due to invalid IL or missing references)
float num = Mathf.Clamp(value1, 0f, 500f);
float num2 = Mathf.Clamp(value2, 0f, 500f);
float num3 = (float)index * step;
float num4 = (float)(index + 1) * step;
float num5 = num / 500f * graphHeight;
float num6 = num2 / 500f * graphHeight;
GameObject val;
if (poolIndex < linePool.Count && (Object)(object)linePool[poolIndex] != (Object)null)
{
val = linePool[poolIndex];
val.SetActive(true);
}
else
{
val = new GameObject("GraphLine");
val.transform.SetParent((Transform)(object)graphContainer);
val.AddComponent<Image>();
linePool.Add(val);
}
Image component = val.GetComponent<Image>();
((Graphic)component).color = color;
RectTransform val2 = val.GetComponent<RectTransform>() ?? val.AddComponent<RectTransform>();
float num7 = Vector2.Distance(new Vector2(num3, num5), new Vector2(num4, num6));
val2.sizeDelta = new Vector2(num7, 2f);
val2.anchorMin = new Vector2(0f, 0f);
val2.anchorMax = new Vector2(0f, 0f);
val2.pivot = new Vector2(0f, 0.5f);
val2.anchoredPosition = new Vector2(num3, num5);
float num8 = Mathf.Atan2(num6 - num5, num4 - num3) * 57.29578f;
((Transform)val2).localRotation = Quaternion.Euler(0f, 0f, num8);
}
private void UpdateTextValues()
{
if (!((Object)(object)maxPingText == (Object)null) && !((Object)(object)avgPingText == (Object)null) && !((Object)(object)minPingText == (Object)null) && !((Object)(object)changeText == (Object)null) && !((Object)(object)timerText == (Object)null) && PingTracker.AveragePings.Count > 0)
{
avgPingText.text = $"Avg: {PingTracker.AveragePings.Last():F1} ms";
maxPingText.text = $"Max: {PingTracker.MaxPings.Last():F1} ms";
minPingText.text = $"Min: {PingTracker.MinPings.Last():F1} ms";
float num = PingTracker.FirstAveragePing ?? PingTracker.AveragePings.First();
float num2 = PingTracker.AveragePings.Last();
float num3 = num2 - num;
changeText.text = $"ΔAvg: {num3:+0.0;-0.0} ms";
TimeSpan timeSpan = TimeSpan.FromSeconds(PingTracker.Timer);
timerText.text = $"Time: {timeSpan:hh\\:mm\\:ss}";
}
}
private void ClearGraphVisuals()
{
foreach (GameObject item in linePool)
{
if (Object.op_Implicit((Object)(object)item))
{
item.SetActive(false);
}
}
if (Object.op_Implicit((Object)(object)maxPingText))
{
maxPingText.text = string.Empty;
}
if (Object.op_Implicit((Object)(object)avgPingText))
{
avgPingText.text = string.Empty;
}
if (Object.op_Implicit((Object)(object)minPingText))
{
minPingText.text = string.Empty;
}
if (Object.op_Implicit((Object)(object)changeText))
{
changeText.text = string.Empty;
}
if (Object.op_Implicit((Object)(object)timerText))
{
timerText.text = string.Empty;
}
}
private void DestroyGraphCanvas()
{
if ((Object)(object)graphCanvas != (Object)null)
{
Object.Destroy((Object)(object)graphCanvas);
graphCanvas = null;
graphContainer = null;
}
maxPingText = null;
avgPingText = null;
minPingText = null;
changeText = null;
timerText = null;
isInitialized = false;
foreach (GameObject item in linePool)
{
if ((Object)(object)item != (Object)null)
{
Object.Destroy((Object)(object)item);
}
}
linePool.Clear();
}
private void EnsurePopoutWindow()
{
if (!PopoutSupported)
{
return;
}
lock (popoutLock)
{
if (popoutWindow == null || popoutWindow.IsDisposed)
{
PingGraphPopoutWindow pingGraphPopoutWindow = new PingGraphPopoutWindow();
pingGraphPopoutWindow.UserHideRequested += delegate
{
popoutUserHideRequested = true;
};
popoutWindow = pingGraphPopoutWindow;
}
}
}
private void ClosePopoutWindow(bool waitForThread = false)
{
PingGraphPopoutWindow pingGraphPopoutWindow;
lock (popoutLock)
{
pingGraphPopoutWindow = popoutWindow;
popoutWindow = null;
}
if (pingGraphPopoutWindow == null)
{
return;
}
try
{
pingGraphPopoutWindow.Dispose();
}
catch (Exception ex)
{
ManualLogSource logger = Plugin.Logger;
if (logger != null)
{
logger.LogDebug((object)("Popout window dispose error: " + ex.Message));
}
}
}
private void SetPopoutVisibility(bool visible)
{
PingGraphPopoutWindow pingGraphPopoutWindow;
lock (popoutLock)
{
pingGraphPopoutWindow = popoutWindow;
}
if (pingGraphPopoutWindow != null && !pingGraphPopoutWindow.IsDisposed)
{
pingGraphPopoutWindow.SetWindowVisibility(visible);
}
}
private void PushPopoutSnapshot()
{
PingGraphPopoutWindow pingGraphPopoutWindow;
lock (popoutLock)
{
pingGraphPopoutWindow = popoutWindow;
}
if (pingGraphPopoutWindow != null && !pingGraphPopoutWindow.IsDisposed)
{
pingGraphPopoutWindow.UpdateSnapshot(BuildPopoutSnapshot());
}
}
private PingGraphPopoutSnapshot BuildPopoutSnapshot()
{
float[] array = PingTracker.AveragePings.ToArray();
float[] array2 = PingTracker.MaxPings.ToArray();
float[] array3 = PingTracker.MinPings.ToArray();
List<PingGraphPopoutStatLine> list = new List<PingGraphPopoutStatLine>();
if (array.Length == 0)
{
list.Add(new PingGraphPopoutStatLine
{
Label = string.Empty,
Value = "Waiting for samples...",
ColorRef = PingGraphPopoutWindow.ColorRef(200, 200, 200)
});
}
else
{
float num = array[^1];
float num2 = array2[^1];
float num3 = array3[^1];
float num4 = PingTracker.FirstAveragePing ?? array[0];
float num5 = num - num4;
TimeSpan timeSpan = TimeSpan.FromSeconds(PingTracker.Timer);
float num6 = array.Average();
float valueOrDefault = PingTracker.HighestPing.GetValueOrDefault(num2);
float valueOrDefault2 = PingTracker.LowestPing.GetValueOrDefault(num3);
float num7 = ((PingTracker.PlayerPings.Count > 0) ? ((float)PingTracker.PlayerPings.Average()) : 0f);
float num8 = ((PingTracker.TPSHistory.Count > 0) ? ((float)PingTracker.TPSHistory.Average()) : 0f);
bool flag = Object.op_Implicit((Object)(object)Player._mainPlayer) && Player._mainPlayer._isHostPlayer;
list.Add(new PingGraphPopoutStatLine
{
Label = "Avg",
Value = $"{num:F1} ms (session {num6:F1} ms)",
ColorRef = PingGraphPopoutWindow.ColorRef(0, 200, 80)
});
list.Add(new PingGraphPopoutStatLine
{
Label = "Max",
Value = $"{num2:F1} ms (peak {valueOrDefault:F1} ms)",
ColorRef = PingGraphPopoutWindow.ColorRef(255, 90, 90)
});
list.Add(new PingGraphPopoutStatLine
{
Label = "Min",
Value = $"{num3:F1} ms (floor {valueOrDefault2:F1} ms)",
ColorRef = PingGraphPopoutWindow.ColorRef(100, 220, 255)
});
list.Add(new PingGraphPopoutStatLine
{
Label = "Delta Avg",
Value = $"{num5:+0.0;-0.0} ms",
ColorRef = PingGraphPopoutWindow.ColorRef(255, 200, 60)
});
if (!flag && num7 > 0f)
{
list.Add(new PingGraphPopoutStatLine
{
Label = "Player",
Value = $"{num7:F1} ms (avg)",
ColorRef = PingGraphPopoutWindow.ColorRef(180, 200, 255)
});
}
if (PingTracker.TPSHistory.Count > 0 && num8 > 0f)
{
int num9 = ((PingTracker.LastTPS > 0) ? PingTracker.LastTPS : ((int)Math.Round(num8)));
list.Add(new PingGraphPopoutStatLine
{
Label = "TPS",
Value = $"{num9} (avg {num8:F1})",
ColorRef = PingGraphPopoutWindow.ColorRef(255, 105, 180)
});
}
list.Add(new PingGraphPopoutStatLine
{
Label = "Time",
Value = $"{timeSpan:hh\\:mm\\:ss}",
ColorRef = PingGraphPopoutWindow.ColorRef(200, 200, 200)
});
}
return new PingGraphPopoutSnapshot
{
Average = array,
Max = array2,
Min = array3,
GraphMaxValue = 500f,
StatLines = list.ToArray()
};
}
private void LateUpdate()
{
if (!PingTracker.DisableGraph)
{
UpdateWhoMenuPingLabel();
}
}
private void UpdateWhoMenuPingLabel()
{
try
{
if ((Object)(object)TabMenu._current == (Object)null || (Object)(object)TabMenu._current._whoCell == (Object)null)
{
return;
}
WhoMenuCell whoCell = TabMenu._current._whoCell;
FieldInfo fieldInfo = AccessTools.Field(typeof(WhoMenuCell), "_localPingCounterText");
if (fieldInfo == null)
{
return;
}
object? value = fieldInfo.GetValue(whoCell);
Text val = (Text)((value is Text) ? value : null);
if (val == null || (Object)(object)val == (Object)null)
{
return;
}
string text = val.text ?? string.Empty;
int num = text.LastIndexOf("<color=", StringComparison.OrdinalIgnoreCase);
if (num >= 0 && text.IndexOf("TPS:", num, StringComparison.OrdinalIgnoreCase) >= 0)
{
text = text.Substring(0, num).TrimEnd();
}
else
{
int num2 = text.IndexOf("TPS:", StringComparison.OrdinalIgnoreCase);
if (num2 >= 0)
{
text = text.Substring(0, num2).TrimEnd();
}
}
int num3 = PingTracker.LastTPS;
float num4 = ((PingTracker.TPSHistory.Count > 0) ? ((float)PingTracker.TPSHistory.Average()) : 0f);
if (num3 <= 0 && num4 > 0f)
{
num3 = Mathf.Max(1, Mathf.RoundToInt(num4));
}
if (num3 <= 0)
{
val.text = text;
return;
}
string arg = ((num3 < 10) ? "#FF0000" : ((num3 < 20) ? "#FFFF00" : ((num3 >= 30) ? "#FF69B4" : "#00FF00")));
string text2 = $"<color={arg}>TPS: {num3}</color>";
val.text = (string.IsNullOrWhiteSpace(text) ? text2 : (text + " " + text2));
}
catch (Exception arg2)
{
Plugin.Logger.LogError((object)$"Error updating Ping UI: {arg2}");
}
}
}
namespace ServX
{
internal sealed class PingGraphPopoutWindow : IDisposable
{
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate IntPtr WndProcDelegate(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam);
private static class NativeMethods
{
internal struct WNDCLASSEX
{
public uint cbSize;
public uint style;
public IntPtr lpfnWndProc;
public int cbClsExtra;
public int cbWndExtra;
public IntPtr hInstance;
public IntPtr hIcon;
public IntPtr hCursor;
public IntPtr hbrBackground;
public IntPtr lpszMenuName;
[MarshalAs(UnmanagedType.LPStr)]
public string lpszClassName;
public IntPtr hIconSm;
}
internal struct MSG
{
public IntPtr hwnd;
public uint message;
public IntPtr wParam;
public IntPtr lParam;
public uint time;
public POINT pt;
}
internal struct POINT
{
public int x;
public int y;
}
internal struct RECT
{
public int left;
public int top;
public int right;
public int bottom;
}
internal struct PAINTSTRUCT
{
public IntPtr hdc;
public int fErase;
public RECT rcPaint;
public int fRestore;
public int fIncUpdate;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
public byte[] rgbReserved;
}
internal const int WS_OVERLAPPEDWINDOW = 13565952;
internal const int WS_EX_TOPMOST = 8;
internal const int WS_EX_TOOLWINDOW = 128;
internal const int WS_EX_NOACTIVATE = 134217728;
internal const int CW_USEDEFAULT = int.MinValue;
internal const int SW_SHOW = 5;
internal const int SW_SHOWNOACTIVATE = 4;
internal const int SW_HIDE = 0;
internal const int GWLP_USERDATA = -21;
internal const int WM_PAINT = 15;
internal const int WM_CLOSE = 16;
internal const int WM_DESTROY = 2;
internal const int CS_HREDRAW = 2;
internal const int CS_VREDRAW = 1;
internal const int ERROR_CLASS_ALREADY_EXISTS = 1410;
internal static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
internal const uint SWP_NOSIZE = 1u;
internal const uint SWP_NOMOVE = 2u;
internal const uint SWP_NOZORDER = 4u;
internal const uint SWP_NOACTIVATE = 16u;
internal const uint SWP_SHOWWINDOW = 64u;
internal const uint SRCCOPY = 13369376u;
internal const int PS_SOLID = 0;
internal const int TRANSPARENT = 1;
internal const int DT_LEFT = 0;
internal const int DT_CENTER = 1;
internal const int DT_VCENTER = 4;
internal const int DT_BOTTOM = 8;
internal const int DT_CALCRECT = 1024;
internal const int DT_SINGLELINE = 32;
internal const int FW_NORMAL = 400;
internal const int FW_SEMIBOLD = 600;
internal const int DEFAULT_CHARSET = 1;
internal const int CLEARTYPE_QUALITY = 5;
internal static readonly IntPtr IDC_ARROW = (IntPtr)32512;
internal const uint SPI_GETWORKAREA = 48u;
[DllImport("user32.dll", SetLastError = true)]
internal static extern ushort RegisterClassEx(ref WNDCLASSEX lpwcx);
[DllImport("user32.dll", SetLastError = true)]
internal static extern IntPtr CreateWindowEx(int dwExStyle, string lpClassName, string lpWindowName, int dwStyle, int x, int y, int nWidth, int nHeight, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam);
[DllImport("user32.dll", SetLastError = true)]
internal static extern bool DestroyWindow(IntPtr hWnd);
[DllImport("user32.dll", SetLastError = true)]
internal static extern IntPtr DefWindowProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true)]
internal static extern int GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);
[DllImport("user32.dll")]
internal static extern bool TranslateMessage(ref MSG lpMsg);
[DllImport("user32.dll")]
internal static extern IntPtr DispatchMessage(ref MSG lpMsg);
[DllImport("user32.dll", SetLastError = true)]
internal static extern IntPtr BeginPaint(IntPtr hWnd, ref PAINTSTRUCT lpPaint);
[DllImport("user32.dll", SetLastError = true)]
internal static extern bool EndPaint(IntPtr hWnd, ref PAINTSTRUCT lpPaint);
[DllImport("user32.dll")]
internal static extern bool InvalidateRect(IntPtr hWnd, IntPtr lpRect, bool bErase);
[DllImport("user32.dll")]
internal static extern bool UpdateWindow(IntPtr hWnd);
[DllImport("user32.dll")]
internal static extern int FillRect(IntPtr hDC, ref RECT lprc, IntPtr hbr);
[DllImport("user32.dll")]
internal static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")]
internal static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")]
internal static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
[DllImport("user32.dll")]
internal static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
internal static extern IntPtr LoadCursor(IntPtr hInstance, IntPtr lpCursorName);
[DllImport("user32.dll", SetLastError = true)]
internal static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
[DllImport("user32.dll", SetLastError = true)]
internal static extern IntPtr GetWindowLongPtr(IntPtr hWnd, int nIndex);
[DllImport("user32.dll")]
internal static extern void PostQuitMessage(int nExitCode);
[DllImport("gdi32.dll")]
internal static extern IntPtr CreateSolidBrush(int crColor);
[DllImport("gdi32.dll")]
internal static extern IntPtr CreateCompatibleDC(IntPtr hdc);
[DllImport("gdi32.dll")]
internal static extern bool DeleteDC(IntPtr hdc);
[DllImport("gdi32.dll")]
internal static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int nWidth, int nHeight);
[DllImport("gdi32.dll")]
internal static extern bool BitBlt(IntPtr hdcDest, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, uint dwRop);
[DllImport("gdi32.dll")]
internal static extern bool DeleteObject(IntPtr hObject);
[DllImport("gdi32.dll")]
internal static extern IntPtr CreatePen(int fnPenStyle, int nWidth, int crColor);
[DllImport("gdi32.dll")]
internal static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
[DllImport("gdi32.dll")]
internal static extern bool MoveToEx(IntPtr hdc, int X, int Y, IntPtr lpPoint);
[DllImport("gdi32.dll")]
internal static extern bool LineTo(IntPtr hdc, int nXEnd, int nYEnd);
[DllImport("gdi32.dll")]
internal static extern int SetBkMode(IntPtr hdc, int mode);
[DllImport("gdi32.dll")]
internal static extern int SetTextColor(IntPtr hdc, int crColor);
[DllImport("gdi32.dll", CharSet = CharSet.Unicode)]
internal static extern IntPtr CreateFontW(int nHeight, int nWidth, int nEscapement, int nOrientation, int fnWeight, uint fdwItalic, uint fdwUnderline, uint fdwStrikeOut, uint fdwCharSet, uint fdwOutputPrecision, uint fdwClipPrecision, uint fdwQuality, uint fdwPitchAndFamily, string lpszFace);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
internal static extern int DrawTextW(IntPtr hdc, string lpchText, int cchText, ref RECT lprc, uint format);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("user32.dll", SetLastError = true)]
internal static extern bool SystemParametersInfo(uint uiAction, uint uiParam, ref RECT pvParam, uint fWinIni);
[DllImport("user32.dll", SetLastError = true)]
internal static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
}
private const string WindowClassName = "ServX_PingGraphWindow";
private const int GraphPadding = 16;
private const int StatsHeight = 180;
private const float DefaultGraphMaxValue = 500f;
private static readonly int BackgroundColor = ColorRef(42, 42, 42);
private static readonly int GraphBackgroundColor = ColorRef(50, 50, 50);
private static readonly int StatsBackgroundColor = ColorRef(40, 40, 40);
private static readonly int TextColor = ColorRef(238, 238, 238);
private static readonly int HeaderColor = ColorRef(255, 255, 255);
private static readonly int DividerColor = ColorRef(80, 80, 80);
private static readonly int MaxLineColor = ColorRef(240, 80, 80);
private static readonly int AvgLineColor = ColorRef(0, 200, 80);
private static readonly int MinLineColor = ColorRef(204, 204, 255);
private const int PenWidth = 2;
private static readonly object WindowClassLock = new object();
private static ushort windowClassAtom;
private static bool windowClassRegistered;
private readonly Thread windowThread;
private readonly ManualResetEvent windowReady = new ManualResetEvent(initialState: false);
private static readonly WndProcDelegate WindowProcDelegateInstance = WindowProcedure;
private static readonly IntPtr WindowProcPtr = Marshal.GetFunctionPointerForDelegate(WindowProcDelegateInstance);
private readonly object snapshotLock = new object();
private GCHandle selfHandle;
private IntPtr windowHandle;
private bool visible;
private bool forceClose;
private bool disposed;
private PingGraphPopoutSnapshot snapshot = PingGraphPopoutSnapshot.Empty;
private static readonly WndProcDelegate WindowProc = WindowProcedure;
internal bool IsDisposed => disposed;
public event Action UserHideRequested;
internal PingGraphPopoutWindow()
{
windowThread = new Thread(WindowThreadProc)
{
IsBackground = true,
Name = "ServX Ping Graph Popout"
};
windowThread.Start();
windowReady.WaitOne();
}
public void UpdateSnapshot(PingGraphPopoutSnapshot newSnapshot)
{
if (!disposed)
{
lock (snapshotLock)
{
snapshot = newSnapshot ?? PingGraphPopoutSnapshot.Empty;
}
if (windowHandle != IntPtr.Zero)
{
NativeMethods.InvalidateRect(windowHandle, IntPtr.Zero, bErase: false);
}
}
}
public void SetWindowVisibility(bool shouldShow)
{
if (!disposed && !(windowHandle == IntPtr.Zero))
{
visible = shouldShow;
if (shouldShow)
{
NativeMethods.ShowWindow(windowHandle, 4);
}
else
{
NativeMethods.ShowWindow(windowHandle, 0);
}
}
}
private void PositionTopRight()
{
if (!(windowHandle == IntPtr.Zero))
{
NativeMethods.RECT pvParam = default(NativeMethods.RECT);
NativeMethods.SystemParametersInfo(48u, 0u, ref pvParam, 0u);
NativeMethods.GetWindowRect(windowHandle, out var lpRect);
int num = lpRect.right - lpRect.left;
int num2 = lpRect.bottom - lpRect.top;
int x = pvParam.right - num - 10;
int y = pvParam.top + 10;
NativeMethods.SetWindowPos(windowHandle, NativeMethods.HWND_TOPMOST, x, y, 0, 0, 17u);
}
}
public void ForceClose()
{
if (!disposed)
{
forceClose = true;
if (windowHandle != IntPtr.Zero)
{
NativeMethods.PostMessage(windowHandle, 16u, IntPtr.Zero, IntPtr.Zero);
}
}
}
public void Dispose()
{
ForceClose();
if (windowThread.IsAlive)
{
windowThread.Join(1000);
}
}
private void WindowThreadProc()
{
IntPtr moduleHandle = NativeMethods.GetModuleHandle(null);
if (RegisterWindowClass(moduleHandle) == 0)
{
windowReady.Set();
return;
}
selfHandle = GCHandle.Alloc(this);
windowHandle = NativeMethods.CreateWindowEx(134217864, "ServX_PingGraphWindow", "ServX Ping Graph", 13565952, int.MinValue, int.MinValue, 550, 300, IntPtr.Zero, IntPtr.Zero, moduleHandle, IntPtr.Zero);
if (windowHandle == IntPtr.Zero)
{
windowReady.Set();
return;
}
NativeMethods.SetWindowLongPtr(windowHandle, -21, GCHandle.ToIntPtr(selfHandle));
PositionTopRight();
NativeMethods.ShowWindow(windowHandle, 0);
NativeMethods.UpdateWindow(windowHandle);
windowReady.Set();
NativeMethods.MSG lpMsg;
while (NativeMethods.GetMessage(out lpMsg, IntPtr.Zero, 0u, 0u) > 0)
{
NativeMethods.TranslateMessage(ref lpMsg);
NativeMethods.DispatchMessage(ref lpMsg);
}
CleanupWindow();
}
private ushort RegisterWindowClass(IntPtr instance)
{
lock (WindowClassLock)
{
if (windowClassRegistered)
{
return windowClassAtom;
}
NativeMethods.WNDCLASSEX wNDCLASSEX = default(NativeMethods.WNDCLASSEX);
wNDCLASSEX.cbSize = (uint)Marshal.SizeOf<NativeMethods.WNDCLASSEX>();
wNDCLASSEX.style = 3u;
wNDCLASSEX.lpfnWndProc = WindowProcPtr;
wNDCLASSEX.cbClsExtra = 0;
wNDCLASSEX.cbWndExtra = 0;
wNDCLASSEX.hInstance = instance;
wNDCLASSEX.hCursor = NativeMethods.LoadCursor(IntPtr.Zero, NativeMethods.IDC_ARROW);
wNDCLASSEX.hbrBackground = NativeMethods.CreateSolidBrush(BackgroundColor);
wNDCLASSEX.lpszClassName = "ServX_PingGraphWindow";
NativeMethods.WNDCLASSEX lpwcx = wNDCLASSEX;
ushort num = NativeMethods.RegisterClassEx(ref lpwcx);
if (num == 0)
{
int lastWin32Error = Marshal.GetLastWin32Error();
if (lastWin32Error != 1410)
{
ManualLogSource logger = Plugin.Logger;
if (logger != null)
{
logger.LogWarning((object)$"Failed to register popout window class (err {lastWin32Error}).");
}
return 0;
}
windowClassAtom = 1;
windowClassRegistered = true;
return windowClassAtom;
}
windowClassAtom = num;
windowClassRegistered = true;
return windowClassAtom;
}
}
private void CleanupWindow()
{
disposed = true;
if (selfHandle.IsAllocated)
{
selfHandle.Free();
}
windowHandle = IntPtr.Zero;
}
private void HandlePaint()
{
if (windowHandle == IntPtr.Zero)
{
return;
}
NativeMethods.PAINTSTRUCT lpPaint = default(NativeMethods.PAINTSTRUCT);
IntPtr intPtr = NativeMethods.BeginPaint(windowHandle, ref lpPaint);
if (intPtr == IntPtr.Zero)
{
return;
}
try
{
NativeMethods.GetClientRect(windowHandle, out var lpRect);
int num = lpRect.right - lpRect.left;
int num2 = lpRect.bottom - lpRect.top;
if (num <= 0 || num2 <= 0)
{
return;
}
IntPtr intPtr2 = NativeMethods.CreateCompatibleDC(intPtr);
if (intPtr2 == IntPtr.Zero)
{
DrawContents(intPtr, lpRect);
return;
}
IntPtr intPtr3 = NativeMethods.CreateCompatibleBitmap(intPtr, num, num2);
if (intPtr3 == IntPtr.Zero)
{
NativeMethods.DeleteDC(intPtr2);
DrawContents(intPtr, lpRect);
return;
}
IntPtr hgdiobj = NativeMethods.SelectObject(intPtr2, intPtr3);
try
{
DrawContents(intPtr2, lpRect);
NativeMethods.BitBlt(intPtr, 0, 0, num, num2, intPtr2, 0, 0, 13369376u);
}
finally
{
NativeMethods.SelectObject(intPtr2, hgdiobj);
NativeMethods.DeleteObject(intPtr3);
NativeMethods.DeleteDC(intPtr2);
}
}
finally
{
NativeMethods.EndPaint(windowHandle, ref lpPaint);
}
}
private void DrawContents(IntPtr hdc, NativeMethods.RECT clientRect)
{
FillRectangle(hdc, clientRect, BackgroundColor);
PingGraphPopoutSnapshot data;
lock (snapshotLock)
{
data = snapshot.Clone();
}
int num = ComputeStatsPanelHeight(hdc, data);
NativeMethods.RECT rect = clientRect;
rect.left += 16;
rect.right -= 16;
rect.top += 16;
rect.bottom = clientRect.bottom - 16 - num;
FillRectangle(hdc, rect, GraphBackgroundColor);
NativeMethods.RECT rect2 = clientRect;
rect2.top = rect.bottom;
FillRectangle(hdc, rect2, StatsBackgroundColor);
DrawGraphLines(hdc, rect, data);
DrawImprovedStats(hdc, rect2, data);
}
private void DrawGraphLines(IntPtr hdc, NativeMethods.RECT rect, PingGraphPopoutSnapshot data)
{
if (data.Average.Length >= 2)
{
int num = rect.right - rect.left;
int num2 = rect.bottom - rect.top;
if (num > 0 && num2 > 0)
{
float maxValue = ((data.GraphMaxValue > 0f) ? data.GraphMaxValue : 500f);
float step = (float)num / (float)Math.Max(data.Average.Length - 1, 1);
DrawSeries(hdc, rect, data.Min, maxValue, step, MinLineColor);
DrawSeries(hdc, rect, data.Average, maxValue, step, AvgLineColor);
DrawSeries(hdc, rect, data.Max, maxValue, step, MaxLineColor);
}
}
}
private void DrawSeries(IntPtr hdc, NativeMethods.RECT rect, float[] data, float maxValue, float step, int color)
{
if (data == null || data.Length < 2)
{
return;
}
IntPtr intPtr = NativeMethods.CreatePen(0, 2, color);
IntPtr hgdiobj = NativeMethods.SelectObject(hdc, intPtr);
try
{
for (int i = 1; i < data.Length; i++)
{
float num = Clamp(data[i - 1], 0f, maxValue);
float num2 = Clamp(data[i], 0f, maxValue);
int x = rect.left + (int)Math.Round(step * (float)(i - 1));
int nXEnd = rect.left + (int)Math.Round(step * (float)i);
int y = rect.bottom - (int)Math.Round(num / maxValue * (float)(rect.bottom - rect.top));
int nYEnd = rect.bottom - (int)Math.Round(num2 / maxValue * (float)(rect.bottom - rect.top));
NativeMethods.MoveToEx(hdc, x, y, IntPtr.Zero);
NativeMethods.LineTo(hdc, nXEnd, nYEnd);
}
}
finally
{
NativeMethods.SelectObject(hdc, hgdiobj);
NativeMethods.DeleteObject(intPtr);
}
}
private void DrawImprovedStats(IntPtr hdc, NativeMethods.RECT rect, PingGraphPopoutSnapshot data)
{
NativeMethods.SetBkMode(hdc, 1);
IntPtr intPtr = NativeMethods.CreateFontW(15, 0, 0, 0, 400, 0u, 0u, 0u, 1u, 0u, 0u, 5u, 0u, "Segoe UI");
IntPtr intPtr2 = NativeMethods.CreateFontW(13, 0, 0, 0, 400, 0u, 0u, 0u, 1u, 0u, 0u, 5u, 0u, "Segoe UI");
IntPtr hgdiobj = NativeMethods.SelectObject(hdc, intPtr);
DrawHorizontalLine(hdc, rect.left + 20, rect.right - 20, rect.top, DividerColor);
if (data.StatLines == null || data.StatLines.Length == 0)
{
NativeMethods.SetTextColor(hdc, TextColor);
NativeMethods.RECT rECT = default(NativeMethods.RECT);
rECT.left = rect.left + 20;
rECT.top = rect.top + 12;
rECT.right = rect.right - 20;
rECT.bottom = rect.top + 38;
NativeMethods.RECT lprc = rECT;
NativeMethods.DrawTextW(hdc, "Waiting for samples...", 21, ref lprc, 41u);
}
else
{
DrawFormattedStats(hdc, rect, data, intPtr, intPtr2);
}
NativeMethods.SelectObject(hdc, hgdiobj);
NativeMethods.DeleteObject(intPtr);
NativeMethods.DeleteObject(intPtr2);
}
private void DrawFormattedStats(IntPtr hdc, NativeMethods.RECT rect, PingGraphPopoutSnapshot data, IntPtr normalFont, IntPtr smallFont)
{
int num = (rect.right - rect.left - 60) / 2;
int num2 = rect.left + 30;
int x = num2 + num + 20;
int num3 = 26;
int num4 = 6;
int num5 = 5;
int num6 = 10;
int num7 = 5;
NativeMethods.SelectObject(hdc, normalFont);
Dictionary<string, PingGraphPopoutStatLine> dictionary = ParseStatLines(data.StatLines);
int num8 = 0;
if (dictionary.ContainsKey("Max"))
{
num8++;
}
if (dictionary.ContainsKey("Avg"))
{
num8++;
}
if (dictionary.ContainsKey("Min"))
{
num8++;
}
int num9 = 0;
if (dictionary.ContainsKey("Delta Avg"))
{
num9++;
}
if (dictionary.ContainsKey("Player"))
{
num9++;
}
if (dictionary.ContainsKey("TPS"))
{
num9++;
}
int num10 = Math.Max(num8, num9);
string text = (dictionary.ContainsKey("Time") ? ("Elapsed Time: " + dictionary["Time"].Value) : string.Empty);
int num11 = 0;
if (!string.IsNullOrEmpty(text))
{
NativeMethods.RECT rECT = default(NativeMethods.RECT);
rECT.left = 0;
rECT.top = 0;
rECT.right = 2000;
rECT.bottom = 0;
NativeMethods.RECT lprc = rECT;
NativeMethods.DrawTextW(hdc, text, text.Length, ref lprc, 1056u);
num11 = lprc.bottom - lprc.top;
}
int num12 = rect.bottom - num7;
int num13 = num12 - num11;
int num14 = num13 - num6;
int num15 = num10 * num3;
int num16 = num14 - num5 - num15;
int num17 = rect.top + num4;
if (num16 < num17)
{
num16 = num17;
int num18 = num16 + num15;
num14 = Math.Min(num18 + num5, num13 - num6);
}
int num19 = num16;
int num20 = num16;
if (dictionary.ContainsKey("Max"))
{
DrawStatItem(hdc, num2, num19, num, num3, "Maximum", dictionary["Max"].Value, ColorRef(240, 80, 80), normalFont, smallFont);
num19 += num3;
}
if (dictionary.ContainsKey("Avg"))
{
DrawStatItem(hdc, num2, num19, num, num3, "Average", dictionary["Avg"].Value, ColorRef(0, 200, 80), normalFont, smallFont);
num19 += num3;
}
if (dictionary.ContainsKey("Min"))
{
DrawStatItem(hdc, num2, num19, num, num3, "Minimum", dictionary["Min"].Value, ColorRef(204, 204, 255), normalFont, smallFont);
num19 += num3;
}
if (dictionary.ContainsKey("Delta Avg"))
{
DrawStatItem(hdc, x, num20, num, num3, "ΔAvg", dictionary["Delta Avg"].Value, ColorRef(255, 200, 60), normalFont, smallFont);
num20 += num3;
}
if (dictionary.ContainsKey("Player"))
{
DrawStatItem(hdc, x, num20, num, num3, "Player", dictionary["Player"].Value, ColorRef(180, 200, 255), normalFont, smallFont);
num20 += num3;
}
if (dictionary.ContainsKey("TPS"))
{
DrawStatItem(hdc, x, num20, num, num3, "TPS", dictionary["TPS"].Value, ColorRef(255, 105, 180), normalFont, smallFont);
num20 += num3;
}
DrawHorizontalLine(hdc, rect.left + 40, rect.right - 40, num14, DividerColor);
if (!string.IsNullOrEmpty(text))
{
NativeMethods.SetTextColor(hdc, ColorRef(200, 200, 200));
NativeMethods.RECT rECT = default(NativeMethods.RECT);
rECT.left = rect.left + 20;
rECT.top = num13;
rECT.right = rect.right - 20;
rECT.bottom = num12;
NativeMethods.RECT lprc2 = rECT;
NativeMethods.DrawTextW(hdc, text, text.Length, ref lprc2, 41u);
}
}
private void DrawStatItem(IntPtr hdc, int x, int y, int width, int height, string label, string value, int color, IntPtr normalFont, IntPtr smallFont)
{
NativeMethods.SelectObject(hdc, normalFont);
NativeMethods.SetTextColor(hdc, color);
NativeMethods.RECT rECT = default(NativeMethods.RECT);
rECT.left = x;
rECT.top = y;
rECT.right = x + 80;
rECT.bottom = y + height;
NativeMethods.RECT lprc = rECT;
NativeMethods.DrawTextW(hdc, label + ":", (label + ":").Length, ref lprc, 36u);
NativeMethods.SelectObject(hdc, smallFont);
NativeMethods.SetTextColor(hdc, TextColor);
rECT = default(NativeMethods.RECT);
rECT.left = x + 85;
rECT.top = y;
rECT.right = x + width;
rECT.bottom = y + height;
NativeMethods.RECT lprc2 = rECT;
NativeMethods.DrawTextW(hdc, value, value.Length, ref lprc2, 36u);
}
private void DrawHorizontalLine(IntPtr hdc, int x1, int x2, int y, int color)
{
IntPtr intPtr = NativeMethods.CreatePen(0, 1, color);
IntPtr hgdiobj = NativeMethods.SelectObject(hdc, intPtr);
NativeMethods.MoveToEx(hdc, x1, y, IntPtr.Zero);
NativeMethods.LineTo(hdc, x2, y);
NativeMethods.SelectObject(hdc, hgdiobj);
NativeMethods.DeleteObject(intPtr);
}
private Dictionary<string, PingGraphPopoutStatLine> ParseStatLines(PingGraphPopoutStatLine[] lines)
{
Dictionary<string, PingGraphPopoutStatLine> dictionary = new Dictionary<string, PingGraphPopoutStatLine>();
if (lines == null)
{
return dictionary;
}
foreach (PingGraphPopoutStatLine pingGraphPopoutStatLine in lines)
{
if (pingGraphPopoutStatLine != null && !string.IsNullOrEmpty(pingGraphPopoutStatLine.Label))
{
dictionary[pingGraphPopoutStatLine.Label] = pingGraphPopoutStatLine;
}
}
return dictionary;
}
private void HandleClose()
{
if (forceClose)
{
NativeMethods.DestroyWindow(windowHandle);
return;
}
visible = false;
NativeMethods.ShowWindow(windowHandle, 0);
this.UserHideRequested?.Invoke();
}
internal static int ColorRef(int r, int g, int b)
{
return (r & 0xFF) | ((g & 0xFF) << 8) | ((b & 0xFF) << 16);
}
private static float Clamp(float value, float min, float max)
{
if (value < min)
{
return min;
}
if (value > max)
{
return max;
}
return value;
}
private static void FillRectangle(IntPtr hdc, NativeMethods.RECT rect, int colorRef)
{
IntPtr intPtr = NativeMethods.CreateSolidBrush(colorRef);
NativeMethods.FillRect(hdc, ref rect, intPtr);
NativeMethods.DeleteObject(intPtr);
}
private int ComputeStatsPanelHeight(IntPtr hdc, PingGraphPopoutSnapshot data)
{
Dictionary<string, PingGraphPopoutStatLine> dictionary = ParseStatLines(data?.StatLines);
int num = 0;
if (dictionary.ContainsKey("Max"))
{
num++;
}
if (dictionary.ContainsKey("Avg"))
{
num++;
}
if (dictionary.ContainsKey("Min"))
{
num++;
}
int num2 = 0;
if (dictionary.ContainsKey("Delta Avg"))
{
num2++;
}
if (dictionary.ContainsKey("Player"))
{
num2++;
}
if (dictionary.ContainsKey("TPS"))
{
num2++;
}
int num3 = Math.Max(num, num2);
string text = (dictionary.ContainsKey("Time") ? ("Elapsed Time: " + dictionary["Time"].Value) : "Elapsed Time: 00:00:00");
IntPtr intPtr = NativeMethods.CreateFontW(15, 0, 0, 0, 400, 0u, 0u, 0u, 1u, 0u, 0u, 5u, 0u, "Segoe UI");
IntPtr hgdiobj = NativeMethods.SelectObject(hdc, intPtr);
NativeMethods.RECT rECT = default(NativeMethods.RECT);
rECT.left = 0;
rECT.top = 0;
rECT.right = 2000;
rECT.bottom = 0;
NativeMethods.RECT lprc = rECT;
NativeMethods.DrawTextW(hdc, text, text.Length, ref lprc, 1056u);
int num4 = lprc.bottom - lprc.top;
NativeMethods.SelectObject(hdc, hgdiobj);
NativeMethods.DeleteObject(intPtr);
return 6 + num3 * 26 + 5 + 10 + num4 + 5;
}
private static IntPtr WindowProcedure(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam)
{
IntPtr windowLongPtr = NativeMethods.GetWindowLongPtr(hwnd, -21);
PingGraphPopoutWindow pingGraphPopoutWindow = null;
if (windowLongPtr != IntPtr.Zero)
{
pingGraphPopoutWindow = GCHandle.FromIntPtr(windowLongPtr).Target as PingGraphPopoutWindow;
}
switch (msg)
{
case 16u:
pingGraphPopoutWindow?.HandleClose();
return IntPtr.Zero;
case 2u:
NativeMethods.PostQuitMessage(0);
return IntPtr.Zero;
case 15u:
pingGraphPopoutWindow?.HandlePaint();
return IntPtr.Zero;
default:
return NativeMethods.DefWindowProc(hwnd, msg, wParam, lParam);
}
}
}
internal sealed class PingGraphPopoutStatLine
{
public static readonly PingGraphPopoutStatLine Empty = new PingGraphPopoutStatLine();
public string Label { get; internal set; } = string.Empty;
public string Value { get; internal set; } = string.Empty;
public int ColorRef { get; internal set; } = PingGraphPopoutWindow.ColorRef(238, 238, 238);
public PingGraphPopoutStatLine()
{
}
public PingGraphPopoutStatLine(string label, string value, int colorRef)
{
Label = label ?? string.Empty;
Value = value ?? string.Empty;
ColorRef = colorRef;
}
public PingGraphPopoutStatLine Clone()
{
return new PingGraphPopoutStatLine(Label, Value, ColorRef);
}
}
internal sealed class PingGraphPopoutSnapshot
{
public static readonly PingGraphPopoutSnapshot Empty = new PingGraphPopoutSnapshot();
public float[] Max { get; set; } = Array.Empty<float>();
public float[] Average { get; set; } = Array.Empty<float>();
public float[] Min { get; set; } = Array.Empty<float>();
public float GraphMaxValue { get; set; } = 500f;
public PingGraphPopoutStatLine[] StatLines { get; set; } = Array.Empty<PingGraphPopoutStatLine>();
public PingGraphPopoutSnapshot Clone()
{
return new PingGraphPopoutSnapshot
{
Max = (float[])(Max?.Clone() ?? Array.Empty<float>()),
Average = (float[])(Average?.Clone() ?? Array.Empty<float>()),
Min = (float[])(Min?.Clone() ?? Array.Empty<float>()),
GraphMaxValue = GraphMaxValue,
StatLines = CloneStatLines(StatLines)
};
}
private static PingGraphPopoutStatLine[] CloneStatLines(PingGraphPopoutStatLine[] source)
{
if (source == null || source.Length == 0)
{
return Array.Empty<PingGraphPopoutStatLine>();
}
PingGraphPopoutStatLine[] array = new PingGraphPopoutStatLine[source.Length];
for (int i = 0; i < source.Length; i++)
{
array[i] = source[i]?.Clone() ?? PingGraphPopoutStatLine.Empty;
}
return array;
}
}
[BepInPlugin("ServX", "ServX", "1.3.0")]
public class Plugin : BaseUnityPlugin
{
[CompilerGenerated]
private static class <>O
{
public static UnityAction <0>__ApplySettings;
}
internal static ManualLogSource Logger;
internal static ConfigEntry<bool> enablePingGraph;
internal static ConfigEntry<bool> enableSessionSummary;
internal static ConfigEntry<bool> usePopoutGraph;
internal static ConfigEntry<KeyCode> toggleGraphKey;
internal static ConfigEntry<KeyCode> resetGraphKey;
private void Awake()
{
//IL_0028: Unknown result type (might be due to invalid IL or missing references)
//IL_002e: Expected O, but got Unknown
//IL_0041: Unknown result type (might be due to invalid IL or missing references)
//IL_004b: Expected O, but got Unknown
//IL_0061: Unknown result type (might be due to invalid IL or missing references)
//IL_0066: Unknown result type (might be due to invalid IL or missing references)
//IL_006c: Expected O, but got Unknown
Logger = ((BaseUnityPlugin)this).Logger;
InitConfig();
Logger.LogInfo((object)"ServX loaded!");
Harmony val = new Harmony("ServX");
val.PatchAll();
Settings.OnInitialized.AddListener(new UnityAction(AddSettings));
UnityEvent onApplySettings = Settings.OnApplySettings;
object obj = <>O.<0>__ApplySettings;
if (obj == null)
{
UnityAction val2 = ApplySettings;
<>O.<0>__ApplySettings = val2;
obj = (object)val2;
}
onApplySettings.AddListener((UnityAction)obj);
enablePingGraph.SettingChanged += delegate
{
ApplySettings();
};
enableSessionSummary.SettingChanged += delegate
{
ApplySettings();
};
usePopoutGraph.SettingChanged += delegate
{
PingGraph.RefreshDisplayMode();
};
ApplySettings();
((Component)this).gameObject.AddComponent<PingGraph>();
}
private void InitConfig()
{
//IL_0011: Unknown result type (might be due to invalid IL or missing references)
//IL_0022: Unknown result type (might be due to invalid IL or missing references)
//IL_002c: Expected O, but got Unknown
//IL_002c: Expected O, but got Unknown
//IL_0041: Unknown result type (might be due to invalid IL or missing references)
//IL_0052: Unknown result type (might be due to invalid IL or missing references)
//IL_005c: Expected O, but got Unknown
//IL_005c: Expected O, but got Unknown
//IL_0071: Unknown result type (might be due to invalid IL or missing references)
//IL_0082: Unknown result type (might be due to invalid IL or missing references)
//IL_008c: Expected O, but got Unknown
//IL_008c: Expected O, but got Unknown
//IL_00a1: Unknown result type (might be due to invalid IL or missing references)
//IL_00b6: Unknown result type (might be due to invalid IL or missing references)
//IL_00c0: Expected O, but got Unknown
//IL_00c0: Expected O, but got Unknown
//IL_00d5: Unknown result type (might be due to invalid IL or missing references)
//IL_00ea: Unknown result type (might be due to invalid IL or missing references)
//IL_00f4: Expected O, but got Unknown
//IL_00f4: Expected O, but got Unknown
enablePingGraph = ((BaseUnityPlugin)this).Config.Bind<bool>(new ConfigDefinition("ServX", "Enable Ping Graph"), false, new ConfigDescription("Show the ping graph while the Who tab is open.", (AcceptableValueBase)null, Array.Empty<object>()));
enableSessionSummary = ((BaseUnityPlugin)this).Config.Bind<bool>(new ConfigDefinition("ServX", "Show Session Summary"), true, new ConfigDescription("Log a ping session summary when the graph closes.", (AcceptableValueBase)null, Array.Empty<object>()));
usePopoutGraph = ((BaseUnityPlugin)this).Config.Bind<bool>(new ConfigDefinition("ServX", "Pop Out Ping Graph"), false, new ConfigDescription("Show the ping graph in a separate desktop window instead of the in-game overlay.", (AcceptableValueBase)null, Array.Empty<object>()));
toggleGraphKey = ((BaseUnityPlugin)this).Config.Bind<KeyCode>(new ConfigDefinition("ServX", "Toggle Graph Key"), (KeyCode)280, new ConfigDescription("Key that hides/shows the ping graph while enabled.", (AcceptableValueBase)null, Array.Empty<object>()));
resetGraphKey = ((BaseUnityPlugin)this).Config.Bind<KeyCode>(new ConfigDefinition("ServX", "Restart Graph Key"), (KeyCode)281, new ConfigDescription("Key that restarts the ping graph session.", (AcceptableValueBase)null, Array.Empty<object>()));
}
internal static void ApplySettings()
{
bool flag = !(enablePingGraph?.Value ?? true);
bool flag2 = !(enableSessionSummary?.Value ?? true);
bool flag3 = PingTracker.AveragePings.Count > 0;
bool flag4 = !PingTracker.DisableGraph;
if (flag && !flag2 && flag4 && flag3)
{
PingTracker.LogSessionSummaryIfAvailable();
}
PingTracker.DisableGraph = flag;
PingTracker.DisableSummary = flag2;
if (flag)
{
PingTracker.ResetSessionMetrics();
}
PingGraph.RefreshDisplayMode();
}
private void AddSettings()
{
SettingsTab modTab = Settings.ModTab;
modTab.AddHeader("ServX");
modTab.AddToggle("Enable ping graph", enablePingGraph);
modTab.AddToggle("Show session summary", enableSessionSummary);
modTab.AddToggle("Pop out ping graph window", usePopoutGraph);
modTab.AddKeyButton("Toggle ping graph display", toggleGraphKey);
modTab.AddKeyButton("Restart ping graph session", resetGraphKey);
}
}
public static class PingTracker
{
public static List<float> AveragePings = new List<float>();
public static List<float> MaxPings = new List<float>();
public static List<float> MinPings = new List<float>();
public static List<int> PlayerPings = new List<int>();
public static List<int> TPSHistory = new List<int>();
public static bool DisableGraph;
public static bool DisableSummary;
public static float? FirstAveragePing;
public static float? HighestPing;
public static float? LowestPing;
public static float Timer;
public static int Ticks;
public static int LastTPS;
public static float PingUpdateTimer;
public static float LastSampleTime;
public static void ResetSessionMetrics()
{
AveragePings.Clear();
MaxPings.Clear();
MinPings.Clear();
PlayerPings.Clear();
TPSHistory.Clear();
FirstAveragePing = null;
HighestPing = null;
LowestPing = null;
Timer = 0f;
Ticks = 0;
LastTPS = 0;
LastSampleTime = 0f;
}
public static bool TryCaptureSnapshot(out float avg, out float max, out float min, out int playerPing)
{
avg = (max = (min = 0f));
playerPing = 0;
if (!Object.op_Implicit((Object)(object)Player._mainPlayer))
{
return false;
}
Player[] array = Object.FindObjectsOfType<Player>();
if (array == null || array.Length == 0)
{
return false;
}
List<int> list = new List<int>(array.Length);
Player[] array2 = array;
foreach (Player val in array2)
{
if (!Object.op_Implicit((Object)(object)val))
{
continue;
}
int latency = val._latency;
if (latency > 0)
{
list.Add(latency);
if ((Object)(object)val == (Object)(object)Player._mainPlayer)
{
playerPing = latency;
}
}
}
if (list.Count == 0)
{
return false;
}
avg = (float)list.Average();
max = list.Max();
min = list.Min();
return true;
}
public static void LogSessionSummaryIfAvailable()
{
if (!DisableSummary && AveragePings.Count != 0)
{
float num = AveragePings.Average();
float valueOrDefault = HighestPing.GetValueOrDefault(num);
float valueOrDefault2 = LowestPing.GetValueOrDefault(num);
float num2 = ((PlayerPings.Count > 0) ? ((float)PlayerPings.Average()) : 0f);
float num3 = ((TPSHistory.Count > 0) ? ((float)TPSHistory.Average()) : 0f);
float num4 = AveragePings.Last() - (FirstAveragePing ?? AveragePings.First());
bool flag = Object.op_Implicit((Object)(object)Player._mainPlayer) && Player._mainPlayer._isHostPlayer;
TimeSpan timeSpan = TimeSpan.FromSeconds(Timer);
Plugin.Logger.LogInfo((object)string.Empty);
Plugin.Logger.LogInfo((object)"======= Session Summary =======");
Plugin.Logger.LogInfo((object)$" * Average Ping: {num:F1} ms");
Plugin.Logger.LogInfo((object)$" * Delta Avg Ping: {num4:+0.0;-0.0} ms");
Plugin.Logger.LogInfo((object)$" * Highest Ping: {valueOrDefault:F1} ms");
Plugin.Logger.LogInfo((object)$" * Lowest Ping: {valueOrDefault2:F1} ms");
if (!flag && num2 > 0f)
{
Plugin.Logger.LogInfo((object)$" * Player Ping: {num2:F1} ms (average)");
}
if (TPSHistory.Count > 0 && num3 > 0f)
{
Plugin.Logger.LogInfo((object)$" * TPS: {num3:F1} (average)");
}
Plugin.Logger.LogInfo((object)"--------------------------------");
Plugin.Logger.LogInfo((object)$" Total Time Elapsed: {timeSpan:hh\\:mm\\:ss}");
Plugin.Logger.LogInfo((object)("================================" + Environment.NewLine));
}
}
}
public static class MyPluginInfo
{
public const string PLUGIN_GUID = "ServX";
public const string PLUGIN_NAME = "ServX";
public const string PLUGIN_VERSION = "1.3.0";
}
}