Decompiled source of LagWatch v0.3.7

LagWatch.dll

Decompiled 15 hours ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;
using UnityEngine.EventSystems;
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(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[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;
		}
	}
}
namespace LagWatch
{
	[BepInPlugin("captain.LagWatch", "Lag Watch", "0.3.7")]
	public class Plugin : BaseUnityPlugin
	{
		public const string PluginGuid = "captain.LagWatch";

		public const string Version = "0.3.7";

		internal const string Protocol = "2";

		internal static ManualLogSource Log;

		internal static ConfigEntry<float> SampleIntervalSec;

		internal static ConfigEntry<float> ZdoScanIntervalSec;

		internal static ConfigEntry<bool> LogSamples;

		internal static ConfigEntry<bool> AutoWarnEnabled;

		internal static ConfigEntry<float> WarnCooldownSec;

		internal static ConfigEntry<float> PingWarnMs;

		internal static ConfigEntry<float> PingBadMs;

		internal static ConfigEntry<float> ZdoWarn;

		internal static ConfigEntry<float> ZdoBad;

		internal static ConfigEntry<float> FpsWarn;

		internal static ConfigEntry<float> FpsBad;

		internal static ConfigEntry<float> QueueWarnKB;

		internal static ConfigEntry<float> QueueBadKB;

		internal static ConfigEntry<float> TrafficWarnKBs;

		internal static ConfigEntry<float> TrafficBadKBs;

		internal static ConfigEntry<float> ServerCpuWarn;

		internal static ConfigEntry<float> ServerCpuBad;

		internal static ConfigEntry<float> ServerFrameWarnMs;

		internal static ConfigEntry<float> ServerFrameBadMs;

		internal static ConfigEntry<bool> AllowServerClear;

		internal static ConfigEntry<int> ClearBatchPerFrame;

		internal static ConfigEntry<bool> DiagEnabled;

		internal static ConfigEntry<float> DiagReportIntervalMin;

		internal static ConfigEntry<bool> DiagIncludeWarnings;

		internal static ConfigEntry<float> DiagHitchMs;

		internal static ConfigEntry<int> DiagMaxDistinctErrors;

		internal static ConfigEntry<int> DiagMaxLogsPerSec;

		internal static ConfigEntry<int> DiagRetainDays;

		internal static ConfigEntry<float> DiagMaxFileMB;

		internal static ConfigEntry<KeyboardShortcut> PanelHotkey;

		internal static ConfigEntry<bool> ReportEnabled;

		internal static ConfigEntry<float> ReportIntervalSec;

		private Harmony _harmony;

		private void Awake()
		{
			//IL_003a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0044: Expected O, but got Unknown
			//IL_0077: Unknown result type (might be due to invalid IL or missing references)
			//IL_0081: Expected O, but got Unknown
			//IL_00f4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00fe: Expected O, but got Unknown
			//IL_02b3: Unknown result type (might be due to invalid IL or missing references)
			//IL_02bd: Expected O, but got Unknown
			//IL_0310: Unknown result type (might be due to invalid IL or missing references)
			//IL_031a: Expected O, but got Unknown
			//IL_036d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0377: Expected O, but got Unknown
			//IL_03a7: Unknown result type (might be due to invalid IL or missing references)
			//IL_03b1: Expected O, but got Unknown
			//IL_03e1: Unknown result type (might be due to invalid IL or missing references)
			//IL_03eb: Expected O, but got Unknown
			//IL_0413: Unknown result type (might be due to invalid IL or missing references)
			//IL_041d: Expected O, but got Unknown
			//IL_0450: Unknown result type (might be due to invalid IL or missing references)
			//IL_045a: Expected O, but got Unknown
			//IL_0509: Unknown result type (might be due to invalid IL or missing references)
			//IL_056b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0575: Expected O, but got Unknown
			//IL_0580: Unknown result type (might be due to invalid IL or missing references)
			//IL_058a: Expected O, but got Unknown
			Log = ((BaseUnityPlugin)this).Logger;
			SampleIntervalSec = ((BaseUnityPlugin)this).Config.Bind<float>("Server", "SampleIntervalSec", 5f, new ConfigDescription("Seconds between per-peer ping/traffic samples.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 60f), Array.Empty<object>()));
			ZdoScanIntervalSec = ((BaseUnityPlugin)this).Config.Bind<float>("Server", "ZdoScanIntervalSec", 30f, new ConfigDescription("Seconds between full ZDO ownership scans (costly on large worlds).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(10f, 600f), Array.Empty<object>()));
			LogSamples = ((BaseUnityPlugin)this).Config.Bind<bool>("Server", "LogSamples", false, "Write every sample to the server log (for verification).");
			AutoWarnEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Server", "AutoWarnEnabled", false, "Automatically send the notice to a player whose status stays red. Admins can also toggle this in the panel.");
			WarnCooldownSec = ((BaseUnityPlugin)this).Config.Bind<float>("Server", "WarnCooldownSec", 120f, new ConfigDescription("Minimum seconds between notices to the same player (manual and automatic).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(10f, 3600f), Array.Empty<object>()));
			PingWarnMs = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "PingWarnMs", 150f, "Ping (ms) at which a player turns orange.");
			PingBadMs = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "PingBadMs", 300f, "Ping (ms) at which a player turns red.");
			ZdoWarn = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "OwnedObjectsWarn", 3000f, "Owned ZDO count for orange.");
			ZdoBad = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "OwnedObjectsBad", 8000f, "Owned ZDO count for red.");
			FpsWarn = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "FpsWarn", 30f, "Client average FPS at or below this is orange (client-reported).");
			FpsBad = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "FpsBad", 15f, "Client average FPS at or below this is red (client-reported).");
			QueueWarnKB = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "SendQueueWarnKB", 50f, "Server send queue toward the player (KB) for orange.");
			QueueBadKB = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "SendQueueBadKB", 200f, "Server send queue toward the player (KB) for red.");
			TrafficWarnKBs = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "TrafficWarnKBs", 150f, "In+out traffic (KB/s) for orange.");
			TrafficBadKBs = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "TrafficBadKBs", 400f, "In+out traffic (KB/s) for red.");
			AllowServerClear = ((BaseUnityPlugin)this).Config.Bind<bool>("Maintenance", "AllowServerClear", true, "Allow admins to run Server Clear from the panel (deletes ground items, hostile monsters and temporary debris after a preview and confirmation). Set to false to disable it completely.");
			ClearBatchPerFrame = ((BaseUnityPlugin)this).Config.Bind<int>("Maintenance", "ClearBatchPerFrame", 100, new ConfigDescription("Objects deleted per frame during Server Clear. Lower values spread the work out and avoid server stalls.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(10, 2000), Array.Empty<object>()));
			DiagEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Diagnostics", "Enabled", false, "Record server load factors and frequent error messages to BepInEx/LagWatch/diag-YYYY-MM-DD.jsonl for later analysis. Off = no listener and no per-frame work. Can be toggled while running.");
			DiagReportIntervalMin = ((BaseUnityPlugin)this).Config.Bind<float>("Diagnostics", "ReportIntervalMin", 10f, new ConfigDescription("Minutes between written records (one line per record).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 120f), Array.Empty<object>()));
			DiagIncludeWarnings = ((BaseUnityPlugin)this).Config.Bind<bool>("Diagnostics", "IncludeWarnings", false, "Also count warnings, not only errors. Applied when diagnostics start.");
			DiagHitchMs = ((BaseUnityPlugin)this).Config.Bind<float>("Diagnostics", "HitchMs", 100f, new ConfigDescription("A single frame at or above this many ms counts as a hitch.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(30f, 2000f), Array.Empty<object>()));
			DiagMaxDistinctErrors = ((BaseUnityPlugin)this).Config.Bind<int>("Diagnostics", "MaxDistinctErrors", 300, new ConfigDescription("Cap on distinct error messages tracked (memory limit). Applied when diagnostics start.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(50, 2000), Array.Empty<object>()));
			DiagMaxLogsPerSec = ((BaseUnityPlugin)this).Config.Bind<int>("Diagnostics", "MaxLogsPerSec", 500, new ConfigDescription("Log events processed per second; the rest are only counted as dropped (protects the server during error storms). Applied when diagnostics start.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(50, 10000), Array.Empty<object>()));
			DiagRetainDays = ((BaseUnityPlugin)this).Config.Bind<int>("Diagnostics", "RetainDays", 7, new ConfigDescription("Delete record files older than this many days.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 90), Array.Empty<object>()));
			DiagMaxFileMB = ((BaseUnityPlugin)this).Config.Bind<float>("Diagnostics", "MaxFileMB", 5f, new ConfigDescription("Stop writing to today's file after it reaches this size.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 100f), Array.Empty<object>()));
			ServerCpuWarn = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "ServerCpuWarn", 85f, "Server process CPU for Fair (yellow). 100 = one full core, so values above 100 mean several cores are busy.");
			ServerCpuBad = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "ServerCpuBad", 130f, "Server process CPU for Struggling (red).");
			ServerFrameWarnMs = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "ServerFrameWarnMs", 50f, "Average server frame time (ms) for Fair (yellow).");
			ServerFrameBadMs = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "ServerFrameBadMs", 100f, "Average server frame time (ms) for Struggling (red).");
			PanelHotkey = ((BaseUnityPlugin)this).Config.Bind<KeyboardShortcut>("Client", "PanelHotkey", new KeyboardShortcut((KeyCode)291, Array.Empty<KeyCode>()), "Opens the lag panel (admins only).");
			ReportEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Client", "ReportEnabled", true, "Send your FPS and installed mod list to the server so admins can diagnose lag.");
			ReportIntervalSec = ((BaseUnityPlugin)this).Config.Bind<float>("Client", "ReportIntervalSec", 5f, new ConfigDescription("Seconds between client reports.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(2f, 60f), Array.Empty<object>()));
			_harmony = new Harmony("captain.LagWatch");
			_harmony.PatchAll();
			((Component)this).gameObject.AddComponent<PeerSampler>();
			((Component)this).gameObject.AddComponent<ServerHealth>();
			((Component)this).gameObject.AddComponent<ServerMaintenance>();
			((Component)this).gameObject.AddComponent<ClientReporter>();
			((Component)this).gameObject.AddComponent<LagPanel>();
		}

		private void OnDestroy()
		{
			Harmony harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
		}
	}
	internal class ClientReporter : MonoBehaviour
	{
		private const float SliceSec = 0.5f;

		private const float WarmupSec = 10f;

		private const int ModsEveryNReports = 12;

		private float _inGameSince = -1f;

		private float _winTime;

		private float _winFrames;

		private float _sliceTime;

		private float _sliceFrames;

		private float _minSliceFps = float.MaxValue;

		private const int MaxUnacked = 3;

		private int _reportCount;

		private int _unacked;

		private int _session = -1;

		private string _modsCache;

		private int _modCount;

		private void Update()
		{
			if (!Plugin.ReportEnabled.Value || (Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsDedicated())
			{
				return;
			}
			if ((Object)(object)Player.m_localPlayer == (Object)null)
			{
				_inGameSince = -1f;
				ResetWindow();
				return;
			}
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			if (_inGameSince < 0f)
			{
				_inGameSince = realtimeSinceStartup;
			}
			if (!(realtimeSinceStartup - _inGameSince < 10f))
			{
				float unscaledDeltaTime = Time.unscaledDeltaTime;
				_winTime += unscaledDeltaTime;
				_winFrames += 1f;
				_sliceTime += unscaledDeltaTime;
				_sliceFrames += 1f;
				if (_sliceTime >= 0.5f)
				{
					_minSliceFps = Mathf.Min(_minSliceFps, _sliceFrames / _sliceTime);
					_sliceTime = 0f;
					_sliceFrames = 0f;
				}
				if (_winTime >= Plugin.ReportIntervalSec.Value)
				{
					Send();
				}
			}
		}

		private void Send()
		{
			if (_session != LagWatchRpc.Session)
			{
				_session = LagWatchRpc.Session;
				_unacked = 0;
				_reportCount = 0;
			}
			if (!LagWatchRpc.ServerAcked)
			{
				if (_unacked >= 3)
				{
					ResetWindow();
					return;
				}
				_unacked++;
			}
			float num = _winFrames / _winTime;
			float minFps = ((_minSliceFps == float.MaxValue) ? num : _minSliceFps);
			string mods = "";
			if (_reportCount % 12 == 0)
			{
				mods = GetMods();
			}
			_reportCount++;
			LagWatchRpc.SendReport(num, minFps, _modCount, mods);
			ResetWindow();
		}

		private void ResetWindow()
		{
			_winTime = (_winFrames = (_sliceTime = (_sliceFrames = 0f)));
			_minSliceFps = float.MaxValue;
		}

		private string GetMods()
		{
			if (_modsCache != null)
			{
				return _modsCache;
			}
			List<string> list = new List<string>();
			foreach (PluginInfo value in Chainloader.PluginInfos.Values)
			{
				list.Add(value.Metadata.Name + " " + value.Metadata.Version);
			}
			list.Sort();
			_modCount = list.Count;
			_modsCache = string.Join("|", list);
			return _modsCache;
		}
	}
	internal class NoticeData
	{
		public byte PingLv;

		public byte ZdoLv;

		public byte FpsLv;

		public byte QueueLv;

		public byte TrafficLv;

		public int Ping;

		public int Zdo;

		public int QueueKB;

		public int TrafficKBs;

		public float Fps;
	}
	internal static class NoticeText
	{
		internal static List<string> Build(NoticeData n)
		{
			List<string> list = new List<string>();
			if (n.PingLv > 0)
			{
				list.Add(Strings.Fmt("hintPing", n.Ping));
			}
			if (n.ZdoLv > 0)
			{
				list.Add(Strings.Fmt("hintZdo", n.Zdo));
			}
			if (n.FpsLv > 0)
			{
				list.Add(Strings.Fmt("hintFps", n.Fps.ToString("F0")));
			}
			if (n.QueueLv > 0)
			{
				list.Add(Strings.Fmt("hintQueue", n.QueueKB));
			}
			if (n.TrafficLv > 0)
			{
				list.Add(Strings.Fmt("hintTraffic", n.TrafficKBs));
			}
			list.Insert(0, Strings.Get((list.Count > 0) ? "noticeHead" : "noticeGeneric"));
			return list;
		}

		internal static void Show(NoticeData n)
		{
			List<string> list = Build(n);
			foreach (string item in list)
			{
				Plugin.Log.LogInfo((object)("[LagWatch] notice: " + item));
			}
			MessageHud instance = MessageHud.instance;
			if (!((Object)(object)instance == (Object)null))
			{
				instance.ShowMessage((MessageType)2, list[0], 0, (Sprite)null, false, true);
				for (int i = 1; i < list.Count; i++)
				{
					instance.ShowMessage((MessageType)1, list[i], 0, (Sprite)null, false, true);
				}
			}
		}
	}
	internal static class Strings
	{
		private static readonly Dictionary<string, string[]> T = new Dictionary<string, string[]>
		{
			["title"] = new string[2] { "서버 랙 모니터", "Server Lag Monitor" },
			["colPlayer"] = new string[2] { "접속자", "Player" },
			["colPing"] = new string[2] { "핑", "Ping" },
			["colFps"] = new string[2] { "FPS", "FPS" },
			["colObjects"] = new string[2] { "소유 오브젝트", "Owned obj." },
			["colQueue"] = new string[2] { "송신 대기", "Send queue" },
			["colTraffic"] = new string[2] { "트래픽", "Traffic" },
			["colMods"] = new string[2] { "모드", "Mods" },
			["colStatus"] = new string[2] { "상태", "Status" },
			["stOk"] = new string[2] { "정상", "OK" },
			["stWarn"] = new string[2] { "주의", "Caution" },
			["stBad"] = new string[2] { "위험", "Critical" },
			["summary"] = new string[2] { "정상 {0}  주의 {1}  위험 {2}", "OK {0}  Caution {1}  Critical {2}" },
			["btnSend"] = new string[2] { "안내 보내기", "Send notice" },
			["btnSending"] = new string[2] { "전송 중", "Sending" },
			["btnSent"] = new string[2] { "전송됨", "Sent" },
			["btnCooldown"] = new string[2] { "잠시 후", "Wait" },
			["btnNoMod"] = new string[2] { "모드 없음", "No mod" },
			["autoOn"] = new string[2] { "자동 경고: 켜짐", "Auto warn: ON" },
			["autoOff"] = new string[2] { "자동 경고: 꺼짐", "Auto warn: OFF" },
			["close"] = new string[2] { "닫기", "Close" },
			["hint"] = new string[2] { "ESC 또는 단축키로 닫기. 행을 클릭하면 상세 정보가 표시됩니다.", "Press ESC or the hotkey to close. Click a row for details." },
			["empty"] = new string[2] { "접속자 정보를 기다리는 중입니다. 서버에 LagWatch 모드가 설치되어 있어야 합니다.", "Waiting for data. The server must have LagWatch installed." },
			["adminOnly"] = new string[2] { "LagWatch 패널은 관리자 전용입니다.", "The LagWatch panel is admin only." },
			["detailEmpty"] = new string[2] { "접속자를 선택하세요.", "Select a player." },
			["detail"] = new string[2] { "{0} - {1}\n핑 평균 {2}ms / 최대 {3}ms   소유 오브젝트 {4}개   송신 대기 {5}KB   트래픽 {6}KB/s\nFPS 평균 {7} / 최저 {8}   설치 모드 {9}개\n상위 오브젝트: {10}\n모드: {11}", "{0} - {1}\nPing avg {2}ms / max {3}ms   Owned objects {4}   Send queue {5}KB   Traffic {6}KB/s\nFPS avg {7} / min {8}   Mods {9}\nTop objects: {10}\nMods: {11}" },
			["notReported"] = new string[2] { "미보고(클라이언트에 모드 없음)", "not reported (client lacks the mod)" },
			["modsMore"] = new string[2] { " 외 {0}개", " +{0} more" },
			["srvGood"] = new string[2] { "좋음", "Good" },
			["srvFair"] = new string[2] { "보통", "Fair" },
			["srvBad"] = new string[2] { "힘듦", "Struggling" },
			["srvState"] = new string[2] { "서버 상태", "Server" },
			["srvPing"] = new string[2] { "내 핑", "My ping" },
			["srvFrame"] = new string[2] { "프레임 평균/최대", "Frame avg/max" },
			["srvHitch"] = new string[2] { "프레임 평균/최대 (끊김 {0})", "Frame avg/max (hitches {0})" },
			["srvCpu"] = new string[2] { "CPU (코어1=100)", "CPU (1 core=100)" },
			["srvMem"] = new string[2] { "메모리", "Memory" },
			["srvPlayers"] = new string[2] { "접속자", "Players" },
			["srvObjects"] = new string[2] { "전체 오브젝트", "All objects" },
			["srvTraffic"] = new string[2] { "트래픽 송/수", "Traffic out/in" },
			["srvUptime"] = new string[2] { "가동 시간", "Uptime" },
			["srvHost"] = new string[2] { "호스트", "host" },
			["btnClear"] = new string[2] { "서버 클리어", "Server clear" },
			["btnMemory"] = new string[2] { "메모리 정리", "Free memory" },
			["dlgOk"] = new string[2] { "확인", "OK" },
			["dlgRun"] = new string[2] { "실행", "Run" },
			["dlgCancel"] = new string[2] { "취소", "Cancel" },
			["dlgWorking"] = new string[2] { "서버에 요청하는 중입니다...", "Asking the server..." },
			["clearNotice"] = new string[2] { "안정적인 서버 운영을 위해 필드의 아이템 {0}개, 몬스터 {1}마리를 지웁니다.", "For stable server operation, {0} items and {1} monsters in the field will be cleared." },
			["clearPreview"] = new string[2] { "안정적인 서버 운영을 위해 필드의 아이템 {0}개, 몬스터 {1}마리를 지웁니다.\n(임시 이펙트/잔해 {2}개 포함. 플레이어가 지은 건축물, 상자, 무덤, 길들인 동물은 지우지 않습니다. 되돌릴 수 없습니다.)\n\n실행할까요?", "For stable server operation, {0} items and {1} monsters in the field will be cleared.\n(Includes {2} temporary effects/debris. Player-built pieces, containers, tombstones and tamed animals are never removed. This cannot be undone.)\n\nRun it?" },
			["clearNone"] = new string[2] { "지울 대상이 없습니다.", "Nothing to clear." },
			["clearDone"] = new string[2] { "서버 클리어 완료: 아이템 {0}개, 몬스터 {1}마리, 잔해 {2}개를 지웠습니다.", "Server clear finished: removed {0} items, {1} monsters and {2} debris objects." },
			["clearExpired"] = new string[2] { "확인 시간이 지났습니다. 다시 시도해 주세요.", "The confirmation expired. Please try again." },
			["clearBusy"] = new string[2] { "방금 실행했거나 다른 정리가 진행 중입니다. 잠시 후 다시 시도해 주세요.", "A cleanup just ran or is still running. Please try again shortly." },
			["clearDisabled"] = new string[2] { "서버 컨피그(Maintenance.AllowServerClear)에서 서버 클리어가 꺼져 있습니다.", "Server clear is turned off in the server config (Maintenance.AllowServerClear)." },
			["memConfirm"] = new string[2] { "서버 메모리를 정리합니다.\n- LagWatch 에러 기록 비우기\n- 사용하지 않는 에셋 해제\n- GC(가비지 컬렉션) 실행\n\n서버가 1~2초 멈출 수 있습니다. 실행할까요?", "This frees server memory.\n- Clear LagWatch's error records\n- Unload unused assets\n- Run garbage collection\n\nThe server may stall for a second or two. Run it?" },
			["memDone"] = new string[2] { "메모리 정리 완료 ({4}초)\n프로세스 메모리: {0}MB -> {1}MB\n관리 힙(GC): {2}MB -> {3}MB\n\n※ 프로세스 메모리는 OS로 바로 반환되지 않아 줄지 않을 수 있습니다.", "Memory cleanup finished ({4}s)\nProcess memory: {0}MB -> {1}MB\nManaged heap (GC): {2}MB -> {3}MB\n\nNote: process memory is not always returned to the OS right away, so it may not shrink." },
			["noticeHead"] = new string[2] { "[서버 관리자] 현재 접속 상태가 서버 랙을 유발하고 있습니다. 게임을 종료하고 아래 내용을 조치한 뒤 다시 접속해 주세요.", "[Server admin] Your connection is causing lag on the server. Please quit the game, fix the items below, and rejoin." },
			["noticeGeneric"] = new string[2] { "[서버 관리자] 서버 안정성을 위해 안내드립니다. 게임을 종료한 뒤 PC/네트워크 상태를 확인하고 다시 접속해 주세요.", "[Server admin] For server stability, please quit the game, check your PC/network, and rejoin." },
			["hintPing"] = new string[2] { "- 핑 {0}ms: 네트워크가 불안정합니다. 다운로드/스트리밍/VPN을 끄고 가능하면 유선으로 연결하세요.", "- Ping {0}ms: your network is unstable. Stop downloads/streaming/VPN and use a wired connection if possible." },
			["hintZdo"] = new string[2] { "- 소유 오브젝트 {0}개: 주변의 몬스터/드랍/건축물이 너무 많습니다. 정리한 뒤 접속하세요.", "- {0} owned objects: too many monsters/drops/pieces near you. Clean them up before rejoining." },
			["hintFps"] = new string[2] { "- FPS {0}: PC 성능이 낮아 서버와 동기화가 지연됩니다. 그래픽 옵션과 설치한 모드 수를 줄이세요.", "- FPS {0}: low performance delays sync with the server. Lower graphics settings and reduce installed mods." },
			["hintQueue"] = new string[2] { "- 송신 대기 {0}KB: 서버가 보낸 데이터를 제때 받지 못하고 있습니다. 회선 속도와 방화벽/보안 프로그램을 확인하세요.", "- Send queue {0}KB: you are not receiving server data in time. Check your line speed and firewall/security software." },
			["hintTraffic"] = new string[2] { "- 트래픽 {0}KB/s: 통신량이 많습니다. 대역폭을 쓰는 다른 프로그램을 종료하세요.", "- Traffic {0}KB/s: heavy network usage. Close other programs using bandwidth." }
		};

		internal static bool Korean
		{
			get
			{
				try
				{
					return Localization.instance != null && Localization.instance.GetSelectedLanguage() == "Korean";
				}
				catch
				{
					return false;
				}
			}
		}

		internal static string Get(string key)
		{
			string[] value;
			return T.TryGetValue(key, out value) ? value[(!Korean) ? 1u : 0u] : key;
		}

		internal static string Fmt(string key, params object[] args)
		{
			return string.Format(Get(key), args);
		}
	}
	internal class ErrorCollector : ILogListener, IDisposable
	{
		internal class Entry
		{
			public string Source;

			public string Level;

			public string Message;

			public int IntervalCount;

			public long TotalCount;
		}

		private const string OverflowKey = "(other: distinct limit reached)";

		private const int MaxLineLen = 160;

		private readonly object _lock = new object();

		private readonly Dictionary<string, Entry> _entries = new Dictionary<string, Entry>();

		private readonly bool _includeWarnings;

		private readonly int _maxDistinct;

		private readonly int _maxPerSec;

		private long _bucketSec;

		private int _bucketCount;

		private long _dropped;

		private long _intervalTotal;

		internal ErrorCollector(bool includeWarnings, int maxDistinct, int maxPerSec)
		{
			_includeWarnings = includeWarnings;
			_maxDistinct = maxDistinct;
			_maxPerSec = maxPerSec;
		}

		public void LogEvent(object sender, LogEventArgs e)
		{
			//IL_0002: Unknown result type (might be due to invalid IL or missing references)
			//IL_0007: Unknown result type (might be due to invalid IL or missing references)
			//IL_0008: Unknown result type (might be due to invalid IL or missing references)
			//IL_000a: Unknown result type (might be due to invalid IL or missing references)
			//IL_000c: Invalid comparison between Unknown and I4
			//IL_001a: Unknown result type (might be due to invalid IL or missing references)
			//IL_001c: Unknown result type (might be due to invalid IL or missing references)
			//IL_001e: Invalid comparison between Unknown and I4
			LogLevel level = e.Level;
			bool flag = (level & 3) > 0;
			if (!flag && (!_includeWarnings || (level & 4) == 0))
			{
				return;
			}
			long num = Stopwatch.GetTimestamp() / Stopwatch.Frequency;
			lock (_lock)
			{
				if (num != _bucketSec)
				{
					_bucketSec = num;
					_bucketCount = 0;
				}
				if (++_bucketCount > _maxPerSec)
				{
					_dropped++;
					_intervalTotal++;
					return;
				}
			}
			ILogSource source = e.Source;
			string text = ((source != null) ? source.SourceName : null) ?? "?";
			string text2 = text + "|" + Normalize(e.Data?.ToString() ?? "");
			lock (_lock)
			{
				_intervalTotal++;
				if (!_entries.TryGetValue(text2, out var value))
				{
					if (_entries.Count >= _maxDistinct)
					{
						text2 = "(other: distinct limit reached)";
						if (!_entries.TryGetValue(text2, out value))
						{
							value = new Entry
							{
								Source = "-",
								Level = "-",
								Message = "(other: distinct limit reached)"
							};
							_entries[text2] = value;
						}
					}
					else
					{
						value = new Entry
						{
							Source = text,
							Level = (flag ? "Error" : "Warning"),
							Message = text2.Substring(text.Length + 1)
						};
						_entries[text2] = value;
					}
				}
				value.IntervalCount++;
				value.TotalCount++;
			}
		}

		internal void Clear()
		{
			lock (_lock)
			{
				_entries.Clear();
				_intervalTotal = 0L;
				_dropped = 0L;
			}
		}

		internal List<Entry> TakeIntervalTop(int n, out long intervalTotal, out long dropped)
		{
			List<Entry> list = new List<Entry>();
			lock (_lock)
			{
				foreach (Entry value in _entries.Values)
				{
					if (value.IntervalCount > 0)
					{
						list.Add(new Entry
						{
							Source = value.Source,
							Level = value.Level,
							Message = value.Message,
							IntervalCount = value.IntervalCount,
							TotalCount = value.TotalCount
						});
					}
				}
				foreach (Entry value2 in _entries.Values)
				{
					value2.IntervalCount = 0;
				}
				intervalTotal = _intervalTotal;
				dropped = _dropped;
				_intervalTotal = 0L;
				_dropped = 0L;
			}
			list.Sort((Entry a, Entry b) => b.IntervalCount.CompareTo(a.IntervalCount));
			if (list.Count > n)
			{
				list.RemoveRange(n, list.Count - n);
			}
			return list;
		}

		private static string Normalize(string raw)
		{
			StringBuilder stringBuilder = new StringBuilder(324);
			int num = 0;
			int num2 = 0;
			while (num2 < raw.Length && num < 2)
			{
				int num3 = raw.IndexOf('\n', num2);
				if (num3 < 0)
				{
					num3 = raw.Length;
				}
				int num4 = num3 - num2;
				string text = raw.Substring(num2, (num4 > 160) ? 160 : num4).Trim();
				num2 = num3 + 1;
				if (text.Length != 0 && !text.StartsWith("Stack trace"))
				{
					if (num > 0)
					{
						stringBuilder.Append(" | ");
					}
					AppendMasked(stringBuilder, text);
					num++;
				}
			}
			return stringBuilder.ToString();
		}

		private static void AppendMasked(StringBuilder sb, string line)
		{
			bool flag = false;
			foreach (char c in line)
			{
				if (c >= '0' && c <= '9')
				{
					if (!flag)
					{
						sb.Append('#');
					}
					flag = true;
				}
				else
				{
					flag = false;
					sb.Append(c);
				}
			}
		}

		public void Dispose()
		{
		}
	}
	internal class ServerHealth : MonoBehaviour
	{
		private const int TopErrors = 10;

		private const int TopPrefabs = 10;

		private static ServerHealth _instance;

		private ErrorCollector _errors;

		private float _nextReport;

		private float _startedAt;

		private int _frames;

		private int _hitches;

		private float _sumDt;

		private float _maxDt;

		private bool _sizeWarned;

		private int _lastCleanupDay = -1;

		internal static bool Active { get; private set; }

		private static string Dir => Path.Combine(Paths.BepInExRootPath, "LagWatch");

		internal static void ClearErrorTable()
		{
			_instance?._errors?.Clear();
		}

		private void Awake()
		{
			_instance = this;
		}

		private void Update()
		{
			bool flag = Plugin.DiagEnabled.Value && (Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer();
			if (flag != Active)
			{
				if (flag)
				{
					StartMonitoring();
				}
				else
				{
					StopMonitoring();
				}
			}
			if (Active)
			{
				float unscaledDeltaTime = Time.unscaledDeltaTime;
				_frames++;
				_sumDt += unscaledDeltaTime;
				if (unscaledDeltaTime > _maxDt)
				{
					_maxDt = unscaledDeltaTime;
				}
				if (unscaledDeltaTime * 1000f >= Plugin.DiagHitchMs.Value)
				{
					_hitches++;
				}
				if (Time.realtimeSinceStartup >= _nextReport)
				{
					Report();
				}
			}
		}

		private void OnDestroy()
		{
			StopMonitoring();
		}

		private void StartMonitoring()
		{
			_errors = new ErrorCollector(Plugin.DiagIncludeWarnings.Value, Plugin.DiagMaxDistinctErrors.Value, Plugin.DiagMaxLogsPerSec.Value);
			Logger.Listeners.Add((ILogListener)(object)_errors);
			Active = true;
			_startedAt = Time.realtimeSinceStartup;
			_nextReport = _startedAt + Plugin.DiagReportIntervalMin.Value * 60f;
			ResetFrameStats();
			Plugin.Log.LogInfo((object)$"[LagWatch] diagnostics ON (every {Plugin.DiagReportIntervalMin.Value} min, dir={Dir})");
		}

		private void StopMonitoring()
		{
			if (Active)
			{
				Active = false;
				if (_errors != null)
				{
					Logger.Listeners.Remove((ILogListener)(object)_errors);
				}
				_errors = null;
				Plugin.Log.LogInfo((object)"[LagWatch] diagnostics OFF");
			}
		}

		private void ResetFrameStats()
		{
			_frames = (_hitches = 0);
			_sumDt = (_maxDt = 0f);
		}

		private void Report()
		{
			_nextReport = Time.realtimeSinceStartup + Plugin.DiagReportIntervalMin.Value * 60f;
			try
			{
				long intervalTotal;
				long dropped;
				List<ErrorCollector.Entry> list = _errors.TakeIntervalTop(10, out intervalTotal, out dropped);
				string json = BuildJson(list, intervalTotal, dropped);
				WriteLine(json);
				float num = ((_frames > 0) ? (_sumDt / (float)_frames * 1000f) : 0f);
				Plugin.Log.LogInfo((object)$"[LagWatch] diag: frame avg {num:F1}ms max {_maxDt * 1000f:F0}ms hitches {_hitches}, peers {PeerSampler.Stats.Count}, errors {intervalTotal} (distinct top {list.Count}), zdo {ZdoOwnership.TotalZdos}");
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[LagWatch] diag report failed (ignored): " + ex.Message));
			}
			ResetFrameStats();
		}

		private string BuildJson(List<ErrorCollector.Entry> top, long errTotal, long dropped)
		{
			StringBuilder stringBuilder = new StringBuilder(2048);
			float num = ((_frames > 0) ? (_sumDt / (float)_frames * 1000f) : 0f);
			stringBuilder.Append("{\"t\":\"").Append(DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss")).Append('"');
			stringBuilder.Append(",\"uptimeMin\":").Append(((Time.realtimeSinceStartup - _startedAt) / 60f).ToString("F0"));
			stringBuilder.Append(",\"peers\":").Append(PeerSampler.Stats.Count);
			stringBuilder.Append(",\"frame\":{\"count\":").Append(_frames).Append(",\"avgMs\":")
				.Append(num.ToString("F1"))
				.Append(",\"maxMs\":")
				.Append((_maxDt * 1000f).ToString("F0"))
				.Append(",\"hitches\":")
				.Append(_hitches)
				.Append('}');
			stringBuilder.Append(",\"gcMB\":").Append(((float)GC.GetTotalMemory(forceFullCollection: false) / 1048576f).ToString("F0"));
			stringBuilder.Append(",\"scene\":").Append(((Object)(object)ZNetScene.instance != (Object)null) ? ZNetScene.instance.m_instances.Count : 0);
			stringBuilder.Append(",\"zdo\":{\"total\":").Append(ZdoOwnership.TotalZdos).Append(",\"scanMs\":")
				.Append(ZdoOwnership.LastScanMs)
				.Append(",\"top\":[");
			AppendTopPrefabs(stringBuilder);
			stringBuilder.Append("]}");
			stringBuilder.Append(",\"errors\":{\"total\":").Append(errTotal).Append(",\"dropped\":")
				.Append(dropped)
				.Append(",\"top\":[");
			for (int i = 0; i < top.Count; i++)
			{
				ErrorCollector.Entry entry = top[i];
				if (i > 0)
				{
					stringBuilder.Append(',');
				}
				stringBuilder.Append("{\"n\":").Append(entry.IntervalCount).Append(",\"total\":")
					.Append(entry.TotalCount)
					.Append(",\"lvl\":")
					.Append(Q(entry.Level))
					.Append(",\"src\":")
					.Append(Q(entry.Source))
					.Append(",\"msg\":")
					.Append(Q(entry.Message))
					.Append('}');
			}
			stringBuilder.Append("]}");
			stringBuilder.Append(",\"players\":[");
			bool flag = true;
			foreach (PeerStat value in PeerSampler.Stats.Values)
			{
				if (!flag)
				{
					stringBuilder.Append(',');
				}
				flag = false;
				stringBuilder.Append("{\"name\":").Append(Q(value.Name)).Append(",\"lv\":")
					.Append(value.Level)
					.Append(",\"ping\":")
					.Append(value.PingAvg)
					.Append(",\"zdo\":")
					.Append(value.OwnedZdos)
					.Append(",\"queueKB\":")
					.Append(value.SendQueue / 1024)
					.Append('}');
			}
			stringBuilder.Append("]}");
			return stringBuilder.ToString();
		}

		private static void AppendTopPrefabs(StringBuilder sb)
		{
			List<KeyValuePair<int, int>> list = new List<KeyValuePair<int, int>>(ZdoOwnership.GlobalPrefabs);
			list.Sort((KeyValuePair<int, int> a, KeyValuePair<int, int> b) => b.Value.CompareTo(a.Value));
			for (int num = 0; num < list.Count && num < 10; num++)
			{
				GameObject val = (((Object)(object)ZNetScene.instance != (Object)null) ? ZNetScene.instance.GetPrefab(list[num].Key) : null);
				string s = (((Object)(object)val != (Object)null) ? ((Object)val).name : list[num].Key.ToString());
				if (num > 0)
				{
					sb.Append(',');
				}
				sb.Append('[').Append(Q(s)).Append(',')
					.Append(list[num].Value)
					.Append(']');
			}
		}

		private void WriteLine(string json)
		{
			Directory.CreateDirectory(Dir);
			CleanupOldFiles();
			string text = Path.Combine(Dir, "diag-" + DateTime.Now.ToString("yyyy-MM-dd") + ".jsonl");
			long num = (long)(Plugin.DiagMaxFileMB.Value * 1048576f);
			if (File.Exists(text) && new FileInfo(text).Length > num)
			{
				if (!_sizeWarned)
				{
					_sizeWarned = true;
					Plugin.Log.LogWarning((object)$"[LagWatch] diag file reached {Plugin.DiagMaxFileMB.Value}MB; skipping writes for today.");
				}
			}
			else
			{
				_sizeWarned = false;
				File.AppendAllText(text, json + "\n", new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
			}
		}

		private void CleanupOldFiles()
		{
			int dayOfYear = DateTime.Now.DayOfYear;
			if (dayOfYear == _lastCleanupDay)
			{
				return;
			}
			_lastCleanupDay = dayOfYear;
			DateTime dateTime = DateTime.Now.AddDays(-Plugin.DiagRetainDays.Value);
			string[] files = Directory.GetFiles(Dir, "diag-*.jsonl");
			foreach (string path in files)
			{
				if (File.GetLastWriteTime(path) < dateTime)
				{
					File.Delete(path);
				}
			}
		}

		private static string Q(string s)
		{
			if (s == null)
			{
				return "\"\"";
			}
			StringBuilder stringBuilder = new StringBuilder(s.Length + 2);
			stringBuilder.Append('"');
			foreach (char c in s)
			{
				switch (c)
				{
				case '"':
					stringBuilder.Append("\\\"");
					continue;
				case '\\':
					stringBuilder.Append("\\\\");
					continue;
				case '\n':
					stringBuilder.Append("\\n");
					continue;
				case '\t':
					stringBuilder.Append(' ');
					continue;
				case '\r':
					continue;
				}
				if (c >= ' ')
				{
					stringBuilder.Append(c);
				}
			}
			return stringBuilder.Append('"').ToString();
		}
	}
	internal static class AdminActionRpc
	{
		private const string RpcClearPreview = "LagWatch.ClearPreview";

		private const string RpcClearPreviewReply = "LagWatch.ClearPreviewReply";

		private const string RpcClearRun = "LagWatch.ClearRun";

		private const string RpcClearResult = "LagWatch.ClearResult";

		private const string RpcClearNotice = "LagWatch.ClearNotice";

		private const string RpcMemRun = "LagWatch.MemRun";

		private const string RpcMemResult = "LagWatch.MemResult";

		private static bool IsServer => (Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer();

		internal static void Register()
		{
			ZRoutedRpc instance = ZRoutedRpc.instance;
			instance.Register<ZPackage>("LagWatch.ClearPreview", (Action<long, ZPackage>)OnClearPreview);
			instance.Register<ZPackage>("LagWatch.ClearPreviewReply", (Action<long, ZPackage>)OnClearPreviewReply);
			instance.Register<ZPackage>("LagWatch.ClearRun", (Action<long, ZPackage>)OnClearRun);
			instance.Register<ZPackage>("LagWatch.ClearResult", (Action<long, ZPackage>)OnClearResult);
			instance.Register<ZPackage>("LagWatch.ClearNotice", (Action<long, ZPackage>)OnClearNotice);
			instance.Register<ZPackage>("LagWatch.MemRun", (Action<long, ZPackage>)OnMemRun);
			instance.Register<ZPackage>("LagWatch.MemResult", (Action<long, ZPackage>)OnMemResult);
		}

		internal static void RequestClearPreview()
		{
			LagWatchRpc.ToServer("LagWatch.ClearPreview", LagWatchRpc.NewPackage());
		}

		internal static void RequestClearRun(string token)
		{
			ZPackage val = LagWatchRpc.NewPackage();
			val.Write(token ?? "");
			LagWatchRpc.ToServer("LagWatch.ClearRun", val);
		}

		internal static void RequestMemoryCleanup()
		{
			LagWatchRpc.ToServer("LagWatch.MemRun", LagWatchRpc.NewPackage());
		}

		private static void OnClearPreview(long sender, ZPackage p)
		{
			if (!IsServer)
			{
				return;
			}
			try
			{
				if (LagWatchRpc.VersionOk(p) && AdminGate.IsAdmin(sender))
				{
					string token;
					ClearCounts clearCounts = ServerMaintenance.Instance.Preview(sender, out token);
					ZPackage val = LagWatchRpc.NewPackage();
					val.Write(Plugin.AllowServerClear.Value);
					val.Write(token);
					val.Write(clearCounts.Items);
					val.Write(clearCounts.Monsters);
					val.Write(clearCounts.Debris);
					ZRoutedRpc.instance.InvokeRoutedRPC(sender, "LagWatch.ClearPreviewReply", new object[1] { val });
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[LagWatch] clear preview failed (ignored): " + ex.Message));
			}
		}

		private static void OnClearRun(long sender, ZPackage p)
		{
			if (!IsServer)
			{
				return;
			}
			try
			{
				if (!LagWatchRpc.VersionOk(p))
				{
					return;
				}
				string token = p.ReadString();
				if (AdminGate.IsAdmin(sender))
				{
					ClearCounts counts;
					int num = ServerMaintenance.Instance.TryRun(sender, token, out counts);
					if (num != 0)
					{
						SendClearResult(sender, num, 0, 0, 0);
						return;
					}
					ZPackage val = LagWatchRpc.NewPackage();
					val.Write(counts.Items);
					val.Write(counts.Monsters);
					ZRoutedRpc.instance.InvokeRoutedRPC(0L, "LagWatch.ClearNotice", new object[1] { val });
					Plugin.Log.LogInfo((object)$"[LagWatch] server clear started by admin peer {sender}: items={counts.Items} monsters={counts.Monsters} debris={counts.Debris}");
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[LagWatch] clear run failed (ignored): " + ex.Message));
			}
		}

		private static void OnMemRun(long sender, ZPackage p)
		{
			if (!IsServer)
			{
				return;
			}
			try
			{
				if (LagWatchRpc.VersionOk(p) && AdminGate.IsAdmin(sender))
				{
					int num = ServerMaintenance.Instance.TryCleanMemory(sender);
					if (num != 0)
					{
						SendMemResult(sender, num, 0f, 0f, 0f, 0f, 0f);
					}
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[LagWatch] memory cleanup failed (ignored): " + ex.Message));
			}
		}

		internal static void SendClearResult(long admin, int code, int items, int monsters, int debris)
		{
			ZPackage val = LagWatchRpc.NewPackage();
			val.Write(code);
			val.Write(items);
			val.Write(monsters);
			val.Write(debris);
			ZRoutedRpc.instance.InvokeRoutedRPC(admin, "LagWatch.ClearResult", new object[1] { val });
		}

		internal static void SendMemResult(long admin, int code, float wsBefore, float wsAfter, float heapBefore, float heapAfter, float seconds)
		{
			ZPackage val = LagWatchRpc.NewPackage();
			val.Write(code);
			val.Write(wsBefore);
			val.Write(wsAfter);
			val.Write(heapBefore);
			val.Write(heapAfter);
			val.Write(seconds);
			ZRoutedRpc.instance.InvokeRoutedRPC(admin, "LagWatch.MemResult", new object[1] { val });
		}

		private static void OnClearPreviewReply(long sender, ZPackage p)
		{
			try
			{
				if (LagWatchRpc.VersionOk(p))
				{
					bool allowed = p.ReadBool();
					string token = p.ReadString();
					int items = p.ReadInt();
					int monsters = p.ReadInt();
					int debris = p.ReadInt();
					LagPanel.Instance?.OnClearPreview(allowed, token, items, monsters, debris);
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[LagWatch] preview reply parse failed (ignored): " + ex.Message));
			}
		}

		private static void OnClearResult(long sender, ZPackage p)
		{
			try
			{
				if (LagWatchRpc.VersionOk(p))
				{
					int code = p.ReadInt();
					int items = p.ReadInt();
					int monsters = p.ReadInt();
					int debris = p.ReadInt();
					LagPanel.Instance?.OnClearResult(code, items, monsters, debris);
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[LagWatch] clear result parse failed (ignored): " + ex.Message));
			}
		}

		private static void OnMemResult(long sender, ZPackage p)
		{
			try
			{
				if (LagWatchRpc.VersionOk(p))
				{
					int code = p.ReadInt();
					float wsBefore = p.ReadSingle();
					float wsAfter = p.ReadSingle();
					float heapBefore = p.ReadSingle();
					float heapAfter = p.ReadSingle();
					float seconds = p.ReadSingle();
					LagPanel.Instance?.OnMemResult(code, wsBefore, wsAfter, heapBefore, heapAfter, seconds);
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[LagWatch] memory result parse failed (ignored): " + ex.Message));
			}
		}

		private static void OnClearNotice(long sender, ZPackage p)
		{
			try
			{
				if (LagWatchRpc.VersionOk(p))
				{
					int num = p.ReadInt();
					int num2 = p.ReadInt();
					string text = Strings.Fmt("clearNotice", num, num2);
					Plugin.Log.LogInfo((object)("[LagWatch] notice: " + text));
					MessageHud instance = MessageHud.instance;
					if (instance != null)
					{
						instance.ShowMessage((MessageType)2, text, 0, (Sprite)null, false, true);
					}
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[LagWatch] clear notice parse failed (ignored): " + ex.Message));
			}
		}
	}
	internal static class AdminGate
	{
		private const string RequestRpc = "LagWatch.AdminReq";

		private const string SyncRpc = "LagWatch.AdminSync";

		private static bool _serverSaysAdmin;

		internal static bool IsLocalAdmin => _serverSaysAdmin || ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.LocalPlayerIsAdminOrHost());

		internal static void Register()
		{
			_serverSaysAdmin = false;
			ZRoutedRpc.instance.Register<string>("LagWatch.AdminReq", (Action<long, string>)OnRequest);
			ZRoutedRpc.instance.Register<bool>("LagWatch.AdminSync", (Action<long, bool>)OnSync);
		}

		internal static void Request()
		{
			if (ZRoutedRpc.instance != null)
			{
				ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.instance.GetServerPeerID(), "LagWatch.AdminReq", new object[1] { "" });
			}
		}

		internal static bool IsAdmin(long sender)
		{
			if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer())
			{
				return false;
			}
			if (sender == ZRoutedRpc.instance.GetServerPeerID())
			{
				return true;
			}
			ZNetPeer peer = ZNet.instance.GetPeer(sender);
			string text = ((peer != null) ? peer.m_socket.GetHostName() : null);
			return text != null && ZNet.instance.IsAdmin(text);
		}

		private static void OnRequest(long sender, string _)
		{
			if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer())
			{
				return;
			}
			try
			{
				ZRoutedRpc.instance.InvokeRoutedRPC(sender, "LagWatch.AdminSync", new object[1] { IsAdmin(sender) });
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[LagWatch] admin check failed (ignored): " + ex.Message));
			}
		}

		private static void OnSync(long sender, bool isAdmin)
		{
			_serverSaysAdmin = isAdmin;
		}
	}
	[HarmonyPatch(typeof(Player), "OnSpawned")]
	internal static class Player_OnSpawned_AdminGatePatch
	{
		[HarmonyPostfix]
		private static void Postfix(Player __instance)
		{
			if ((Object)(object)__instance == (Object)(object)Player.m_localPlayer)
			{
				AdminGate.Request();
			}
		}
	}
	[HarmonyPatch]
	internal static class LagWatchRpc
	{
		private const string RpcReport = "LagWatch.Report";

		private const string RpcSubscribe = "LagWatch.Subscribe";

		private const string RpcSnapshot = "LagWatch.Snapshot";

		private const string RpcWarn = "LagWatch.Warn";

		private const string RpcWarnAck = "LagWatch.WarnAck";

		private const string RpcNotice = "LagWatch.Notice";

		private const string RpcSetAuto = "LagWatch.SetAuto";

		private const string RpcDetail = "LagWatch.Detail";

		private const string RpcDetailReply = "LagWatch.DetailReply";

		private const string RpcReportAck = "LagWatch.ReportAck";

		internal const int WarnSent = 0;

		internal const int WarnCooldown = 1;

		internal const int WarnNoMod = 2;

		private static readonly HashSet<long> Subscribers = new HashSet<long>();

		private static readonly Dictionary<long, float> LastWarn = new Dictionary<long, float>();

		private static bool _versionWarned;

		internal static bool ServerAcked;

		internal static int Session;

		private static bool IsServer => (Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer();

		internal static bool HasSubscribers => Subscribers.Count > 0;

		private static void ResetSession()
		{
			Subscribers.Clear();
			LastWarn.Clear();
			ClientReportStore.Reports.Clear();
			PeerSampler.Stats.Clear();
			ServerAcked = false;
			Session++;
		}

		internal static void ForgetWarn(long uid)
		{
			LastWarn.Remove(uid);
		}

		[HarmonyPatch(typeof(Game), "Start")]
		[HarmonyPostfix]
		private static void RegisterHandlers()
		{
			ResetSession();
			ZRoutedRpc instance = ZRoutedRpc.instance;
			instance.Register<ZPackage>("LagWatch.ReportAck", (Action<long, ZPackage>)OnReportAck);
			instance.Register<ZPackage>("LagWatch.Report", (Action<long, ZPackage>)OnReport);
			instance.Register<ZPackage>("LagWatch.Subscribe", (Action<long, ZPackage>)OnSubscribe);
			instance.Register<ZPackage>("LagWatch.Snapshot", (Action<long, ZPackage>)OnSnapshot);
			instance.Register<ZPackage>("LagWatch.Warn", (Action<long, ZPackage>)OnWarn);
			instance.Register<ZPackage>("LagWatch.WarnAck", (Action<long, ZPackage>)OnWarnAck);
			instance.Register<ZPackage>("LagWatch.Notice", (Action<long, ZPackage>)OnNotice);
			instance.Register<ZPackage>("LagWatch.SetAuto", (Action<long, ZPackage>)OnSetAuto);
			instance.Register<ZPackage>("LagWatch.Detail", (Action<long, ZPackage>)OnDetail);
			instance.Register<ZPackage>("LagWatch.DetailReply", (Action<long, ZPackage>)OnDetailReply);
			AdminGate.Register();
			AdminActionRpc.Register();
		}

		internal static ZPackage NewPackage()
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_0007: Expected O, but got Unknown
			ZPackage val = new ZPackage();
			val.Write("2");
			return val;
		}

		internal static void ToServer(string rpc, ZPackage p)
		{
			if (ZRoutedRpc.instance != null)
			{
				ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.instance.GetServerPeerID(), rpc, new object[1] { p });
			}
		}

		internal static void SendReport(float avgFps, float minFps, int modCount, string mods)
		{
			ZPackage val = NewPackage();
			val.Write(avgFps);
			val.Write(minFps);
			val.Write(modCount);
			val.Write(mods ?? "");
			ToServer("LagWatch.Report", val);
		}

		internal static void SendSubscribe(bool on)
		{
			ZPackage val = NewPackage();
			val.Write(on);
			ToServer("LagWatch.Subscribe", val);
		}

		internal static void SendWarn(long targetUid)
		{
			ZPackage val = NewPackage();
			val.Write(targetUid);
			ToServer("LagWatch.Warn", val);
		}

		internal static void SendSetAuto(bool on)
		{
			ZPackage val = NewPackage();
			val.Write(on);
			ToServer("LagWatch.SetAuto", val);
		}

		internal static void SendDetailRequest(long targetUid)
		{
			ZPackage val = NewPackage();
			val.Write(targetUid);
			ToServer("LagWatch.Detail", val);
		}

		internal static bool VersionOk(ZPackage p)
		{
			string text = p.ReadString();
			if (text == "2")
			{
				return true;
			}
			if (!_versionWarned)
			{
				_versionWarned = true;
				Plugin.Log.LogWarning((object)("[LagWatch] protocol mismatch (peer=" + text + ", local=2); ignoring messages. Update LagWatch on both server and client."));
			}
			return false;
		}

		private static void OnReport(long sender, ZPackage p)
		{
			if (!IsServer)
			{
				return;
			}
			try
			{
				if (VersionOk(p))
				{
					float avgFps = Mathf.Clamp(p.ReadSingle(), 0f, 1000f);
					float minFps = Mathf.Clamp(p.ReadSingle(), 0f, 1000f);
					int modCount = Mathf.Clamp(p.ReadInt(), 0, 2000);
					string text = p.ReadString();
					if (!ClientReportStore.Reports.TryGetValue(sender, out var value))
					{
						value = new ClientReport();
						ClientReportStore.Reports[sender] = value;
						ZRoutedRpc.instance.InvokeRoutedRPC(sender, "LagWatch.ReportAck", new object[1] { NewPackage() });
					}
					value.AvgFps = avgFps;
					value.MinFps = minFps;
					value.ModCount = modCount;
					if (text.Length > 0)
					{
						value.Mods = ((text.Length > 16384) ? text.Substring(0, 16384) : text);
					}
					value.ReceivedAt = Time.realtimeSinceStartup;
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[LagWatch] report parse failed (ignored): " + ex.Message));
			}
		}

		private static void OnSubscribe(long sender, ZPackage p)
		{
			if (!IsServer)
			{
				return;
			}
			try
			{
				if (!VersionOk(p))
				{
					return;
				}
				if (!p.ReadBool())
				{
					Subscribers.Remove(sender);
				}
				else if (AdminGate.IsAdmin(sender))
				{
					bool flag = Subscribers.Count == 0;
					Subscribers.Add(sender);
					if (flag)
					{
						ServerMonitor.Sample(Time.realtimeSinceStartup);
					}
					SendSnapshot(sender);
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[LagWatch] subscribe failed (ignored): " + ex.Message));
			}
		}

		private static void OnSetAuto(long sender, ZPackage p)
		{
			if (!IsServer)
			{
				return;
			}
			try
			{
				if (VersionOk(p))
				{
					bool flag = p.ReadBool();
					if (AdminGate.IsAdmin(sender))
					{
						Plugin.AutoWarnEnabled.Value = flag;
						Plugin.Log.LogInfo((object)$"[LagWatch] AutoWarnEnabled set to {flag} by admin peer {sender}");
						PushSnapshots();
					}
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[LagWatch] set-auto failed (ignored): " + ex.Message));
			}
		}

		private static void OnDetail(long sender, ZPackage p)
		{
			if (!IsServer)
			{
				return;
			}
			try
			{
				if (VersionOk(p))
				{
					long num = p.ReadLong();
					if (AdminGate.IsAdmin(sender))
					{
						ZPackage val = NewPackage();
						val.Write(num);
						val.Write(ClientReportStore.Reports.TryGetValue(num, out var value) ? value.Mods : "");
						ZRoutedRpc.instance.InvokeRoutedRPC(sender, "LagWatch.DetailReply", new object[1] { val });
					}
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[LagWatch] detail failed (ignored): " + ex.Message));
			}
		}

		private static void OnWarn(long sender, ZPackage p)
		{
			if (!IsServer)
			{
				return;
			}
			try
			{
				if (VersionOk(p))
				{
					long num = p.ReadLong();
					if (AdminGate.IsAdmin(sender))
					{
						int num2 = TryWarn(num, auto: false);
						ZPackage val = NewPackage();
						val.Write(num);
						val.Write(num2);
						ZRoutedRpc.instance.InvokeRoutedRPC(sender, "LagWatch.WarnAck", new object[1] { val });
					}
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[LagWatch] warn failed (ignored): " + ex.Message));
			}
		}

		internal static int TryWarn(long target, bool auto)
		{
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			ClientReport fresh = ClientReportStore.GetFresh(target, realtimeSinceStartup);
			if (fresh == null || !PeerSampler.Stats.TryGetValue(target, out var value))
			{
				return 2;
			}
			if (LastWarn.TryGetValue(target, out var value2) && realtimeSinceStartup - value2 < Plugin.WarnCooldownSec.Value)
			{
				return 1;
			}
			LastWarn[target] = realtimeSinceStartup;
			ZPackage val = NewPackage();
			val.Write(value.PingLv);
			val.Write(value.ZdoLv);
			val.Write(value.FpsLv);
			val.Write(value.QueueLv);
			val.Write(value.TrafficLv);
			val.Write(value.PingAvg);
			val.Write(value.OwnedZdos);
			val.Write(fresh.AvgFps);
			val.Write(value.SendQueue / 1024);
			val.Write((int)((value.OutBytesSec + value.InBytesSec) / 1024f));
			ZRoutedRpc.instance.InvokeRoutedRPC(target, "LagWatch.Notice", new object[1] { val });
			Plugin.Log.LogInfo((object)$"[LagWatch] notice sent to {value.Name}({target}) auto={auto} lv={value.Level}");
			return 0;
		}

		private static void SendSnapshot(long to)
		{
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			ZPackage val = NewPackage();
			val.Write(Plugin.AutoWarnEnabled.Value);
			ServerMonitor.Current.Write(val);
			val.Write(PeerSampler.Stats.Count);
			foreach (PeerStat value in PeerSampler.Stats.Values)
			{
				SnapRow.From(value, ClientReportStore.GetFresh(value.Uid, realtimeSinceStartup)).Write(val);
			}
			ZRoutedRpc.instance.InvokeRoutedRPC(to, "LagWatch.Snapshot", new object[1] { val });
		}

		internal static void PushSnapshots()
		{
			if (Subscribers.Count == 0)
			{
				return;
			}
			List<long> list = new List<long>();
			foreach (long subscriber in Subscribers)
			{
				if (subscriber != ZRoutedRpc.instance.GetServerPeerID() && ZNet.instance.GetPeer(subscriber) == null)
				{
					list.Add(subscriber);
				}
				else
				{
					SendSnapshot(subscriber);
				}
			}
			foreach (long item in list)
			{
				Subscribers.Remove(item);
			}
		}

		private static void OnReportAck(long sender, ZPackage p)
		{
			try
			{
				if (VersionOk(p))
				{
					ServerAcked = true;
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[LagWatch] report ack parse failed (ignored): " + ex.Message));
			}
		}

		private static void OnSnapshot(long sender, ZPackage p)
		{
			try
			{
				if (VersionOk(p))
				{
					bool autoWarn = p.ReadBool();
					ServerSnap server = ServerSnap.Read(p);
					int num = p.ReadInt();
					List<SnapRow> list = new List<SnapRow>(num);
					for (int i = 0; i < num; i++)
					{
						list.Add(SnapRow.Read(p));
					}
					LagPanel.Instance?.OnSnapshot(autoWarn, server, list);
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[LagWatch] snapshot parse failed (ignored): " + ex.Message));
			}
		}

		private static void OnWarnAck(long sender, ZPackage p)
		{
			try
			{
				if (VersionOk(p))
				{
					long uid = p.ReadLong();
					int code = p.ReadInt();
					LagPanel.Instance?.OnWarnAck(uid, code);
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[LagWatch] ack parse failed (ignored): " + ex.Message));
			}
		}

		private static void OnDetailReply(long sender, ZPackage p)
		{
			try
			{
				if (VersionOk(p))
				{
					long uid = p.ReadLong();
					string mods = p.ReadString();
					LagPanel.Instance?.OnDetail(uid, mods);
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[LagWatch] detail parse failed (ignored): " + ex.Message));
			}
		}

		private static void OnNotice(long sender, ZPackage p)
		{
			try
			{
				if (VersionOk(p))
				{
					NoticeData n = new NoticeData
					{
						PingLv = p.ReadByte(),
						ZdoLv = p.ReadByte(),
						FpsLv = p.ReadByte(),
						QueueLv = p.ReadByte(),
						TrafficLv = p.ReadByte(),
						Ping = p.ReadInt(),
						Zdo = p.ReadInt(),
						Fps = p.ReadSingle(),
						QueueKB = p.ReadInt(),
						TrafficKBs = p.ReadInt()
					};
					NoticeText.Show(n);
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[LagWatch] notice parse failed (ignored): " + ex.Message));
			}
		}
	}
	internal class SnapRow
	{
		public long Uid;

		public string Name;

		public int PingAvg;

		public int PingMax;

		public float OutKBs;

		public float InKBs;

		public int QueueBytes;

		public int Zdo;

		public string Top;

		public bool Reported;

		public float AvgFps;

		public float MinFps;

		public int ModCount;

		public byte PingLv;

		public byte ZdoLv;

		public byte FpsLv;

		public byte QueueLv;

		public byte TrafficLv;

		public byte Level;

		public float TrafficKBs => OutKBs + InKBs;

		internal static SnapRow From(PeerStat s, ClientReport r)
		{
			return new SnapRow
			{
				Uid = s.Uid,
				Name = (s.Name ?? ""),
				PingAvg = s.PingAvg,
				PingMax = s.MaxPing,
				OutKBs = s.OutBytesSec / 1024f,
				InKBs = s.InBytesSec / 1024f,
				QueueBytes = s.SendQueue,
				Zdo = s.OwnedZdos,
				Top = (s.TopPrefabs ?? ""),
				Reported = (r != null),
				AvgFps = (r?.AvgFps ?? 0f),
				MinFps = (r?.MinFps ?? 0f),
				ModCount = (r?.ModCount ?? 0),
				PingLv = s.PingLv,
				ZdoLv = s.ZdoLv,
				FpsLv = s.FpsLv,
				QueueLv = s.QueueLv,
				TrafficLv = s.TrafficLv,
				Level = s.Level
			};
		}

		internal void Write(ZPackage p)
		{
			p.Write(Uid);
			p.Write(Name);
			p.Write(PingAvg);
			p.Write(PingMax);
			p.Write(OutKBs);
			p.Write(InKBs);
			p.Write(QueueBytes);
			p.Write(Zdo);
			p.Write(Top);
			p.Write(Reported);
			p.Write(AvgFps);
			p.Write(MinFps);
			p.Write(ModCount);
			p.Write(PingLv);
			p.Write(ZdoLv);
			p.Write(FpsLv);
			p.Write(QueueLv);
			p.Write(TrafficLv);
			p.Write(Level);
		}

		internal static SnapRow Read(ZPackage p)
		{
			return new SnapRow
			{
				Uid = p.ReadLong(),
				Name = p.ReadString(),
				PingAvg = p.ReadInt(),
				PingMax = p.ReadInt(),
				OutKBs = p.ReadSingle(),
				InKBs = p.ReadSingle(),
				QueueBytes = p.ReadInt(),
				Zdo = p.ReadInt(),
				Top = p.ReadString(),
				Reported = p.ReadBool(),
				AvgFps = p.ReadSingle(),
				MinFps = p.ReadSingle(),
				ModCount = p.ReadInt(),
				PingLv = p.ReadByte(),
				ZdoLv = p.ReadByte(),
				FpsLv = p.ReadByte(),
				QueueLv = p.ReadByte(),
				TrafficLv = p.ReadByte(),
				Level = p.ReadByte()
			};
		}
	}
	internal class ClientReport
	{
		public float AvgFps;

		public float MinFps;

		public int ModCount;

		public string Mods = "";

		public float ReceivedAt;
	}
	internal static class ClientReportStore
	{
		private const float StaleSec = 30f;

		internal static readonly Dictionary<long, ClientReport> Reports = new Dictionary<long, ClientReport>();

		internal static ClientReport GetFresh(long uid, float now)
		{
			if (Reports.TryGetValue(uid, out var value) && now - value.ReceivedAt <= 30f)
			{
				return value;
			}
			return null;
		}
	}
	internal static class LagScore
	{
		internal static void Apply(PeerStat s, ClientReport report)
		{
			s.PingLv = High(s.PingAvg, Plugin.PingWarnMs.Value, Plugin.PingBadMs.Value);
			s.ZdoLv = High(s.OwnedZdos, Plugin.ZdoWarn.Value, Plugin.ZdoBad.Value);
			s.QueueLv = High((float)s.SendQueue / 1024f, Plugin.QueueWarnKB.Value, Plugin.QueueBadKB.Value);
			s.TrafficLv = High((s.OutBytesSec + s.InBytesSec) / 1024f, Plugin.TrafficWarnKBs.Value, Plugin.TrafficBadKBs.Value);
			s.FpsLv = (byte)((report != null && report.AvgFps > 0f) ? Low(report.AvgFps, Plugin.FpsWarn.Value, Plugin.FpsBad.Value) : 0);
			s.Level = (byte)Mathf.Max(new int[5] { s.PingLv, s.ZdoLv, s.QueueLv, s.TrafficLv, s.FpsLv });
		}

		private static byte High(float v, float warn, float bad)
		{
			return (byte)((v >= bad) ? 2u : ((v >= warn) ? 1u : 0u));
		}

		private static byte Low(float v, float warn, float bad)
		{
			return (byte)((v <= bad) ? 2u : ((v <= warn) ? 1u : 0u));
		}
	}
	internal class PeerStat
	{
		public long Uid;

		public string Name;

		public int Ping;

		public int PingAvg;

		public int MaxPing;

		public float OutBytesSec;

		public float InBytesSec;

		public int SendQueue;

		public int OwnedZdos;

		public string TopPrefabs = "";

		public float SampledAt;

		public byte PingLv;

		public byte ZdoLv;

		public byte FpsLv;

		public byte QueueLv;

		public byte TrafficLv;

		public byte Level;
	}
	internal class PeerSampler : MonoBehaviour
	{
		internal static readonly Dictionary<long, PeerStat> Stats = new Dictionary<long, PeerStat>();

		private float _nextSample;

		private float _nextZdoScan;

		private void Update()
		{
			if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer())
			{
				return;
			}
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			if (LagWatchRpc.HasSubscribers)
			{
				ServerMonitor.OnFrame(Time.unscaledDeltaTime);
			}
			if (ZdoOwnership.Scanning)
			{
				ZdoOwnership.Step(20000);
			}
			else if (ZdoScanNeeded() && (!ZdoOwnership.HasData || realtimeSinceStartup >= _nextZdoScan))
			{
				_nextZdoScan = realtimeSinceStartup + Plugin.ZdoScanIntervalSec.Value;
				ZdoOwnership.BeginScan();
			}
			if (realtimeSinceStartup >= _nextSample)
			{
				_nextSample = realtimeSinceStartup + Plugin.SampleIntervalSec.Value;
				Sample(realtimeSinceStartup);
				if (LagWatchRpc.HasSubscribers)
				{
					ServerMonitor.Sample(realtimeSinceStartup);
				}
				LagWatchRpc.PushSnapshots();
			}
		}

		private static bool ZdoScanNeeded()
		{
			return LagWatchRpc.HasSubscribers || ServerHealth.Active || Plugin.AutoWarnEnabled.Value || Plugin.LogSamples.Value;
		}

		private static void Sample(float now)
		{
			HashSet<long> hashSet = new HashSet<long>();
			float num = default(float);
			float num2 = default(float);
			int ping = default(int);
			float outBps = default(float);
			float inBps = default(float);
			foreach (ZNetPeer peer in ZNet.instance.GetPeers())
			{
				if (peer.IsReady())
				{
					peer.m_socket.GetConnectionQuality(ref num, ref num2, ref ping, ref outBps, ref inBps);
					Record(peer.m_uid, peer.m_playerName, ping, outBps, inBps, peer.m_socket.GetSendQueueSize(), now, hashSet, isHost: false);
				}
			}
			if (!ZNet.instance.IsDedicated() && (Object)(object)Player.m_localPlayer != (Object)null && ZRoutedRpc.instance != null)
			{
				Record(ZRoutedRpc.instance.GetServerPeerID(), Player.m_localPlayer.GetPlayerName(), 0, 0f, 0f, 0, now, hashSet, isHost: true);
			}
			List<long> list = new List<long>();
			foreach (long key in Stats.Keys)
			{
				if (!hashSet.Contains(key))
				{
					list.Add(key);
				}
			}
			foreach (long item in list)
			{
				Stats.Remove(item);
				ClientReportStore.Reports.Remove(item);
				LagWatchRpc.ForgetWarn(item);
			}
		}

		private static void Record(long uid, string name, int ping, float outBps, float inBps, int queue, float now, HashSet<long> seen, bool isHost)
		{
			PeerStat value;
			bool flag = !Stats.TryGetValue(uid, out value);
			if (flag)
			{
				value = new PeerStat
				{
					Uid = uid
				};
				Stats[uid] = value;
			}
			seen.Add(uid);
			int num = ((!flag) ? value.Level : 0);
			value.Name = name;
			value.Ping = ping;
			value.PingAvg = (flag ? ping : Mathf.RoundToInt((float)value.PingAvg * 0.6f + (float)ping * 0.4f));
			value.MaxPing = Mathf.Max(value.MaxPing, ping);
			value.OutBytesSec = outBps;
			value.InBytesSec = inBps;
			value.SendQueue = queue;
			value.OwnedZdos = (ZdoOwnership.Owners.TryGetValue(uid, out var value2) ? value2.Count : 0);
			value.TopPrefabs = ZdoOwnership.TopPrefabs(uid, 3);
			value.SampledAt = now;
			ClientReport fresh = ClientReportStore.GetFresh(uid, now);
			LagScore.Apply(value, fresh);
			if (Plugin.LogSamples.Value)
			{
				Plugin.Log.LogInfo((object)string.Format("[LagWatch] {0}({1}) lv={2} ping={3}ms out={4:F0}B/s in={5:F0}B/s queue={6} zdo={7} fps={8} top=[{9}]", value.Name, value.Uid, value.Level, value.PingAvg, value.OutBytesSec, value.InBytesSec, value.SendQueue, value.OwnedZdos, (fresh != null) ? fresh.AvgFps.ToString("F0") : "n/a", value.TopPrefabs));
			}
			if (value.Level != num)
			{
				Plugin.Log.LogInfo((object)$"[LagWatch] {value.Name}({value.Uid}) status {num} -> {value.Level} (ping={value.PingAvg}ms zdo={value.OwnedZdos} queue={value.SendQueue / 1024}KB)");
			}
			if (value.Level == 2 && Plugin.AutoWarnEnabled.Value && !isHost)
			{
				LagWatchRpc.TryWarn(uid, auto: true);
			}
		}
	}
	internal enum PrefabKind : byte
	{
		None,
		Built,
		Item,
		Monster,
		Debris
	}
	internal static class PrefabClassifier
	{
		private static readonly Dictionary<int, PrefabKind> Cache = new Dictionary<int, PrefabKind>();

		internal static PrefabKind Classify(int prefabHash)
		{
			if (Cache.TryGetValue(prefabHash, out var value))
			{
				return value;
			}
			if ((Object)(object)ZNetScene.instance == (Object)null)
			{
				return PrefabKind.None;
			}
			GameObject prefab = ZNetScene.instance.GetPrefab(prefabHash);
			PrefabKind prefabKind = (((Object)(object)prefab != (Object)null) ? ClassifyPrefab(prefab) : PrefabKind.None);
			Cache[prefabHash] = prefabKind;
			return prefabKind;
		}

		private static PrefabKind ClassifyPrefab(GameObject p)
		{
			if ((Object)(object)p.GetComponent<Piece>() != (Object)null || (Object)(object)p.GetComponent<Container>() != (Object)null || (Object)(object)p.GetComponent<TombStone>() != (Object)null)
			{
				return PrefabKind.Built;
			}
			if ((Object)(object)p.GetComponent<Player>() != (Object)null || (Object)(object)p.GetComponent<Trader>() != (Object)null || (Object)(object)p.GetComponent<Fish>() != (Object)null)
			{
				return PrefabKind.None;
			}
			Character component = p.GetComponent<Character>();
			if ((Object)(object)component != (Object)null)
			{
				return IsHostile(component) ? PrefabKind.Monster : PrefabKind.None;
			}
			if ((Object)(object)p.GetComponent<ItemDrop>() != (Object)null)
			{
				return PrefabKind.Item;
			}
			if ((Object)(object)p.GetComponent<Ragdoll>() != (Object)null)
			{
				return PrefabKind.Debris;
			}
			ZNetView component2 = p.GetComponent<ZNetView>();
			if ((Object)(object)component2 != (Object)null && !component2.m_persistent && (Object)(object)p.GetComponent<TimedDestruction>() != (Object)null && (Object)(object)p.GetComponent<Projectile>() == (Object)null)
			{
				return PrefabKind.Debris;
			}
			return PrefabKind.None;
		}

		private static bool IsHostile(Character c)
		{
			//IL_0010: Unknown result type (might be due to invalid IL or missing references)
			//IL_0015: Unknown result type (might be due to invalid IL or missing references)
			//IL_0016: 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_0018: Unknown result type (might be due to invalid IL or missing references)
			//IL_001a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0050: Expected I4, but got Unknown
			if (c.m_boss)
			{
				return false;
			}
			Faction faction = c.m_faction;
			Faction val = faction;
			switch (val - 2)
			{
			case 0:
			case 1:
			case 2:
			case 3:
			case 4:
			case 5:
			case 7:
			case 8:
			case 11:
				return true;
			default:
				return false;
			}
		}
	}
	internal class ClearCounts
	{
		public int Items;

		public int Monsters;

		public int Debris;
	}
	internal class ServerMaintenance : MonoBehaviour
	{
		internal static ServerMaintenance Instance;

		internal const int ClearOk = 0;

		internal const int ClearExpired = 1;

		internal const int ClearBusy = 2;

		internal const int ClearDisabled = 3;

		private const float TokenLifeSec = 60f;

		private const float ClearCooldownSec = 30f;

		private const float MemCooldownSec = 60f;

		private string _token;

		private long _tokenOwner;

		private float _tokenAt;

		private float _lastClear = -999f;

		private float _lastMem = -999f;

		private bool _busy;

		private void Awake()
		{
			Instance = this;
		}

		internal ClearCounts Preview(long admin, out string token)
		{
			token = "";
			ClearCounts clearCounts = new ClearCounts();
			if (!Plugin.AllowServerClear.Value || ZDOMan.instance == null)
			{
				return clearCounts;
			}
			Collect(out var items, out var monsters, out var debris);
			clearCounts.Items = items.Count;
			clearCounts.Monsters = monsters.Count;
			clearCounts.Debris = debris.Count;
			token = Guid.NewGuid().ToString("N");
			_token = token;
			_tokenOwner = admin;
			_tokenAt = Time.realtimeSinceStartup;
			return clearCounts;
		}

		internal int TryRun(long admin, string token, out ClearCounts counts)
		{
			counts = new ClearCounts();
			if (!Plugin.AllowServerClear.Value)
			{
				return 3;
			}
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			if (string.IsNullOrEmpty(_token) || token != _token || admin != _tokenOwner || realtimeSinceStartup - _tokenAt > 60f)
			{
				return 1;
			}
			if (_busy || realtimeSinceStartup - _lastClear < 30f)
			{
				return 2;
			}
			_token = null;
			_lastClear = realtimeSinceStartup;
			Collect(out var items, out var monsters, out var debris);
			counts.Items = items.Count;
			counts.Monsters = monsters.Count;
			counts.Debris = debris.Count;
			_busy = true;
			((MonoBehaviour)this).StartCoroutine(RunClear(admin, items, monsters, debris));
			return 0;
		}

		private IEnumerator RunClear(long admin, List<ZDOID> items, List<ZDOID> monsters, List<ZDOID> debris)
		{
			int doneItems = 0;
			int doneMonsters = 0;
			int doneDebris = 0;
			int batch = Mathf.Max(10, Plugin.ClearBatchPerFrame.Value);
			int inBatch = 0;
			foreach (ZDOID id in items)
			{
				if (DestroyZdo(id, recheckTamed: false))
				{
					doneItems++;
				}
				int num = inBatch + 1;
				inBatch = num;
				if (num >= batch)
				{
					inBatch = 0;
					yield return null;
				}
			}
			foreach (ZDOID id2 in monsters)
			{
				if (DestroyZdo(id2, recheckTamed: true))
				{
					doneMonsters++;
				}
				int num = inBatch + 1;
				inBatch = num;
				if (num >= batch)
				{
					inBatch = 0;
					yield return null;
				}
			}
			foreach (ZDOID id3 in debris)
			{
				if (DestroyZdo(id3, recheckTamed: false))
				{
					doneDebris++;
				}
				int num = inBatch + 1;
				inBatch = num;
				if (num >= batch)
				{
					inBatch = 0;
					yield return null;
				}
			}
			_busy = false;
			Plugin.Log.LogInfo((object)$"[LagWatch] server clear by admin peer {admin}: items={doneItems} monsters={doneMonsters} debris={doneDebris}");
			AdminActionRpc.SendClearResult(admin, 0, doneItems, doneMonsters, doneDebris);
		}

		private static bool DestroyZdo(ZDOID id, bool recheckTamed)
		{
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			ZDOMan instance = ZDOMan.instance;
			ZDO val = ((instance != null) ? instance.GetZDO(id) : null);
			if (val == null)
			{
				return false;
			}
			if (recheckTamed && val.GetBool(ZDOVars.s_tamed, false))
			{
				return false;
			}
			val.SetOwner(ZDOMan.GetSessionID());
			ZDOMan.instance.DestroyZDO(val);
			return true;
		}

		private static void Collect(out List<ZDOID> items, out List<ZDOID> monsters, out List<ZDOID> debris)
		{
			//IL_007e: Unknown result type (might be due to invalid IL or missing references)
			//IL_008e: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b3: Unknown result type (might be due to invalid IL or missing references)
			items = new List<ZDOID>();
			monsters = new List<ZDOID>();
			debris = new List<ZDOID>();
			if (ZDOMan.instance == null || (Object)(object)ZNetScene.instance == (Object)null)
			{
				return;
			}
			foreach (ZDO value in ZDOMan.instance.m_objectsByID.Values)
			{
				switch (PrefabClassifier.Classify(value.GetPrefab()))
				{
				case PrefabKind.Item:
					items.Add(value.m_uid);
					break;
				case PrefabKind.Debris:
					debris.Add(value.m_uid);
					break;
				case PrefabKind.Monster:
					if (!value.GetBool(ZDOVars.s_tamed, false))
					{
						monsters.Add(value.m_uid);
					}
					break;
				}
			}
		}

		internal int TryCleanMemory(long admin)
		{
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			if (_busy || realtimeSinceStartup - _lastMem < 60f)
			{
				return 2;
			}
			_lastMem = realtimeSinceStartup;
			_busy = true;
			((MonoBehaviour)this).StartCoroutine(RunCleanMemory(admin));
			return 0;
		}

		private IEnumerator RunCleanMemory(long admin)
		{
			Stopwatch sw = Stopwatch.StartNew();
			ServerMonitor.ReadMemory(out var wsBefore, out var heapBefore);
			ServerHealth.ClearErrorTable();
			AsyncOperation unload = Resources.UnloadUnusedAssets();
			while (!unload.isDone)
			{
				yield return null;
			}
			GC.Collect();
			yield return null;
			yield return (object)new WaitForSecondsRealtime(1f);
			ServerMonitor.ReadMemory(out var wsAfter, out var heapAfter);
			sw.Stop();
			_busy = false;
			Plugin.Log.LogInfo((object)$"[LagWatch] memory cleanup by admin peer {admin}: process {wsBefore:F0}MB -> {wsAfter:F0}MB, managed heap {heapBefore:F0}MB -> {heapAfter:F0}MB, {sw.Elapsed.TotalSeconds:F1}s");
			AdminActionRpc.SendMemResult(admin, 0, wsBefore, wsAfter, heapBefore, heapAfter, (float)sw.Elapsed.TotalSeconds);
		}
	}
	internal class ServerSnap
	{
		public bool Valid;

		public float CpuPct;

		public float MemMB;

		public float GcMB;

		public float FrameAvgMs;

		public float FrameMaxMs;

		public float OutKBs;

		public float InKBs;

		public int Hitches;

		public int Peers;

		public int Zdo;

		public int ScanMs;

		public int UptimeMin;

		public byte CpuLv;

		public byte FrameLv;

		public byte Level;

		internal void Write(ZPackage p)
		{
			p.Write(Valid);
			p.Write(CpuPct);
			p.Write(MemMB);
			p.Write(GcMB);
			p.Write(FrameAvgMs);
			p.Write(FrameMaxMs);
			p.Write(OutKBs);
			p.Write(InKBs);
			p.Write(Hitches);
			p.Write(Peers);
			p.Write(Zdo);
			p.Write(ScanMs);
			p.Write(UptimeMin);
			p.Write(CpuLv);
			p.Write(FrameLv);
			p.Write(Level);
		}

		internal static ServerSnap Read(ZPackage p)
		{
			return new ServerSnap
			{
				Valid = p.ReadBool(),
				CpuPct = p.ReadSingle(),
				MemMB = p.ReadSingle(),
				GcMB = p.ReadSingle(),
				FrameAvgMs = p.ReadSingle(),
				FrameMaxMs = p.ReadSingle(),
				OutKBs = p.ReadSingle(),
				InKBs = p.ReadSingle(),
				Hitches = p.ReadInt(),
				Peers = p.ReadInt(),
				Zdo = p.ReadInt(),
				ScanMs = p.ReadInt(),
				UptimeMin = p.ReadInt(),
				CpuLv = p.ReadByte(),
				FrameLv = p.ReadByte(),
				Level = p.ReadByte()
			};
		}
	}
	internal static class ServerMonitor
	{
		internal static ServerSnap Current = new ServerSnap();

		private static Process _proc;

		private static TimeSpan _lastCpu;

		private static float _lastWall = -1f;

		private static int _frames;

		private static int _hitches;

		private static float _sumDt;

		private static float _maxDt;

		internal static void OnFrame(float dt)
		{
			_frames++;
			_sumDt += dt;
			if (dt > _maxDt)
			{
				_maxDt = dt;
			}
			if (dt * 1000f >= Plugin.DiagHitchMs.Value)
			{
				_hitches++;
			}
		}

		internal static void Sample(float now)
		{
			ServerSnap serverSnap = new ServerSnap
			{
				Valid = true
			};
			serverSnap.FrameAvgMs = ((_frames > 0) ? (_sumDt / (float)_frames * 1000f) : 0f);
			serverSnap.FrameMaxMs = _maxDt * 1000f;
			serverSnap.Hitches = _hitches;
			_frames = (_hitches = 0);
			_sumDt = (_maxDt = 0f);
			try
			{
				if (_proc == null)
				{
					_proc = Process.GetCurrentProcess();
				}
				_proc.Refresh();
				TimeSpan totalProcessorTime = _proc.TotalProcessorTime;
				float num = now - _lastWall;
				bool flag = _lastWall < 0f || num > Plugin.SampleIntervalSec.Value * 3f;
				serverSnap.CpuPct = (flag ? Current.CpuPct : ((float)((totalProcessorTime - _lastCpu).TotalSeconds / (double)Mathf.Max(num, 0.1f) * 100.0)));
				_lastCpu = totalProcessorTime;
				_lastWall = now;
				serverSnap.MemMB = (float)_proc.WorkingSet64 / 1048576f;
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[LagWatch] process stats unavailable: " + ex.Message));
			}
			serverSnap.GcMB = (float)GC.GetTotalMemory(forceFullCollection: false) / 1048576f;
			foreach (PeerStat value in PeerSampler.Stats.Values)
			{
				serverSnap.OutKBs += value.OutBytesSec / 1024f;
				serverSnap.InKBs += value.InBytesSec / 1024f;
			}
			serverSnap.Peers = PeerSampler.Stats.Count;
			serverSnap.Zdo = ZdoOwnership.TotalZdos;
			serverSnap.ScanMs = (int)ZdoOwnership.LastScanMs;
			serverSnap.UptimeMin = (int)(Time.realtimeSinceStartup / 60f);
			serverSnap.CpuLv = High(serverSnap.CpuPct, Plugin.ServerCpuWarn.Value, Plugin.ServerCpuBad.Value);
			serverSnap.FrameLv = High(serverSnap.FrameAvgMs, Plugin.ServerFrameWarnMs.Value, Plugin.ServerFrameBadMs.Value);
			serverSnap.Level = (byte)Mathf.Max((int)serverSnap.CpuLv, (int)serverSnap.FrameLv);
			Current = serverSnap;
		}

		internal static void ReadMemory(out float workingSetMB, out float managedHeapMB)
		{
			workingSetMB = 0f;
			try
			{
				if (_proc == null)
				{
					_proc = Process.GetCurrentProcess();
				}
				_proc.Refresh();
				workingSetMB = (float)_proc.WorkingSet64 / 1048576f;
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[LagWatch] process memory unavailable: " + ex.Message));
			}
			managedHeapMB = (float)GC.GetTotalMemory(forceFullCollection: false) / 1048576f;
		}

		private static byte High(float v, float warn, float bad)
		{
			return (byte)((v >= bad) ? 2u : ((v >= warn) ? 1u : 0u));
		}
	}
	internal static class ZdoOwnership
	{
		internal class OwnerInfo
		{
			public int Count;

			public readonly Dictionary<int, int> PrefabCounts = new Dictionary<int, int>();
		}

		internal const int StepBudget = 20000;

		internal static Dictionary<long, OwnerInfo> Owners = new Dictionary<long, OwnerInfo>();

		internal static Dictionary<int, int> GlobalPrefabs = new Dictionary<int, int>();

		internal static int TotalZdos;

		internal static long LastScanMs;

		internal static bool HasData;

		private static ZDO[] _snapshot = (ZDO[])(object)new ZDO[0];

		private static int _count;

		private static int _pos;

		private static bool _scanning;

		private static bool _countGlobal;

		private static Dictionary<long, OwnerInfo> _tmpOwners = new Dictionary<long, OwnerInfo>();

		private static Dictionary<int, int> _tmpGlobal = new Dictionary<int, int>();

		private static long _workTicks;

		internal static bool Scanning => _scanning;

		internal static void BeginScan()
		{
			if (!_scanning && ZDOMan.instance != null)
			{
				Stopwatch stopwatch = Stopwatch.StartNew();
				Dictionary<ZDOID, ZDO> objectsByID = ZDOMan.instance.m_objectsByID;
				_count = objectsByID.Count;
				if (_snapshot.Length < _count)
				{
					_snapshot = (ZDO[])(object)new ZDO[_count + _count / 4 + 16];
				}
				objectsByID.Values.CopyTo(_snapshot, 0);
				stopwatch.Stop();
				_pos = 0;
				_countGlobal = ServerHealth.Active;
				_tmpOwners = new Dictionary<long, OwnerInfo>();
				_tmpGlobal = new Dictionary<int, int>();
				_workTicks = stopwatch.ElapsedTicks;
				_scanning = true;
			}
		}

		internal static void Step(int budget)
		{
			if (!_scanning)
			{
				return;
			}
			Stopwatch stopwatch = Stopwatch.StartNew();
			int num = Math.Min(_pos + budget, _count);
			while (_pos < num)
			{
				ZDO val = _snapshot[_pos];
				if (val != null)
				{
					int prefab = val.GetPrefab();
					if (_countGlobal)
					{
						_tmpGlobal.TryGetValue(prefab, out var value);
						_tmpGlobal[prefab] = value + 1;
					}
					long owner = val.GetOwner();
					if (owner != 0 && PrefabClassifier.Classify(prefab) != PrefabKind.Built)
					{
						if (!_tmpOwners.TryGetValue(owner, out var value2))
						{
							value2 = new OwnerInfo();
							_tmpOwners[owner] = value2;
						}
						value2.Count++;
						value2.PrefabCounts.TryGetValue(prefab, out var value3);
						value2.PrefabCounts[prefab] = value3 + 1;
					}
				}
				_pos++;
			}
			stopwatch.Stop();
			_workTicks += stopwatch.ElapsedTicks;
			if (_pos >= _count)
			{
				Owners = _tmpOwners;
				GlobalPrefabs = _tmpGlobal;
				TotalZdos = _count;
				LastScanMs = _workTicks * 1000 / Stopwatch.Frequency;
				HasData = true;
				_scanning = false;
				Array.Clear(_snapshot, 0, _count);
			}
		}

		internal static string TopPrefabs(long owner, int take)
		{
			if (!Owners.TryGetValue(owner, out var value))
			{
				return "";
			}
			List<KeyValuePair<int, int>> list = new List<KeyValuePair<int, int>>(value.PrefabCounts);
			list.Sort((KeyValuePair<int, int> a, KeyValuePair<int, int> b) => b.Value.CompareTo(a.Value));
			List<string> list2 = new List<string>();
			for (int num = 0; num < list.Count && num < take; num++)
			{
				GameObject val = (((Object)(object)ZNetScene.instance != (Object)null) ? ZNetScene.instance.GetPrefab(list[num].Key) : null);
				string arg = (((Object)(object)val != (Object)null) ? ((Object)val).name : list[num].Key.ToString());
				list2.Add($"{arg}x{list[num].Value}");
			}
			return string.Join(", ", list2);
		}
	}
	internal class ButtonFx : MonoBehaviour, IPointerEnterHandler, IEventSystemHandler, IPointerExitHandler, IPointerDownHandler, IPointerUpHandler
	{
		private const float HoverScale = 1.04f;

		private const float PressScale = 0.97f;

		private const float Speed = 18f;

		private Button _button;

		private bool _hover;

		private bool _down;

		private void Awake()
		{
			_button = ((Component)this).GetComponent<Button>();
		}

		private void OnDisable()
		{
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			_hover = (_down = false);
			((Component)this).transform.localScale = Vector3.one;
		}

		public void OnPointerEnter(PointerEventData e)
		{
			_hover = true;
		}

		public void OnPointerExit(PointerEventData e)
		{
			_hover = false;
			_down = false;
		}

		public void OnPointerDown(PointerEventData e)
		{
			_down = true;
		}

		public void OnPointerUp(PointerEventData e)
		{
			_down = false;
		}

		private void Update()
		{
			//IL_0052: 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)
			float num = ((!((Object)(object)_button == (Object)null) && !((Selectable)_button).interactable) ? 1f : (_down ? 0.97f : (_hover ? 1.04f : 1f)));
			float x = ((Component)this).transform.localScale.x;
			if (!Mathf.Approximately(x, num))
			{
				float num2 = Mathf.Lerp(x, num, 1f - Mathf.Exp(-18f * Time.unscaledDeltaTime));
				if (Mathf.Abs(num2 - num) < 0.0005f)
				{
					num2 = num;
				}
				((Component)this).transform.localScale = new Vector3(num2, num2, 1f);
			}
		}
	}
	internal class LagPanel : MonoBehaviour
	{
		private class Row
		{
			public GameObject Go;

			public Image Bg;

			public Image Light;

			public Text Name;

			public Text Ping;

			public Text Fps;

			public Text Objects;

			public Text Queue;

			public Text Traffic;

			public Text Mods;

			public Text Status;

			public Text SendLabel;

			public Button SendBtn;

			public float LabelResetAt;

			public float PendingSince;
		}

		internal static LagPanel Instance;

		private const int ModPreviewCount = 12;

		private const float AckLabelSec = 2.5f;

		private const float AckTimeoutSec = 6f;

		private static readonly float[] Cols = new float[10] { 32f, 230f, 100f, 90f, 130f, 110f, 120f, 80f, 100f, 150f };

		private static readonly string[] ColKeys = new string[10] { "", "colPlayer", "colPing", "colFps", "colObjects", "colQueue", "colTraffic", "colMods", "colStatus", "" };

		private GameObject _canvasGo;

		private Transform _content;

		private Text _summary;

		private Text _autoLabel;

		private Text _detail;

		private Text _empty;

		private Button _autoBtn;

		private readonly Dictionary<Text, string> _labels = new Dictionary<Text, string>();

		private readonly Dictionary<long, Row> _rows = new Dictionary<long, Row>();

		private readonly Dictionary<long, string> _mods = new Dictionary<long, string>();

		private List<SnapRow> _latest = new List<SnapRow>();

		private ServerSnap _server;

		private Image _srvLight;

		private Text[] _srvCells;

		private GameObject _dialog;

		private Text _dialogText;

		private Text _dialogOkLabel;

		private Text _dialogCancelLabel;

		private Button _dialogOk;

		private Button _dialogCancel;

		private Action _onConfirm;

		private bool _auto;

		private long _selected;

		private bool _open;

		private bool _openedInventory;

		private static readonly float[] SrvCols = new float[9] { 110f, 110f, 190f, 120f, 100f, 90f, 130f, 150f, 100f };

		private void Awake()
		{
			Instance = this;
		}

		private void Update()
		{
			//IL_008c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0091: Unknown result type (might be due to invalid IL or missing references)
			//IL_003d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0042: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsDedicated())
			{
				return;
			}
			KeyboardShortcut value;
			if (_open)
			{
				if (!Input.GetKeyDown((KeyCode)27))
				{
					value = Plugin.PanelHotkey.Value;
					if (!((KeyboardShortcut)(ref value)).IsDown())
					{
						if (_openedInventory && !InventoryGui.IsVisible())
						{
							Close();
						}
						else
						{
							TickLabels();
						}
						return;
					}
				}
				Close();
			}
			else
			{
				value = Plugin.PanelHotkey.Value;
				if (((KeyboardShortcut)(ref value)).IsDown() && !TypingInGame())
				{
					Open();
				}
			}
		}

		private static bool TypingInGame()
		{
			return ((Object)(object)Chat.instance != (Object)null && Chat.instance.HasFocus()) || Console.IsVisible() || TextInput.IsVisible();
		}

		private void Open()
		{
			if ((Object)(object)Player.m_localPlayer == (Object)null)
			{
				return;
			}
			if (!AdminGate.IsLocalAdmin)
			{
				AdminGate.Request();
				MessageHud instance = MessageHud.instance;
				if (instance != null)
				{
					instance.ShowMessage((MessageType)2, Strings.Get("adminOnly"), 0, (Sprite)null, false, true);
				}
				return;
			}
			EnsureUi();
			foreach (KeyValuePair<Text, string> label in _labels)
			{
				label.Key.text = Strings.Get(label.Value);
			}
			_canvasGo.SetActive(true);
			_open = true;
			if ((Object)(object)InventoryGui.instance != (Object)null && !InventoryGui.IsVisible())
			{
				InventoryGui.instance.Show((Container)null, 1);
				_openedInventory = true;
			}
			Refresh();
			LagWatchRpc.SendSubscribe(on: true);
		}

		private void Close()
		{
			if (_open)
			{
				_open = false;
				LagWatchRpc.SendSubscribe(on: false);
				if ((Object)(object)_dialog != (Object)null)
				{
					_dialog.SetActive(false);
				}
				_onConfirm = null;
				if ((Object)(object)_canvasGo != (Object)null)
				{
					_canvasGo.SetActive(false);
				}
				if (_openedInventory && (Object)(object)InventoryGui.instance != (Object)null && InventoryGui.IsVisible())
				{
					InventoryGui.instance.Hide();
				}
				_openedInventory = false;
			}
		}

		internal void OnSnapshot(bool autoWarn, ServerSnap server, List<SnapRow> rows)
		{
			_auto = autoWarn;
			_server = server;
			_latest = rows;
			if (_open)
			{
				Refresh();
			}
		}

		internal void OnWarnAck(long uid, int code)
		{
			if (_rows.TryGetValue(uid, out var value))
			{
				value.SendLabel.text = Strings.Get(code switch
				{
					1 => "btnCooldown", 
					0 => "btnSent", 
					_ => "btnNoMod", 
				});
				UiKit.SetButtonStyle(value.SendBtn, (code == 0) ? UiKit.BtnStyle.Success : UiKit.BtnStyle.Neutral);
				value.PendingSince = 0f;
				value.LabelResetAt = Time.unscaledTime + 2.5f;
			}
		}

		internal void OnDetail(long uid, string mods)
		{
			_mods[uid] = mods;
			if (_open && uid == _selected)
			{
				RefreshDetail();
			}
		}

		private void TickLabels()
		{
			float unscaledTime = Time.unscaledTime;
			foreach (Row value in _rows.Values)
			{
				bool flag = value.PendingSince > 0f && unscaledTime - value.PendingSince > 6f;
				bool flag2 = value.LabelResetAt > 0f && unscaledTime >= value.LabelResetAt;
				if (flag || flag2)
				{
					value.SendLabel.text = Strings.Get("btnSend");
					UiKit.SetButtonStyle(value.SendBtn, UiKit.BtnStyle.Primary);
					value.PendingSince = 0f;
					value.LabelResetAt = 0f;
				}
			}
		}

		private void Refresh()
		{
			_autoLabel.text = Strings.Get(_auto ? "autoOn" : "autoOff");
			UiKit.SetButtonStyle(_autoBtn, _auto ? UiKit.BtnStyle.Success : UiKit.BtnStyle.Neutral);
			RefreshServer();
			int num = 0;
			int num2 = 0;
			int num3 = 0;
			foreach (SnapRow item in _latest)
			{
				if (item.Level >= 2)
				{
					num3++;
				}
				else if (item.Level == 1)
				{
					num2++;
				}
				else
				{
					num++;
				}
			}
			_summary.text = Strings.Fmt("summary", num, num2, num3);
			((Component)_empty).gameObject.SetActive(_latest.Count == 0);
			List<SnapRow> list = new List<SnapRow>(_latest);
			list.Sort((SnapRow a, SnapRow b) => (a.Level != b.Level) ? b.Level.CompareTo(a.Level) : b.PingAvg.CompareTo(a.PingAvg));
			HashSet<long> hashSet = new HashSet<long>();
			for (int num4 = 0; num4 < list.Count; num4++)
			{
				SnapRow snapRow = list[num4];
				hashSet.Add(snapRow.Uid);
				if (!_rows.TryGetValue(snapRow.Uid, out var value))
				{
					value = CreateRow(snapRow.Uid);
					_rows[snapRow.Uid] = value;
				}
				value.Go.transform.SetSiblingIndex(num4);
				UpdateRow(value, snapRow);
			}
			List<long> list2 = new List<long>();
			foreach (long key in _rows.Keys)
			{
				if (!hashSet.Contains(key))
				{
					list2.Add(key);
				}
			}
			foreach (long item2 in list2)
			{
				Object.Destroy((Object)(object)_rows[item2].Go);
				_rows.Remove(item2);
				_mods.Remove(item2);
			}
			RefreshDetail();
		}

		private void RefreshServer()
		{
			//IL_007b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0074: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a4: Unknown result type (might be due to invalid IL or missing references)
			//IL_009d: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cb: Unknown result type (might be due to invalid IL or missing references)
			//IL_015a: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b5: Unknown result type (might be due to invalid IL or missing references)
			//IL_019a: Unknown result type (might be due to invalid IL or missing references)
			//IL_013f: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ec: Unknown result type (might be due to invalid IL or missing references)
			//IL_0223: Unknown result type (might be due to invalid IL or missing references)
			//IL_025f: Unknown result type (might be due to invalid IL or missing references)
			//IL_02ab: Unknown result type (might be due to invalid IL or missing references)
			//IL_02e2: Unknown result type (might be due to invalid IL or missing references)
			if (_srvCells != null)
			{
				ServerSnap serverSnap = ((_server != null && _server.Valid) ? _server : null);
				int num = serverSnap?.Level ?? 0;
				string value = ((serverSnap == null) ? "--" : Strings.Get((num >= 2) ? "srvBad" : ((num == 1) ? "srvFair" : "srvGood")));
				((Graphic)_srvLight).color = ((serverSnap == null) ? UiKit.Gray : UiKit.ServerColor(num));
				_srvCells[0].text = Cell(Strings.Get("srvState"), value, (serverSnap == null) ? UiKit.Gray : UiKit.ServerColor(num));
				_srvCells[1].text = Cell(Strings.Get("srvPing"), MyPing(), UiKit.TextMain);
				_srvCells[2].text = ((serverSnap == null) ? Cell(Strings.Get("srvFrame"), "--", UiKit.Gray) : Cell((serverSnap.Hitches > 0) ? Strings.Fmt("srvHitch", serverSnap.Hitches) : Strings.Get("srvFrame"), $"{serverSnap.FrameAvgMs:F0}/{serverSnap.FrameMaxMs:F0}ms", UiKit.ServerColor(serverSnap.FrameLv)));
				_srvCells[3].text = ((serverSnap == null) ? Cell(Strings.Get("srvCpu"), "--", UiKit.Gray) : Cell(Strings.Get("srvCpu"), $"{serverSnap.CpuPct:F0}%", UiKit.ServerColor(serverSnap.CpuLv)));
				_srvCells[4].text = Cell(Strings.Get("srvMem"), (serverSnap == null) ? "--" : FormatMem(serverSnap.MemMB), UiKit.TextMain);
				_srvCells[5].text = Cell(Strings.Get("srvPlayers"), (serverSnap == null) ? "--" : serverSnap.Peers.ToString(), UiKit.TextMain);
				_srvCells[6].text = Cell(Strings.Get("srvObjects"), (serverSnap == null) ? "--" : serverSnap.Zdo.ToString("N0"), UiKit.TextMain);
				_srvCells[7].text = Cell(Strings.Get("srvTraffic"), (serverSnap == null) ? "--" : $"{serverSnap.OutKBs:F0}/{serverSnap.InKBs:F0}KB/s", UiKit.TextMain);
				_srvCells[8].text = Cell(Strings.Get("srvUptime"), (serverSnap == null) ? "--" : FormatUptime(serverSnap.UptimeMin), UiKit.TextMain);
			}
		}

		private static string Cell(string label, string value, Color color)
		{
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			return "<size=15><color=#9a9aa5>" + label + "</color></size>\n<color=#" + ColorUtility.ToHtmlStringRGB(color) + ">" + value + "</color>";
		}

		private static string MyPing()
		{
			if ((Object)(object)ZNet.instance == (Object)null)
			{
				return "--";
			}
			if (ZNet.instance.IsServer())
			{
				return "0ms (" + Strings.Get("srvHost") + ")";
			}
			float num = default(float);
			float num2 = default(float);
			int num3 = default(int);
			float num4 = default(float);
			float num5 = default(float);
			ZNet.instance.GetNetStats(ref num, ref num2, ref num3, ref num4, ref num5);
			return num3 + "ms";
		}

		private static string FormatMem(float mb)
		{
			return (mb >= 1024f) ? $"{mb / 1024f:F1}GB" : $"{mb:F0}MB";
		}

		private static string FormatUptime(int min)
		{
			return (min >= 60) ? $"{min / 60}h {min % 60}m" : $"{min}m";
		}

		private void UpdateRow(Row row, SnapRow s)
		{
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			//IL_005b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0040: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b5: Unknown result type (might be due to invalid IL or missing references)
			((Graphic)row.Light).color = UiKit.LevelColor(s.Level);
			((Graphic)row.Bg).color = ((s.Uid == _selected) ? new Color(0.24f, 0.28f, 0.38f, 0.95f) : new Color(0.14f, 0.14f, 0.17f, 0.92f));
			row.Name.text = s.Name;
			SetCell(row.Ping, $"{s.PingAvg}ms", s.PingLv);
			SetCell(row.Fps, s.Reported ? s.AvgFps.ToString("F0") : "--", s.Reported ? s.FpsLv : (-1));
			SetCell(row.Objects, s.Zdo.ToString(), s.ZdoLv);
			SetCell(row.Queue, $"{s.QueueBytes / 1024}KB", s.QueueLv);
			SetCell(row.Traffic, $"{s.TrafficKBs:F0}KB/s", s.TrafficLv);
			SetCell(row.Mods, s.Reported ? s.ModCount.ToString() : "--", -1);
			row.Status.text = Strings.Get((s.Level >= 2) ? "stBad" : ((s.Level == 1) ? "stWarn" : "stOk"));
			((Graphic)row.Status).color = UiKit.LevelColor(s.Level);
		}

		private static void SetCell(Text t, string value, int level)
		{
			//IL_0020: Unknown result type (might be due to invalid IL or missing references)
			//IL_0019: Unknown result type (might be due to invalid IL or missing references)
			//IL_0012: Unknown result type (might be due to invalid IL or missing references)
			t.text = value;
			((Graphic)t).color = ((level < 0) ? UiKit.Gray : ((level == 0) ? UiKit.TextMain : UiKit.LevelColor(level)));
		}

		private void RefreshDetail()
		{
			SnapRow snapRow = _latest.Find((SnapRow r) => r.Uid == _selected);
			if (snapRow == null)
			{
				_detail.text = Strings.Get("detailEmpty");
				return;
			}
			string text = Strings.Get((snapRow.Level >= 2) ? "stBad" : ((snapRow.Level == 1) ? "stWarn" : "stOk"));
			string text2 = (snapRow.Reported ? snapRow.AvgFps.ToString("F0") : "--");
			string text3 = (snapRow.Reported ? snapRow.MinFps.ToString("F0") : "--");
			string text4 = (snapRow.Reported ? snapRow.ModCount.ToString() : "--");
			_detail.text = Strings.Fmt("detail", snapRow.Name, text, snapRow.PingAvg, snapRow.PingMax, snapRow.Zdo, snapRow.QueueBytes / 1024, snapRow.TrafficKBs.ToString("F0"), text2, text3, text4, string.IsNullOrEmpty(snapRow.Top) ? "-" : snapRow.Top, ModPreview(snapRow));
		}

		private string ModPreview(SnapRow s)
		{
			if (!s.Reported)
			{
				return Strings.Get("notReported");
			}
			if (!_mods.TryGetValue(s.Uid, out var value) || value.Length == 0)
			{
				return "...";
			}
			string[] array = value.Split(new char[1] { '|' });
			int num = Mathf.Min(array.Length, 12);
			string text = string.Join(", ", array, 0, num);
			return (array.Length > num) ? (text + Strings.Fmt("modsMore", array.Length - num)) : text;
		}

		private void Select(long uid)
		{
			_selected = uid;
			if (!_mods.ContainsKey(uid))
			{
				LagWatchRpc.SendDetailRequest(uid);
			}
			Refresh();
		}

		private void EnsureUi()
		{
			//IL_001e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0028: Expected O, but got Unknown
			//IL_0073: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e5: 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_00f9: Unknown result type (might be due to invalid IL or missing references)
			//IL_0118: Unknown result type (might be due to invalid IL or missing references)
			//IL_0140: Unknown result type (might be due to invalid IL or missing references)
			//IL_014f: Unknown result type (might be due to invalid IL or missing references)
			//IL_015e: Unknown result type (might be due to invalid IL or missing references)
			//IL_017a: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a6: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d5: Unknown result type (might be due to invalid IL or missing references)
			//IL_01e4: Unknown result type (might be due to invalid IL or missing references)
			//IL_01f3: Unknown result type (might be due to invalid IL or missing references)
			//IL_0213: Unknown result type (might be due to invalid IL or missing references)
			//IL_0252: Unknown result type (might be due to invalid IL or missing references)
			//IL_0261: Unknown result type (might be due to invalid IL or missing references)
			//IL_0270: Unknown result type (might be due to invalid IL or missing references)
			//IL_027f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0289: Expected O, but got Unknown
			//IL_0295: Unknown result type (might be due to invalid IL or missing references)
			//IL_02a4: Unknown result type (might be due to invalid IL or missing references)
			//IL_02c1: Unknown result type (might be due to invalid IL or missing references)
			//IL_02cb: Expected O, but got Unknown
			//IL_02f4: Unknown result type (might be due to invalid IL or missing references)
			//IL_0303: Unknown result type (might be due to invalid IL or missing references)
			//IL_0312: Unknown result type (might be due to invalid IL or missing references)
			//IL_0321: Unknown result type (might be due to invalid IL or missing references)
			//