Decompiled source of Snitch v1.7.0

Snitch.dll

Decompiled 3 weeks ago
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Net.WebSockets;
using System.Reflection;
using System.Reflection.Emit;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Cryptography;
using System.Security.Permissions;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using HarmonyLib;
using Hash.Api;
using Hotline.Api;
using Il2CppScheduleOne;
using Il2CppScheduleOne.DevUtilities;
using Il2CppScheduleOne.GameTime;
using Il2CppScheduleOne.NPCs;
using Il2CppScheduleOne.Networking;
using Il2CppScheduleOne.Quests;
using Il2CppScheduleOne.Trash;
using Il2CppSystem.Collections.Generic;
using MelonLoader;
using MelonLoader.Preferences;
using Microsoft.CodeAnalysis;
using Snitch;
using Snitch.Ablation;
using Snitch.Bridge;
using Snitch.Compat;
using Snitch.Config;
using Snitch.Engine;
using Snitch.Logging;
using Snitch.Panels;
using Snitch.Providers;
using Snitch.Registries;
using Snitch.Reporting;
using Snitch.Sections;
using Snitch.Server;
using Snitch.UI;
using Snitch.Vanilla;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: MelonInfo(typeof(Core), "Snitch", "1.7.0", "DooDesch", "https://github.com/DooDesch-Mods/ScheduleOne-Snitch")]
[assembly: MelonGame("TVGS", "Schedule I")]
[assembly: MelonOptionalDependencies(new string[] { "Hotline" })]
[assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")]
[assembly: AssemblyCompany("DooDesch")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyCopyright("Copyright © DooDesch")]
[assembly: AssemblyFileVersion("1.7.0.0")]
[assembly: AssemblyInformationalVersion("1.7.0+dff04885b86c4ccbb98c65bf2f59955047d954a6")]
[assembly: AssemblyProduct("Snitch")]
[assembly: AssemblyTitle("Snitch")]
[assembly: NeutralResourcesLanguage("en-US")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.7.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.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[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 DooDesch
{
	internal static class ModVersion
	{
		internal const string Current = "1.7.0";
	}
}
namespace Hash.Api
{
	public static class HashCommands
	{
		private const string BridgeTypeName = "Hash.Bridge.HashBridge, Hash";

		private static readonly List<string[]> _pending = new List<string[]>();

		private static Action<string, string, string, string> _declare;

		private static bool _bound;

		public static bool Available
		{
			get
			{
				Bind();
				return _bound;
			}
		}

		private static string Owner
		{
			get
			{
				try
				{
					return Assembly.GetExecutingAssembly().GetName().Name ?? "";
				}
				catch
				{
					return "";
				}
			}
		}

		public static void Add(string word, string description, string example = null)
		{
			if (!string.IsNullOrEmpty(word))
			{
				Bind();
				if (_declare != null)
				{
					Safely(word, description, example);
					return;
				}
				_pending.Add(new string[3]
				{
					word,
					description ?? "",
					example ?? ""
				});
			}
		}

		private static void Safely(string word, string description, string example)
		{
			try
			{
				_declare(word, description ?? "", example ?? "", Owner);
			}
			catch
			{
			}
		}

		private static void Bind()
		{
			if (_bound)
			{
				return;
			}
			try
			{
				Type type = Type.GetType("Hash.Bridge.HashBridge, Hash", throwOnError: false);
				if (type == null)
				{
					return;
				}
				_declare = type.GetField("Declare", BindingFlags.Static | BindingFlags.Public)?.GetValue(null) as Action<string, string, string, string>;
				if (_declare == null)
				{
					return;
				}
				_bound = true;
				foreach (string[] item in _pending)
				{
					Safely(item[0], item[1], item[2]);
				}
				_pending.Clear();
			}
			catch
			{
			}
		}
	}
}
namespace Hotline.Api
{
	public static class Hud
	{
		private static bool _bound;

		private static bool _autoDone;

		private static int _probeAttempts;

		private static readonly List<Action> _pending = new List<Action>();

		private static Action<string, string> _registerPanel;

		private static Action<string, string, string, Action> _registerAction;

		private static Action<string, string, string, Func<bool>, Action<bool>> _registerToggle;

		private static Action<string, Func<string>> _registerText;

		private static Action<string, Func<int[]>> _registerImage;

		private static Action<string, string, string, double, double, double, string, Func<double>, Action<double>> _registerSlider;

		private static Action<string> _bindPanelLog;

		private static Action<string, int, string> _log;

		private static Action<string, string, int, Action> _registerHotkey;

		private static Action<bool> _showOverlay;

		private static Action<string, bool> _showPanel;

		private static Func<string, bool> _isPanelVisible;

		public static bool Available
		{
			get
			{
				EnsureBound();
				return _bound;
			}
		}

		public static Panel RegisterPanel(string id, string title = null)
		{
			Panel result = new Panel(id);
			if (string.IsNullOrEmpty(id))
			{
				return result;
			}
			string t = title;
			EnsureBound();
			if (_registerPanel != null)
			{
				_registerPanel(id, t);
			}
			else
			{
				_pending.Add(delegate
				{
					_registerPanel?.Invoke(id, t);
				});
			}
			return result;
		}

		public static void RegisterAction(string panelId, string label, Action run)
		{
			if (run == null || string.IsNullOrEmpty(label))
			{
				return;
			}
			string actionId = panelId + ":" + Slug(label);
			EnsureBound();
			if (_registerAction != null)
			{
				_registerAction(panelId, actionId, label, run);
				return;
			}
			_pending.Add(delegate
			{
				_registerAction?.Invoke(panelId, actionId, label, run);
			});
		}

		public static void RegisterToggle(string panelId, string label, Func<bool> get, Action<bool> set)
		{
			if (get == null || set == null || string.IsNullOrEmpty(label))
			{
				return;
			}
			string toggleId = panelId + ":" + Slug(label);
			EnsureBound();
			if (_registerToggle != null)
			{
				_registerToggle(panelId, toggleId, label, get, set);
				return;
			}
			_pending.Add(delegate
			{
				_registerToggle?.Invoke(panelId, toggleId, label, get, set);
			});
		}

		public static void RegisterSlider(string panelId, string label, double min, double max, Func<double> get, Action<double> set, double step = 0.0, string unit = null)
		{
			if (get == null || set == null || string.IsNullOrEmpty(label) || max <= min)
			{
				return;
			}
			string sliderId = panelId + ":" + Slug(label);
			string u = unit ?? "";
			EnsureBound();
			if (_registerSlider != null)
			{
				_registerSlider(panelId, sliderId, label, min, max, step, u, get, set);
				return;
			}
			_pending.Add(delegate
			{
				_registerSlider?.Invoke(panelId, sliderId, label, min, max, step, u, get, set);
			});
		}

		public static void RegisterText(string panelId, Func<string> provider)
		{
			if (provider == null)
			{
				return;
			}
			EnsureBound();
			if (_registerText != null)
			{
				_registerText(panelId, provider);
				return;
			}
			_pending.Add(delegate
			{
				_registerText?.Invoke(panelId, provider);
			});
		}

		public static void ShowOverlay(bool show)
		{
			EnsureBound();
			_showOverlay?.Invoke(show);
		}

		public static void ShowPanel(string panelId, bool show)
		{
			if (!string.IsNullOrEmpty(panelId))
			{
				EnsureBound();
				_showPanel?.Invoke(panelId, show);
			}
		}

		public static bool IsPanelVisible(string panelId)
		{
			if (string.IsNullOrEmpty(panelId))
			{
				return false;
			}
			EnsureBound();
			try
			{
				return _isPanelVisible != null && _isPanelVisible(panelId);
			}
			catch
			{
				return false;
			}
		}

		public static void RegisterImage(string panelId, Func<int[]> provider)
		{
			if (provider == null)
			{
				return;
			}
			EnsureBound();
			if (_registerImage != null)
			{
				_registerImage(panelId, provider);
				return;
			}
			_pending.Add(delegate
			{
				_registerImage?.Invoke(panelId, provider);
			});
		}

		public static void BindPanelLog(string panelId)
		{
			EnsureBound();
			if (_bindPanelLog != null)
			{
				_bindPanelLog(panelId);
				return;
			}
			_pending.Add(delegate
			{
				_bindPanelLog?.Invoke(panelId);
			});
		}

		public static void Log(string channel, string message, LogLevel level = LogLevel.Info)
		{
			if (string.IsNullOrEmpty(message))
			{
				return;
			}
			int lv = (int)level;
			EnsureBound();
			if (_log != null)
			{
				_log(channel, lv, message);
				return;
			}
			_pending.Add(delegate
			{
				_log?.Invoke(channel, lv, message);
			});
		}

		public static void RegisterHotkey(string ownerId, string label, HotlineKey key, Action run)
		{
			if (run == null || string.IsNullOrEmpty(label))
			{
				return;
			}
			int k = (int)key;
			EnsureBound();
			if (_registerHotkey != null)
			{
				_registerHotkey(ownerId, label, k, run);
				return;
			}
			_pending.Add(delegate
			{
				_registerHotkey?.Invoke(ownerId, label, k, run);
			});
		}

		public static void AutoRegister()
		{
			EnsureBound();
			if (_bound)
			{
				RunAutoRegister();
			}
		}

		private static void RunAutoRegister()
		{
			if (_autoDone)
			{
				return;
			}
			_autoDone = true;
			try
			{
				Assembly assembly = typeof(Hud).Assembly;
				((assembly.GetType("HotlineProbe", throwOnError: false) ?? FindByLeafName(assembly, "HotlineProbe"))?.GetMethod("Register", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null))?.Invoke(null, null);
			}
			catch
			{
			}
		}

		private static Type FindByLeafName(Assembly asm, string leaf)
		{
			Type[] types;
			try
			{
				types = asm.GetTypes();
			}
			catch (ReflectionTypeLoadException ex)
			{
				types = ex.Types;
			}
			catch
			{
				return null;
			}
			if (types == null)
			{
				return null;
			}
			Type[] array = types;
			foreach (Type type in array)
			{
				if (type != null && type.IsClass && type.IsAbstract && type.IsSealed && type.Name == leaf)
				{
					return type;
				}
			}
			return null;
		}

		private static string Slug(string s)
		{
			if (string.IsNullOrEmpty(s))
			{
				return "";
			}
			StringBuilder stringBuilder = new StringBuilder(s.Length);
			foreach (char c in s)
			{
				if (char.IsLetterOrDigit(c))
				{
					stringBuilder.Append(char.ToLowerInvariant(c));
				}
				else if (stringBuilder.Length > 0 && stringBuilder[stringBuilder.Length - 1] != '-')
				{
					stringBuilder.Append('-');
				}
			}
			return stringBuilder.ToString().Trim('-');
		}

		private static void EnsureBound()
		{
			if (_bound)
			{
				return;
			}
			try
			{
				Type type = FindBridge(_probeAttempts++ % 30 == 0);
				if (type == null || (type.GetField("AbiVersion", BindingFlags.Static | BindingFlags.Public)?.GetValue(null) is int num && num < 1))
				{
					return;
				}
				_registerPanel = Get<Action<string, string>>(type, "RegisterPanel");
				_registerAction = Get<Action<string, string, string, Action>>(type, "RegisterAction");
				_registerToggle = Get<Action<string, string, string, Func<bool>, Action<bool>>>(type, "RegisterToggle");
				_registerText = Get<Action<string, Func<string>>>(type, "RegisterText");
				_registerImage = Get<Action<string, Func<int[]>>>(type, "RegisterImage");
				_registerSlider = Get<Action<string, string, string, double, double, double, string, Func<double>, Action<double>>>(type, "RegisterSlider");
				_bindPanelLog = Get<Action<string>>(type, "BindPanelLog");
				_log = Get<Action<string, int, string>>(type, "Log");
				_registerHotkey = Get<Action<string, string, int, Action>>(type, "RegisterHotkey");
				_showOverlay = Get<Action<bool>>(type, "ShowOverlay");
				_showPanel = Get<Action<string, bool>>(type, "ShowPanel");
				_isPanelVisible = Get<Func<string, bool>>(type, "IsPanelVisible");
				if (_registerPanel == null)
				{
					return;
				}
				_bound = true;
				for (int i = 0; i < _pending.Count; i++)
				{
					try
					{
						_pending[i]();
					}
					catch
					{
					}
				}
				_pending.Clear();
				RunAutoRegister();
			}
			catch
			{
			}
		}

		private static T Get<T>(Type t, string field) where T : class
		{
			return t.GetField(field, BindingFlags.Static | BindingFlags.Public)?.GetValue(null) as T;
		}

		private static Type FindBridge(bool scan)
		{
			Type type = Type.GetType("Hotline.Bridge.HotlineBridge, Hotline", throwOnError: false);
			if (type != null || !scan)
			{
				return type;
			}
			Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
			foreach (Assembly assembly in assemblies)
			{
				try
				{
					type = assembly.GetType("Hotline.Bridge.HotlineBridge", throwOnError: false);
					if (type != null)
					{
						return type;
					}
				}
				catch
				{
				}
			}
			return null;
		}
	}
	public enum LogLevel
	{
		Info,
		Warning,
		Error
	}
	public enum HotlineKey
	{
		None = 0,
		F1 = 282,
		F2 = 283,
		F3 = 284,
		F4 = 285,
		F5 = 286,
		F6 = 287,
		F7 = 288,
		F8 = 289,
		F9 = 290,
		F10 = 291,
		F11 = 292,
		F12 = 293
	}
	public sealed class Panel
	{
		private readonly string _id;

		public string Id => _id;

		internal Panel(string id)
		{
			_id = id ?? "";
		}

		public Panel Text(Func<string> provider)
		{
			Hud.RegisterText(_id, provider);
			return this;
		}

		public Panel Image(Func<int[]> provider)
		{
			Hud.RegisterImage(_id, provider);
			return this;
		}

		public Panel Action(string label, Action run)
		{
			Hud.RegisterAction(_id, label, run);
			return this;
		}

		public Panel Toggle(string label, Func<bool> get, Action<bool> set)
		{
			Hud.RegisterToggle(_id, label, get, set);
			return this;
		}

		public Panel Slider(string label, double min, double max, Func<double> get, Action<double> set, double step = 0.0, string unit = null)
		{
			Hud.RegisterSlider(_id, label, min, max, get, set, step, unit);
			return this;
		}

		public Panel Hotkey(string label, HotlineKey key, Action run)
		{
			Hud.RegisterHotkey(_id, label, key, run);
			return this;
		}

		public Panel Log()
		{
			Hud.BindPanelLog(_id);
			return this;
		}

		public void Write(string message, LogLevel level = LogLevel.Info)
		{
			Hud.Log(_id, message, level);
		}
	}
}
namespace Snitch
{
	public sealed class Core : MelonMod
	{
		private bool _inWorld;

		public static Core Instance { get; private set; }

		public static Instance Log { get; private set; }

		internal static Harmony HarmonyInst { get; private set; }

		public override void OnInitializeMelon()
		{
			Instance = this;
			Log = ((MelonBase)this).LoggerInstance;
			HarmonyInst = ((MelonBase)this).HarmonyInstance;
			Preferences.Initialize();
			LogHub.Install();
			BridgeHost.Install();
			Hud.RegisterPanel("Snitch", "Snitch (Profiler)").Text(ProfilerHud.BuildOverview).Action("Start sampling", SnitchCore.Start)
				.Action("Stop sampling", SnitchCore.Stop)
				.Action("Reset", delegate
				{
					SnitchCore.Stop();
					SnitchCore.Start();
				})
				.Action("Open dashboard", OpenDashboard)
				.Toggle("Phone remote (scan the QR)", () => LanServer.Running, SetLanRemote)
				.Image(QrImage.Build);
			HashCommands.Add("snitch", "profiler: start, stop, top, unattributed, patches, states, report", "snitch start");
			try
			{
				((MelonBase)this).HarmonyInstance.PatchAll();
			}
			catch (Exception ex)
			{
				Log.Warning("Harmony patch failed: " + ex.Message);
			}
			if (Preferences.Enabled && Preferences.ServerEnabled)
			{
				SnitchServer.Start(Preferences.ServerPort, Preferences.ServerToken, Preferences.AllowedOrigins);
			}
			if (Preferences.Enabled && Preferences.ServerEnabled && Preferences.LanAccess)
			{
				LanServer.Start(Preferences.LanPort);
				RelayHost.Start(Guid.NewGuid().ToString("N").Substring(0, 12));
			}
			Instance log = Log;
			MelonAssembly melonAssembly = ((MelonBase)this).MelonAssembly;
			log.Msg("Snitch v" + (((melonAssembly == null) ? null : melonAssembly.Assembly?.GetName()?.Version?.ToString(3)) ?? "?") + " - profiler. Console: 'snitch start' to begin, 'snitch help' for commands.");
		}

		public override void OnSceneWasLoaded(int buildIndex, string sceneName)
		{
			_inWorld = sceneName == "Main";
			SnitchCore.LastScene = sceneName;
			if (_inWorld && Preferences.Enabled)
			{
				SnitchCore.RegisterBuiltins();
				AutoInstrument.DiscoverProbes();
				if (Preferences.AutoStart)
				{
					SnitchCore.Start();
				}
			}
		}

		public override void OnSceneWasUnloaded(int buildIndex, string sceneName)
		{
			_inWorld = false;
		}

		public override void OnUpdate()
		{
			SnitchServer.Pump();
			if (_inWorld && Preferences.Enabled)
			{
				SnitchCore.Tick();
			}
		}

		internal static string DashboardUrl()
		{
			if (SnitchServer.Running && WebAssets.HasBundledDashboard())
			{
				return "http://127.0.0.1:" + SnitchServer.Port + "/";
			}
			return "https://snitch.doodesch.de";
		}

		internal static void OpenDashboard()
		{
			if (!SnitchServer.Running)
			{
				Log.Warning("[snitch] the local data server is off - a dashboard would have nothing to connect to. Set ServerEnabled in MelonPreferences and restart.");
				return;
			}
			string text = DashboardUrl();
			try
			{
				Application.OpenURL(text);
				Log.Msg("[snitch] opened " + text + (text.StartsWith("http://127") ? " (bundled dashboard)" : " (hosted dashboard, connects back to loopback)"));
			}
			catch (Exception ex)
			{
				Log.Warning("[snitch] could not open a browser (" + ex.Message + "). Open this yourself: " + text);
			}
		}

		private static void SetLanRemote(bool on)
		{
			Preferences.LanAccess = on;
			try
			{
				MelonPreferences.Save();
			}
			catch
			{
			}
			if (on)
			{
				if (!LanServer.Running)
				{
					LanServer.Start(Preferences.LanPort);
				}
				if (!RelayHost.Running)
				{
					RelayHost.Start(Guid.NewGuid().ToString("N").Substring(0, 12));
				}
			}
			else
			{
				RelayHost.Stop();
				LanServer.Stop();
			}
		}

		public override void OnApplicationQuit()
		{
			SnitchServer.Stop();
			RelayHost.Stop();
			LanServer.Stop();
			LogHub.Uninstall();
		}

		public override void OnDeinitializeMelon()
		{
			SnitchServer.Stop();
			RelayHost.Stop();
			LanServer.Stop();
			LogHub.Uninstall();
		}
	}
	internal static class SnitchConsole
	{
		private static int _lastFrame = -1;

		private static string _lastSig = "";

		internal const string PanelId = "Snitch";

		internal static bool TryHandle(string raw)
		{
			if (string.IsNullOrWhiteSpace(raw))
			{
				return false;
			}
			return Dispatch(raw.Trim().Split(new char[2] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries));
		}

		internal static bool TryHandle(List<string> args)
		{
			if (args == null || args.Count == 0)
			{
				return false;
			}
			string[] array = new string[args.Count];
			for (int i = 0; i < args.Count; i++)
			{
				array[i] = args[i];
			}
			return Dispatch(array);
		}

		private static bool Dispatch(string[] p)
		{
			if (p.Length == 0 || !p[0].Equals("snitch", StringComparison.OrdinalIgnoreCase))
			{
				return false;
			}
			string text = string.Join(" ", p);
			int frameCount = Time.frameCount;
			if (frameCount == _lastFrame && text == _lastSig)
			{
				return true;
			}
			_lastFrame = frameCount;
			_lastSig = text;
			LogHub.Write("Console", 0, text);
			string text2 = ((p.Length > 1) ? p[1].ToLowerInvariant() : "status");
			try
			{
				switch (text2)
				{
				case "start":
					SnitchCore.Start();
					break;
				case "stop":
					SnitchCore.Stop();
					break;
				case "status":
					Status();
					break;
				case "frame":
					Frame();
					break;
				case "top":
				case "sections":
					Top(IntArg(p, 2, 8), text2 == "sections");
					break;
				case "states":
					States((p.Length > 2) ? p[2] : null);
					break;
				case "counters":
					Counters();
					break;
				case "unattr":
				case "unattributed":
					Unattributed();
					break;
				case "patches":
					PatchesCmd(p);
					break;
				case "panels":
					PanelsList();
					break;
				case "act":
					ActCmd(p);
					break;
				case "toggle":
					ToggleCmd(p);
					break;
				case "slider":
					SliderCmd(p);
					break;
				case "open":
					OverlayCmd(show: true, p);
					break;
				case "close":
					OverlayCmd(show: false, p);
					break;
				case "dashboard":
					Core.OpenDashboard();
					break;
				case "log":
					LogCmd(p);
					break;
				case "vanilla":
					Vanilla(p);
					break;
				case "lan":
					Lan(p);
					break;
				case "report":
					Report((p.Length > 2) ? p[2].ToLowerInvariant() : "all");
					break;
				case "ablate":
					Ablate(p);
					break;
				case "levers":
					Log("ablation levers: " + string.Join(", ", LeverRegistry.Names));
					break;
				case "help":
					Help();
					break;
				default:
					Log("unknown '" + text2 + "'. Try 'snitch help'.");
					break;
				}
			}
			catch (Exception ex)
			{
				Log("error: " + ex.Message);
			}
			return true;
		}

		private static void Help()
		{
			Log("commands: open [all] | close [all] | start | stop | status | frame | top [n] | sections | states [id] | counters | unattributed | patches [on|off|list|status] | panels | act <actionId> | toggle <toggleId> [on|off] | slider <sliderId> [value] | dashboard | log [<channel>|all] [n] | vanilla [on|off] | lan [on|off] | ablate <lever> | levers | report [md|csv|all]  ('open' shows the Snitch panel in the Hotline overlay; 'open all' shows the whole overlay)");
		}

		private static void OverlayCmd(bool show, string[] p)
		{
			bool flag = p.Length > 2 && p[2].ToLowerInvariant() == "all";
			if (!Hud.Available)
			{
				Log("the in-game overlay needs the Hotline mod - install it, or use 'snitch dashboard' for the web view.");
			}
			else if (flag)
			{
				Hud.ShowOverlay(show);
				Log("overlay " + (show ? "shown" : "hidden") + ".");
			}
			else
			{
				Hud.ShowPanel("Snitch", show);
				Log("Snitch panel " + (show ? "shown" : "hidden") + (show ? " (use 'snitch open all' for every panel)." : "."));
			}
		}

		private static void PanelsList()
		{
			IReadOnlyList<PanelModel> all = PanelRegistry.All;
			if (all.Count == 0)
			{
				Log("no mod panels registered yet (enter the world; panels register on probe discovery).");
				return;
			}
			Log($"{all.Count} panel(s) (toggle their windows in the Hotline overlay):");
			for (int i = 0; i < all.Count; i++)
			{
				PanelModel panelModel = all[i];
				Log($"  {panelModel.Id,-16} actions={panelModel.Actions.Count} toggles={panelModel.Toggles.Count} sliders={panelModel.Sliders.Count} title=\"{panelModel.Title}\"");
			}
		}

		private static void ActCmd(string[] p)
		{
			if (p.Length <= 2)
			{
				Log("usage: snitch act <actionId> (see the panel; ids look like 'Siesta:force-cosmetic').");
			}
			else
			{
				Log(PanelRegistry.Invoke(p[2]) ? ("ran " + p[2]) : ("no action '" + p[2] + "'"));
			}
		}

		private static void ToggleCmd(string[] p)
		{
			if (p.Length <= 2)
			{
				Log("usage: snitch toggle <toggleId> [on|off] (omit to flip).");
				return;
			}
			string text = p[2];
			bool value = BoolArg(p, 3, !PanelRegistry.GetToggle(text));
			Log(PanelRegistry.SetToggle(text, value) ? $"{text} = {value}" : ("no toggle '" + text + "'"));
		}

		private static void SliderCmd(string[] p)
		{
			if (p.Length <= 2)
			{
				Log("usage: snitch slider <sliderId> [value] (omit the value to read it). 'snitch panels' lists panels.");
				return;
			}
			string text = p[2];
			SliderItem slider = PanelRegistry.GetSlider(text);
			double result;
			if (slider == null)
			{
				Log("no slider '" + text + "'");
			}
			else if (p.Length <= 3)
			{
				Log($"{text} = {slider.Read():0.###} {slider.Unit} (range {slider.Min:0.###}..{slider.Max:0.###}, step {slider.Step:0.###})".Replace("  ", " ").TrimEnd());
			}
			else if (!double.TryParse(p[3], NumberStyles.Float, CultureInfo.InvariantCulture, out result))
			{
				Log("not a number: " + p[3]);
			}
			else
			{
				PanelRegistry.SetSlider(text, result);
				Log($"{text} = {slider.Read():0.###} {slider.Unit}".TrimEnd());
			}
		}

		private static void LogCmd(string[] p)
		{
			string text = ((p.Length > 2) ? p[2] : "all");
			int n = IntArg(p, 3, 25);
			List<LogEntry> list = (text.Equals("all", StringComparison.OrdinalIgnoreCase) ? LogHub.Timeline(n) : LogHub.Channel(text, n));
			if (list.Count == 0)
			{
				Log($"log '{text}': no entries (channels: {string.Join(", ", LogHub.Channels())}).");
				return;
			}
			Log($"log '{text}' (last {list.Count}):");
			foreach (LogEntry item in list)
			{
				string value = ((item.Lvl == 2) ? "E" : ((item.Lvl == 1) ? "W" : "I"));
				Log($"  {item.Time} {value} [{item.Ch}] {item.Msg}");
			}
		}

		private static void Status()
		{
			FrameStats latestFrame = SnitchCore.LatestFrame;
			Log($"active={SnitchCore.Active} fps={latestFrame.MeanFps:F0} (min {latestFrame.MinFps:F0}) frame={latestFrame.MeanMs:F2}ms p95={latestFrame.P95Ms:F2}ms sections={SectionProfiler.LabelCount} states={StateRegistry.Count} counters={CounterRegistry.Count} poll={Preferences.PollHz:F0}Hz");
			AttributionStats latestAttribution = SnitchCore.LatestAttribution;
			if (latestAttribution.Samples > 0)
			{
				Log($"unattributed={latestAttribution.UnattributedMeanMs:F2}ms/f ({latestAttribution.UnattributedPct:F0}% of the frame) badFrames={latestAttribution.SpikeFrames} patchTiming={(PatchInstrument.Enabled ? "on" : "off")} " + "('snitch unattributed' explains)");
			}
			if (!SnitchCore.Active)
			{
				Log("(idle - run 'snitch start' to begin sampling)");
			}
		}

		private static void Frame()
		{
			FrameStats latestFrame = SnitchCore.LatestFrame;
			Log($"frame: mean={latestFrame.MeanMs:F2}ms median={latestFrame.MedianMs:F2} p95={latestFrame.P95Ms:F2} p99={latestFrame.P99Ms:F2} min={latestFrame.MinMs:F2} max={latestFrame.MaxMs:F2} | fps mean={latestFrame.MeanFps:F0} min={latestFrame.MinFps:F0} | gc0/1000f={latestFrame.Gc0Per1000:F1} gc1/1000f={latestFrame.Gc1Per1000:F1} samples={latestFrame.Samples}");
			AttributionStats latestAttribution = SnitchCore.LatestAttribution;
			if (latestAttribution.Samples != 0)
			{
				Log($"attribution: sections={latestAttribution.AttributedMeanMs:F2}ms/f unattributed={latestAttribution.UnattributedMeanMs:F2}ms/f ({latestAttribution.UnattributedPct:F0}% of the frame, worst {latestAttribution.MaxUnattributedMs:F2})" + ((latestAttribution.SpikeFrames > 0) ? $" | {latestAttribution.SpikeFrames} bad frame(s), {latestAttribution.SpikeUnexplainedPct:F0}% of their excess unexplained" : " | no bad frames in the window"));
			}
		}

		private static void Top(int n, bool all)
		{
			List<SectionRow> latestSections = SnitchCore.LatestSections;
			if (latestSections == null || latestSections.Count == 0)
			{
				Log("sections: none yet (sample a frame; modder/vanilla sections appear once registered).");
				return;
			}
			int num = (all ? latestSections.Count : Math.Min(n, latestSections.Count));
			Log($"sections (top {num} of {latestSections.Count} by ms/frame):");
			for (int i = 0; i < num; i++)
			{
				SectionRow sectionRow = latestSections[i];
				if (sectionRow.Label == "(unattributed)")
				{
					Log($"  {sectionRow.Label,-28} {sectionRow.MsPerFrame,7:F3} ms/f  {sectionRow.PctFrame,5:F1}%  inside no section  (max {sectionRow.MaxMs:F3})" + "  <- 'snitch unattributed'");
				}
				else
				{
					Log($"  {sectionRow.Label,-28} {sectionRow.MsPerFrame,7:F3} ms/f  {sectionRow.PctFrame,5:F1}%  {sectionRow.Calls,6:F0} calls/f  (max {sectionRow.MaxMs:F3})");
				}
			}
		}

		private static void States(string filter)
		{
			List<StateSnapshot> latestStates = SnitchCore.LatestStates;
			if (latestStates == null || latestStates.Count == 0)
			{
				Log("states: none yet (start sampling first).");
				return;
			}
			foreach (StateSnapshot item in latestStates)
			{
				if (filter != null && item.Title.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0)
				{
					continue;
				}
				StringBuilder stringBuilder = new StringBuilder();
				stringBuilder.Append("  ").Append(item.Title).Append(" (total ")
					.Append(item.EffectiveTotal())
					.Append("): ");
				for (int i = 0; i < item.Buckets.Count; i++)
				{
					if (i > 0)
					{
						stringBuilder.Append("  ");
					}
					stringBuilder.Append(item.Buckets[i].Name).Append('=').Append(item.Buckets[i].Count);
				}
				Log(stringBuilder.ToString());
			}
		}

		private static void Vanilla(string[] p)
		{
			string text = ((p.Length > 2) ? p[2].ToLowerInvariant() : "status");
			if (text == "on")
			{
				VanillaProbes.Enable();
			}
			else if (text == "off")
			{
				VanillaProbes.Disable();
			}
			else
			{
				Log("vanilla probes: " + VanillaProbes.Status() + " (use 'snitch vanilla on|off')");
			}
		}

		private static void Lan(string[] p)
		{
			string text = ((p.Length > 2) ? p[2].ToLowerInvariant() : "status");
			if (text == "on")
			{
				if (LanServer.Running)
				{
					Log("phone remote already on - " + LanUrl());
					return;
				}
				Preferences.LanAccess = true;
				SavePreferences();
				LanServer.Start(Preferences.LanPort);
				if (!RelayHost.Running)
				{
					RelayHost.Start(Guid.NewGuid().ToString("N").Substring(0, 12));
				}
				Log(LanServer.Running ? ("phone remote ON - " + LanUrl() + " (+ relay for other networks; scan the QR from your phone)") : "phone remote failed to start - see the log (port in use? change LanPort).");
			}
			else if (text == "off")
			{
				Preferences.LanAccess = false;
				SavePreferences();
				RelayHost.Stop();
				LanServer.Stop();
				Log("phone remote OFF.");
			}
			else
			{
				Log(LanServer.Running ? ("LAN remote ON - " + LanUrl()) : "LAN remote OFF (use 'snitch lan on'). Lets a phone on your Wi-Fi open the dashboard as a remote.");
			}
		}

		private static string LanUrl()
		{
			return $"http://{LanServer.Ip}:{LanServer.Port}/ (token {LanServer.Token})";
		}

		private static void Report(string fmt)
		{
			if (fmt != "md" && fmt != "csv" && fmt != "all")
			{
				fmt = "all";
			}
			try
			{
				string text = ReportWriter.Write(fmt);
				Log("report written: " + text);
			}
			catch (Exception ex)
			{
				Log("report failed: " + ex.Message);
			}
		}

		private static void Ablate(string[] p)
		{
			if (p.Length <= 2)
			{
				Log("usage: snitch ablate <lever>. levers: " + string.Join(", ", LeverRegistry.Names));
			}
			else
			{
				AblationEngine.Start(p[2].ToLowerInvariant());
			}
		}

		private static void Unattributed()
		{
			AttributionStats latestAttribution = SnitchCore.LatestAttribution;
			if (latestAttribution.Samples == 0)
			{
				Log("unattributed: nothing measured yet" + (SnitchCore.Active ? " (give it a second)." : " - run 'snitch start' first."));
				return;
			}
			Log($"unattributed: {latestAttribution.UnattributedMeanMs:F2} ms/frame of a {latestAttribution.FrameMeanMs:F2} ms frame ({latestAttribution.UnattributedPct:F0}%) - inside no section at all.");
			Log($"  sections account for {latestAttribution.AttributedMeanMs:F2} ms/frame; the worst single frame left {latestAttribution.MaxUnattributedMs:F2} ms unexplained.");
			if (latestAttribution.SpikeFrames == 0)
			{
				Log($"  bad frames: none in the last {latestAttribution.Samples} (nothing above {latestAttribution.SpikeFactor:F1}x the {latestAttribution.FrameMedianMs:F2} ms median).");
			}
			else
			{
				Log($"  bad frames: {latestAttribution.SpikeFrames} of {latestAttribution.Samples} above {latestAttribution.SpikeFactor:F1}x the {latestAttribution.FrameMedianMs:F2} ms median; on those, {latestAttribution.SpikeMeanUnexplainedMs:F2} ms of the {latestAttribution.SpikeMeanExcessMs:F2} ms excess ({latestAttribution.SpikeUnexplainedPct:F0}%) is unexplained (worst {latestAttribution.WorstUnexplainedMs:F2} ms).");
			}
			Log("  most of an unattributed frame is the game itself and always will be; the number that points at a mod is the unexplained share of the bad frames.");
			Log("  " + NextStep(latestAttribution));
		}

		private static string NextStep(AttributionStats a)
		{
			if (!a.PointsAtHiddenWork)
			{
				return "the bad frames are explained by sections that got worse, so 'snitch top' has the answer.";
			}
			if (!PatchInstrument.Enabled)
			{
				return "-> run 'snitch patches on'. A mod's per-frame work does not have to live in OnUpdate: a Harmony postfix on a vanilla method the engine calls every frame belongs to no section, so the mod reads as cheap.";
			}
			return $"-> patch timing is already on ({PatchInstrument.WrappedCount} patches wrapped) and still nothing " + "accounts for it. What is left: a coroutine, an event handler, a Unity message on a MonoBehaviour a mod added, or the game itself - 'snitch vanilla on' and 'snitch ablate' cover the last two.";
		}

		private static void PatchesCmd(string[] p)
		{
			switch ((p.Length > 2) ? p[2].ToLowerInvariant() : "status")
			{
			case "on":
				PatchInstrument.Enable();
				Log(PatchInstrument.Status());
				Log("  numbers include the wrapper's own overhead, the same as the vanilla probes. Run 'snitch patches on' again after a late-loading mod has patched.");
				ReportSkipsAndFailures();
				return;
			case "off":
				PatchInstrument.Disable();
				Log("patch timing OFF (the wrappers stay installed but dormant).");
				return;
			case "list":
				PatchesList();
				return;
			}
			Log("patch timing: " + PatchInstrument.Status());
			if (!PatchInstrument.EverScanned)
			{
				Log("  nothing scanned yet - 'snitch patches on' wraps every loaded mod's Harmony prefixes, postfixes and finalizers.");
			}
			ReportSkipsAndFailures();
		}

		private static void ReportSkipsAndFailures()
		{
			StringBuilder stringBuilder = new StringBuilder();
			foreach (KeyValuePair<string, int> skippedOwner in PatchInstrument.SkippedOwners)
			{
				if (stringBuilder.Length > 0)
				{
					stringBuilder.Append(", ");
				}
				stringBuilder.Append(skippedOwner.Key).Append(' ').Append(skippedOwner.Value);
			}
			if (stringBuilder.Length > 0)
			{
				Log("  not a mod's patch, left alone: " + stringBuilder);
			}
			int failureCount = PatchInstrument.FailureCount;
			if (failureCount == 0)
			{
				return;
			}
			Log($"  {failureCount} wrap(s) FAILED - those methods may now be unusable for later patchers:");
			foreach (string failure in PatchInstrument.Failures)
			{
				Log("    " + failure);
			}
		}

		private static void PatchesList()
		{
			List<WrappedPatch> list = PatchInstrument.All();
			if (list.Count == 0)
			{
				Log("no patches wrapped. 'snitch patches on' wraps every loaded mod's Harmony prefixes, postfixes and finalizers.");
				return;
			}
			Dictionary<string, SectionRow> costByLabel = new Dictionary<string, SectionRow>(StringComparer.Ordinal);
			List<SectionRow> latestSections = SnitchCore.LatestSections;
			if (latestSections != null)
			{
				for (int i = 0; i < latestSections.Count; i++)
				{
					costByLabel[latestSections[i].Label] = latestSections[i];
				}
			}
			list.Sort((WrappedPatch x, WrappedPatch y) => Cost(costByLabel, y).CompareTo(Cost(costByLabel, x)));
			Log($"{list.Count} wrapped patch(es), worst first:");
			for (int num = 0; num < list.Count; num++)
			{
				WrappedPatch wrappedPatch = list[num];
				costByLabel.TryGetValue(wrappedPatch.Label, out var value);
				string text = string.Join(", ", wrappedPatch.Targets);
				if (wrappedPatch.TargetCount > wrappedPatch.Targets.Count)
				{
					text += $", +{wrappedPatch.TargetCount - wrappedPatch.Targets.Count} more";
				}
				Log($"  {wrappedPatch.Label,-38} {value.MsPerFrame,7:F3} ms/f  {value.Calls,7:F0} calls/f  (max {value.MaxMs:F3})  [{string.Join("+", wrappedPatch.Kinds)}] on {text}");
			}
			if (!PatchInstrument.Enabled)
			{
				Log("  timing is OFF, so those numbers are stale - 'snitch patches on' arms them.");
			}
			Log("  a zero is not proof of free: 0.000 ms with calls is below the printed resolution, and 0 calls on a patch you know is hot means it was not measured (the JIT can inline a small patch method past the wrapper).");
		}

		private static double Cost(Dictionary<string, SectionRow> byLabel, WrappedPatch w)
		{
			if (!byLabel.TryGetValue(w.Label, out var value))
			{
				return 0.0;
			}
			return value.MsPerFrame;
		}

		private static void Counters()
		{
			List<CounterRow> latestCounters = SnitchCore.LatestCounters;
			if (latestCounters == null || latestCounters.Count == 0)
			{
				Log("counters: none registered.");
				return;
			}
			foreach (CounterRow item in latestCounters)
			{
				Log($"  {item.Id,-28} {item.Value,12:F2} {item.Unit} [{item.State}]");
			}
		}

		private static int IntArg(string[] p, int idx, int def)
		{
			if (p.Length > idx && int.TryParse(p[idx], out var result))
			{
				return result;
			}
			return def;
		}

		private static float FloatArg(string[] p, int idx, float def)
		{
			if (p.Length > idx && float.TryParse(p[idx], NumberStyles.Float, CultureInfo.InvariantCulture, out var result))
			{
				return result;
			}
			return def;
		}

		private static bool BoolArg(string[] p, int idx, bool toggleDefault)
		{
			if (p.Length <= idx)
			{
				return toggleDefault;
			}
			switch (p[idx].ToLowerInvariant())
			{
			case "on":
			case "true":
			case "1":
			case "yes":
				return true;
			case "off":
			case "false":
			case "0":
			case "no":
				return false;
			default:
				return toggleDefault;
			}
		}

		private static void SavePreferences()
		{
			try
			{
				MelonPreferences.Save();
			}
			catch (Exception ex)
			{
				Instance log = Core.Log;
				if (log != null)
				{
					log.Warning("[snitch] could not write MelonPreferences (the setting is live for this session only): " + ex.Message);
				}
			}
		}

		internal static void Log(string msg)
		{
			Instance log = Core.Log;
			if (log != null)
			{
				log.Msg("[snitch] " + msg);
			}
			LogHub.Write("Snitch", 0, msg);
		}
	}
	[HarmonyPatch(typeof(Console), "SubmitCommand", new Type[] { typeof(string) })]
	internal static class Snitch_Console_SubmitCommand_String_Patch
	{
		private static bool Prefix(string args)
		{
			try
			{
				return !SnitchConsole.TryHandle(args);
			}
			catch (Exception ex)
			{
				Instance log = Core.Log;
				if (log != null)
				{
					log.Error("[snitch] console prefix (string) threw, handing the command to the game: " + ex);
				}
				return true;
			}
		}
	}
	[HarmonyPatch(typeof(Console), "SubmitCommand", new Type[] { typeof(List<string>) })]
	internal static class Snitch_Console_SubmitCommand_List_Patch
	{
		private static bool Prefix(List<string> args)
		{
			try
			{
				return !SnitchConsole.TryHandle(args);
			}
			catch (Exception ex)
			{
				Instance log = Core.Log;
				if (log != null)
				{
					log.Error("[snitch] console prefix (list) threw, handing the command to the game: " + ex);
				}
				return true;
			}
		}
	}
}
namespace Snitch.Vanilla
{
	internal static class AutoInstrument
	{
		internal static volatile bool Enabled;

		private static bool _patched;

		private static readonly Dictionary<MethodBase, int> _ids = new Dictionary<MethodBase, int>();

		private static readonly string[] Lifecycle = new string[4] { "OnUpdate", "OnFixedUpdate", "OnLateUpdate", "OnGUI" };

		private static bool _discovered;

		internal static int InstrumentedCount => _ids.Count;

		internal static void Enable()
		{
			EnsurePatched();
			Enabled = true;
		}

		internal static void Disable()
		{
			Enabled = false;
		}

		internal static void DiscoverProbes()
		{
			if (_discovered)
			{
				return;
			}
			_discovered = true;
			int num = 0;
			try
			{
				IEnumerable<MelonMod> enumerable = RegisteredMods();
				if (enumerable == null)
				{
					return;
				}
				foreach (MelonMod item in enumerable)
				{
					if (item == null || (object)item == Core.Instance)
					{
						continue;
					}
					try
					{
						Assembly assembly = ((object)item).GetType().Assembly;
						MethodInfo methodInfo = (assembly.GetType("SnitchProbe", throwOnError: false) ?? FindLeaf(assembly, "SnitchProbe"))?.GetMethod("Register", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
						if (!(methodInfo == null))
						{
							methodInfo.Invoke(null, null);
							num++;
						}
					}
					catch (Exception ex)
					{
						Instance log = Core.Log;
						if (log != null)
						{
							log.Warning("[snitch] probe discovery on " + ModName(item) + " failed: " + ex.Message);
						}
					}
				}
				if (num > 0)
				{
					Instance log2 = Core.Log;
					if (log2 != null)
					{
						log2.Msg($"[snitch] discovered + registered {num} mod probe(s).");
					}
				}
			}
			catch (Exception ex2)
			{
				Instance log3 = Core.Log;
				if (log3 != null)
				{
					log3.Warning("[snitch] probe discovery failed: " + ex2.Message);
				}
			}
		}

		private static Type FindLeaf(Assembly asm, string leaf)
		{
			Type[] types;
			try
			{
				types = asm.GetTypes();
			}
			catch (ReflectionTypeLoadException ex)
			{
				types = ex.Types;
				Instance log = Core.Log;
				if (log != null)
				{
					log.Warning("[snitch] probe discovery: " + asm.GetName().Name + " loaded only part of its types - " + ex.Message);
				}
			}
			catch (Exception ex2)
			{
				Instance log2 = Core.Log;
				if (log2 != null)
				{
					log2.Warning("[snitch] probe discovery: " + asm.GetName().Name + " would not enumerate its types - " + ex2.Message);
				}
				return null;
			}
			if (types == null)
			{
				Instance log3 = Core.Log;
				if (log3 != null)
				{
					log3.Warning("[snitch] probe discovery: " + asm.GetName().Name + " returned no type list; skipping it.");
				}
				return null;
			}
			Type[] array = types;
			foreach (Type type in array)
			{
				if (type != null && type.Name == leaf)
				{
					return type;
				}
			}
			return null;
		}

		private static void EnsurePatched()
		{
			//IL_0108: Unknown result type (might be due to invalid IL or missing references)
			//IL_0110: Unknown result type (might be due to invalid IL or missing references)
			//IL_011b: Expected O, but got Unknown
			//IL_011b: Expected O, but got Unknown
			if (_patched)
			{
				return;
			}
			_patched = true;
			try
			{
				IEnumerable<MelonMod> enumerable = RegisteredMods();
				if (enumerable == null)
				{
					Instance log = Core.Log;
					if (log != null)
					{
						log.Warning("[snitch] auto-instrument: could not enumerate mods.");
					}
					return;
				}
				MethodInfo methodInfo = AccessTools.Method(typeof(AutoInstrument), "Pre", (Type[])null, (Type[])null);
				MethodInfo methodInfo2 = AccessTools.Method(typeof(AutoInstrument), "Fin", (Type[])null, (Type[])null);
				foreach (MelonMod item in enumerable)
				{
					if (item == null || (object)item == Core.Instance)
					{
						continue;
					}
					string text = ModName(item);
					Type type = ((object)item).GetType();
					for (int i = 0; i < Lifecycle.Length; i++)
					{
						MethodInfo method = type.GetMethod(Lifecycle[i], BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
						if (method == null || method.IsAbstract || method.DeclaringType == typeof(MelonMod) || _ids.ContainsKey(method))
						{
							continue;
						}
						try
						{
							Core.HarmonyInst.Patch((MethodBase)method, new HarmonyMethod(methodInfo), (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(methodInfo2), (HarmonyMethod)null);
							_ids[method] = SectionProfiler.GetId(text + "." + Lifecycle[i]);
						}
						catch (Exception ex)
						{
							Instance log2 = Core.Log;
							if (log2 != null)
							{
								log2.Warning($"[snitch] auto-instrument {text}.{Lifecycle[i]} failed: {ex.Message}");
							}
						}
					}
				}
				Instance log3 = Core.Log;
				if (log3 != null)
				{
					log3.Msg($"[snitch] auto-instrumented {_ids.Count} mod lifecycle method(s) across other mods.");
				}
			}
			catch (Exception ex2)
			{
				Instance log4 = Core.Log;
				if (log4 != null)
				{
					log4.Warning("[snitch] auto-instrument failed: " + ex2.Message);
				}
			}
		}

		private static void Pre(MethodBase __originalMethod)
		{
			if (Enabled && _ids.TryGetValue(__originalMethod, out var value))
			{
				SectionProfiler.Begin(value);
			}
		}

		private static void Fin(MethodBase __originalMethod)
		{
			if (Enabled && _ids.TryGetValue(__originalMethod, out var value))
			{
				SectionProfiler.End(value);
			}
		}

		private static IEnumerable<MelonMod> RegisteredMods()
		{
			try
			{
				return MelonTypeBase<MelonMod>.RegisteredMelons;
			}
			catch (Exception ex)
			{
				Instance log = Core.Log;
				if (log != null)
				{
					log.Warning("[snitch] MelonMod.RegisteredMelons is unreadable, falling back to reflection - " + ex.Message);
				}
			}
			Type[] array = new Type[2]
			{
				typeof(MelonMod),
				typeof(MelonBase)
			};
			foreach (Type type in array)
			{
				try
				{
					if (type.GetProperty("RegisteredMelons", BindingFlags.Static | BindingFlags.Public)?.GetValue(null) is IEnumerable<MelonMod> result)
					{
						return result;
					}
					Instance log2 = Core.Log;
					if (log2 != null)
					{
						log2.Msg("[snitch] " + type.Name + ".RegisteredMelons is not a mod list here; trying the next candidate.");
					}
				}
				catch (Exception ex2)
				{
					Instance log3 = Core.Log;
					if (log3 != null)
					{
						log3.Warning("[snitch] reflecting " + type.Name + ".RegisteredMelons failed - " + ex2.Message);
					}
				}
			}
			Instance log4 = Core.Log;
			if (log4 != null)
			{
				log4.Error("[snitch] no route to the loaded mod list; auto-instrumentation and probe discovery are off.");
			}
			return null;
		}

		private static string ModName(MelonMod mod)
		{
			string text = null;
			try
			{
				MelonInfoAttribute info = ((MelonBase)mod).Info;
				text = ((info != null) ? info.Name : null);
			}
			catch (Exception ex)
			{
				Instance log = Core.Log;
				if (log != null)
				{
					log.Warning("[snitch] auto-instrument: a mod would not give its name - " + ex.Message);
				}
			}
			if (string.IsNullOrEmpty(text))
			{
				text = ((object)mod).GetType().Namespace ?? ((object)mod).GetType().Name;
			}
			return GroupName(text);
		}

		internal static string GroupName(string rawName)
		{
			if (string.IsNullOrEmpty(rawName))
			{
				return "(unnamed)";
			}
			StringBuilder stringBuilder = new StringBuilder(rawName.Length);
			for (int i = 0; i < rawName.Length; i++)
			{
				char c = rawName[i];
				if (c == '\u001b')
				{
					for (; i < rawName.Length && rawName[i] != 'm'; i++)
					{
					}
				}
				else if (c >= ' ')
				{
					stringBuilder.Append((c == '.' || c == ' ') ? '_' : c);
				}
			}
			string text = stringBuilder.ToString();
			if (text.Length <= 0)
			{
				return "(unnamed)";
			}
			return text;
		}
	}
	internal sealed class WrappedPatch
	{
		internal MethodInfo Method;

		internal string Label;

		internal int SectionId;

		internal string Owner;

		internal readonly List<string> Kinds = new List<string>(2);

		internal readonly List<string> Targets = new List<string>(1);

		internal int TargetCount;
	}
	internal static class PatchInstrument
	{
		internal static volatile bool Enabled;

		private static volatile Dictionary<MethodBase, int> _ids = new Dictionary<MethodBase, int>();

		private static readonly Dictionary<MethodBase, WrappedPatch> _wrapped = new Dictionary<MethodBase, WrappedPatch>();

		private static readonly SortedDictionary<string, int> _skips = new SortedDictionary<string, int>(StringComparer.Ordinal);

		private static readonly SortedDictionary<string, int> _skippedOwners = new SortedDictionary<string, int>(StringComparer.Ordinal);

		private static readonly List<string> _failures = new List<string>();

		private static int _scans;

		private const int MaxListedTargets = 8;

		private static readonly Assembly SelfAssembly = typeof(PatchInstrument).Assembly;

		internal static bool Requested { get; private set; }

		internal static int WrappedCount => _wrapped.Count;

		internal static int FailureCount => _failures.Count;

		internal static bool EverScanned => _scans > 0;

		internal static IEnumerable<string> Failures => _failures;

		internal static IEnumerable<KeyValuePair<string, int>> SkippedOwners => _skippedOwners;

		internal static void Enable()
		{
			int value = Scan();
			Enabled = true;
			Requested = true;
			Instance log = Core.Log;
			if (log != null)
			{
				log.Msg($"[snitch] patch timing ON. {value} newly wrapped, {_wrapped.Count} wrapped in total. " + Status());
			}
		}

		internal static void Disable()
		{
			Enabled = false;
			Requested = false;
			Instance log = Core.Log;
			if (log != null)
			{
				log.Msg("[snitch] patch timing OFF (the wrappers stay installed but dormant - one bool read).");
			}
		}

		internal static void Suspend()
		{
			Enabled = false;
		}

		internal static string Status()
		{
			StringBuilder stringBuilder = new StringBuilder(160);
			stringBuilder.Append("enabled=").Append(Enabled).Append(" wrapped=")
				.Append(_wrapped.Count)
				.Append(" scans=")
				.Append(_scans)
				.Append(" failed=")
				.Append(_failures.Count);
			if (_skips.Count > 0)
			{
				stringBuilder.Append(" skipped=[");
				bool flag = true;
				foreach (KeyValuePair<string, int> skip in _skips)
				{
					if (!flag)
					{
						stringBuilder.Append(", ");
					}
					flag = false;
					stringBuilder.Append(skip.Key).Append(' ').Append(skip.Value);
				}
				stringBuilder.Append(']');
			}
			return stringBuilder.ToString();
		}

		internal static List<WrappedPatch> All()
		{
			List<WrappedPatch> list = new List<WrappedPatch>(_wrapped.Values);
			list.Sort((WrappedPatch x, WrappedPatch y) => string.CompareOrdinal(x.Label, y.Label));
			return list;
		}

		internal static int Scan()
		{
			_scans++;
			_skips.Clear();
			_skippedOwners.Clear();
			List<MethodBase> list;
			try
			{
				list = new List<MethodBase>(Harmony.GetAllPatchedMethods());
			}
			catch (Exception ex)
			{
				Instance log = Core.Log;
				if (log != null)
				{
					log.Error("[snitch] patch scan aborted (guard: Harmony.GetAllPatchedMethods threw): " + ex);
				}
				return 0;
			}
			MethodInfo methodInfo = AccessTools.Method(typeof(PatchInstrument), "Pre", (Type[])null, (Type[])null);
			MethodInfo methodInfo2 = AccessTools.Method(typeof(PatchInstrument), "Fin", (Type[])null, (Type[])null);
			if (methodInfo == null || methodInfo2 == null)
			{
				Instance log2 = Core.Log;
				if (log2 != null)
				{
					log2.Error("[snitch] patch scan aborted (guard: Snitch's own Pre/Fin did not resolve through AccessTools).");
				}
				return 0;
			}
			Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.Ordinal);
			Dictionary<string, string> dictionary2 = new Dictionary<string, string>(StringComparer.Ordinal);
			BuildOwnerIndex(dictionary, dictionary2);
			if (dictionary.Count == 0 && dictionary2.Count == 0)
			{
				Instance log3 = Core.Log;
				if (log3 != null)
				{
					log3.Error("[snitch] patch scan aborted (guard: no loaded Melon could be indexed, so no patch could be attributed to a mod).");
				}
				return 0;
			}
			Dictionary<MethodBase, WrappedPatch> pending = new Dictionary<MethodBase, WrappedPatch>();
			for (int i = 0; i < list.Count; i++)
			{
				MethodBase methodBase = list[i];
				if (methodBase == null)
				{
					Skip("target-null", 1);
					continue;
				}
				Patches patchInfo;
				try
				{
					patchInfo = Harmony.GetPatchInfo(methodBase);
				}
				catch (Exception ex2)
				{
					Skip("patch-info-threw", 1);
					Instance log4 = Core.Log;
					if (log4 != null)
					{
						log4.Warning("[snitch] patch scan: GetPatchInfo(" + Describe(methodBase) + ") threw - " + ex2.Message);
					}
					continue;
				}
				if (patchInfo == null)
				{
					Skip("no-patch-info", 1);
					continue;
				}
				Collect(patchInfo.Prefixes, "prefix", methodBase, dictionary, dictionary2, pending);
				Collect(patchInfo.Postfixes, "postfix", methodBase, dictionary, dictionary2, pending);
				Collect(patchInfo.Finalizers, "finalizer", methodBase, dictionary, dictionary2, pending);
				if (patchInfo.Transpilers.Count > 0)
				{
					Skip("transpiler(runs once, not per call)", patchInfo.Transpilers.Count);
				}
				if (patchInfo.ILManipulators.Count > 0)
				{
					Skip("ilmanipulator(runs once, not per call)", patchInfo.ILManipulators.Count);
				}
			}
			return Install(pending, methodInfo, methodInfo2);
		}

		private static int Install(Dictionary<MethodBase, WrappedPatch> pending, MethodInfo pre, MethodInfo fin)
		{
			//IL_008e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0096: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a1: Expected O, but got Unknown
			//IL_00a1: Expected O, but got Unknown
			if (pending.Count == 0)
			{
				return 0;
			}
			Dictionary<MethodBase, int> dictionary = new Dictionary<MethodBase, int>(_ids);
			foreach (WrappedPatch value in pending.Values)
			{
				dictionary[value.Method] = value.SectionId;
			}
			_ids = dictionary;
			int num = 0;
			List<WrappedPatch> list = null;
			foreach (WrappedPatch value2 in pending.Values)
			{
				try
				{
					Core.HarmonyInst.Patch((MethodBase)value2.Method, new HarmonyMethod(pre), (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(fin), (HarmonyMethod)null);
					_wrapped[value2.Method] = value2;
					num++;
				}
				catch (Exception ex)
				{
					string text = value2.Label + " (" + Describe(value2.Method) + "): " + ex.Message;
					_failures.Add(text);
					if (list == null)
					{
						list = new List<WrappedPatch>();
					}
					list.Add(value2);
					Instance log = Core.Log;
					if (log != null)
					{
						log.Error("[snitch] could not wrap patch " + text);
					}
				}
			}
			if (list != null)
			{
				Dictionary<MethodBase, int> dictionary2 = new Dictionary<MethodBase, int>(_ids);
				for (int i = 0; i < list.Count; i++)
				{
					dictionary2.Remove(list[i].Method);
				}
				_ids = dictionary2;
			}
			return num;
		}

		private static void Collect(IList<Patch> patches, string kind, MethodBase target, Dictionary<string, string> byHarmonyId, Dictionary<string, string> byAssembly, Dictionary<MethodBase, WrappedPatch> pending)
		{
			if (patches == null)
			{
				Skip("patch-list-null", 1);
				return;
			}
			for (int i = 0; i < patches.Count; i++)
			{
				Patch val = patches[i];
				if (val == null)
				{
					Skip("patch-entry-null", 1);
					continue;
				}
				MethodInfo patchMethod;
				try
				{
					patchMethod = val.PatchMethod;
				}
				catch (Exception ex)
				{
					Skip("patch-method-unresolvable", 1);
					Instance log = Core.Log;
					if (log != null)
					{
						log.Warning($"[snitch] patch scan: a {kind} on {Describe(target)} owned by '{val.owner}' " + "has no resolvable method - " + ex.Message);
					}
					continue;
				}
				if (patchMethod == null)
				{
					Skip("patch-method-null", 1);
					continue;
				}
				if (pending.TryGetValue(patchMethod, out var value))
				{
					Note(value, kind, target);
					continue;
				}
				if (_wrapped.TryGetValue(patchMethod, out var value2))
				{
					Note(value2, kind, target);
					continue;
				}
				if (patchMethod is DynamicMethod)
				{
					Skip("dynamic-method", 1);
					continue;
				}
				if (patchMethod.DeclaringType == null)
				{
					Skip("no-declaring-type", 1);
					continue;
				}
				if ((object)patchMethod.DeclaringType.Assembly == SelfAssembly)
				{
					Skip("snitch-own", 1);
					continue;
				}
				if (patchMethod.IsAbstract)
				{
					Skip("abstract", 1);
					continue;
				}
				if (patchMethod.IsGenericMethodDefinition || patchMethod.ContainsGenericParameters)
				{
					Skip("open-generic", 1);
					continue;
				}
				try
				{
					if (patchMethod.GetMethodBody() == null)
					{
						Skip("no-il-body", 1);
						continue;
					}
				}
				catch (Exception ex2)
				{
					Skip("method-body-threw", 1);
					Instance log2 = Core.Log;
					if (log2 != null)
					{
						log2.Warning("[snitch] patch scan: " + Describe(patchMethod) + " would not hand over its IL body - " + ex2.Message);
					}
					continue;
				}
				string text = ResolveOwner(patchMethod, val.owner, byHarmonyId, byAssembly);
				if (text == null)
				{
					Skip("not-a-mod-patch", 1);
					NoteSkippedOwner(val.owner, patchMethod);
					continue;
				}
				string label = text + "." + Clean(patchMethod.DeclaringType.Name) + "." + Clean(patchMethod.Name);
				WrappedPatch wrappedPatch = new WrappedPatch
				{
					Method = patchMethod,
					Label = label,
					SectionId = SectionProfiler.GetId(label),
					Owner = text
				};
				Note(wrappedPatch, kind, target);
				pending[patchMethod] = wrappedPatch;
			}
		}

		private static void Note(WrappedPatch w, string kind, MethodBase target)
		{
			if (!w.Kinds.Contains(kind))
			{
				w.Kinds.Add(kind);
			}
			w.TargetCount++;
			if (w.Targets.Count < 8)
			{
				string item = Describe(target);
				if (!w.Targets.Contains(item))
				{
					w.Targets.Add(item);
				}
			}
		}

		private static void Skip(string reason, int count)
		{
			_skips.TryGetValue(reason, out var value);
			_skips[reason] = value + count;
		}

		private static void NoteSkippedOwner(string harmonyOwnerId, MethodInfo pm)
		{
			string text = ShortOwnerId(harmonyOwnerId);
			if (string.IsNullOrEmpty(text))
			{
				try
				{
					text = pm.DeclaringType.Assembly.GetName().Name;
				}
				catch (Exception ex)
				{
					Instance log = Core.Log;
					if (log != null)
					{
						log.Warning("[snitch] patch scan: could not name the owner of " + Describe(pm) + " - " + ex.Message);
					}
					text = "(unnamed)";
				}
			}
			_skippedOwners.TryGetValue(text, out var value);
			_skippedOwners[text] = value + 1;
		}

		private static string ShortOwnerId(string harmonyOwnerId)
		{
			if (string.IsNullOrEmpty(harmonyOwnerId))
			{
				return null;
			}
			int num = harmonyOwnerId.LastIndexOf(':');
			if (num < 0 || num >= harmonyOwnerId.Length - 1)
			{
				return harmonyOwnerId;
			}
			return harmonyOwnerId.Substring(num + 1);
		}

		private static string ResolveOwner(MethodInfo pm, string harmonyOwnerId, Dictionary<string, string> byHarmonyId, Dictionary<string, string> byAssembly)
		{
			if (!string.IsNullOrEmpty(harmonyOwnerId) && byHarmonyId.TryGetValue(harmonyOwnerId, out var value))
			{
				return value;
			}
			string fullName;
			try
			{
				fullName = pm.DeclaringType.Assembly.FullName;
			}
			catch (Exception ex)
			{
				Instance log = Core.Log;
				if (log != null)
				{
					log.Warning("[snitch] patch scan: could not read the assembly of " + Describe(pm) + " - " + ex.Message);
				}
				return null;
			}
			if (!string.IsNullOrEmpty(fullName) && byAssembly.TryGetValue(fullName, out var value2))
			{
				return value2;
			}
			return null;
		}

		private static void BuildOwnerIndex(Dictionary<string, string> byHarmonyId, Dictionary<string, string> byAssembly)
		{
			IEnumerable<MelonBase> registeredMelons;
			try
			{
				registeredMelons = MelonBase.RegisteredMelons;
			}
			catch (Exception ex)
			{
				Instance log = Core.Log;
				if (log != null)
				{
					log.Error("[snitch] patch scan: MelonBase.RegisteredMelons is unreadable, no patch can be attributed - " + ex);
				}
				return;
			}
			if (registeredMelons == null)
			{
				Instance log2 = Core.Log;
				if (log2 != null)
				{
					log2.Error("[snitch] patch scan: MelonBase.RegisteredMelons returned null, no patch can be attributed.");
				}
				return;
			}
			foreach (MelonBase item in registeredMelons)
			{
				if (item == null || (object)item == Core.Instance)
				{
					continue;
				}
				string text = MelonName(item);
				try
				{
					if (item.HarmonyInstance != null)
					{
						byHarmonyId[item.HarmonyInstance.Id] = text;
					}
				}
				catch (Exception ex2)
				{
					Instance log3 = Core.Log;
					if (log3 != null)
					{
						log3.Warning("[snitch] patch scan: " + text + " has no readable Harmony instance - " + ex2.Message);
					}
				}
				try
				{
					MelonAssembly melonAssembly = item.MelonAssembly;
					Assembly assembly = ((melonAssembly != null) ? melonAssembly.Assembly : null) ?? ((object)item).GetType().Assembly;
					if ((object)assembly != null && assembly.FullName != null)
					{
						byAssembly[assembly.FullName] = text;
					}
				}
				catch (Exception ex3)
				{
					Instance log4 = Core.Log;
					if (log4 != null)
					{
						log4.Warning("[snitch] patch scan: " + text + " has no readable assembly - " + ex3.Message);
					}
				}
			}
		}

		private static string MelonName(MelonBase melon)
		{
			string text = null;
			try
			{
				MelonInfoAttribute info = melon.Info;
				text = ((info != null) ? info.Name : null);
			}
			catch (Exception ex)
			{
				Instance log = Core.Log;
				if (log != null)
				{
					log.Warning("[snitch] patch scan: a Melon would not give its name - " + ex.Message);
				}
			}
			if (string.IsNullOrEmpty(text))
			{
				text = ((object)melon).GetType().Namespace ?? ((object)melon).GetType().Name;
			}
			return AutoInstrument.GroupName(text);
		}

		private static string Describe(MethodBase m)
		{
			if (m == null)
			{
				return "(null)";
			}
			return ((m.DeclaringType != null) ? m.DeclaringType.Name : "(global)") + "." + m.Name;
		}

		private static string Clean(string s)
		{
			if (string.IsNullOrEmpty(s))
			{
				return "(unnamed)";
			}
			return s.Replace('`', '_').Replace('<', '_').Replace('>', '_')
				.Replace(' ', '_');
		}

		private static void Pre(MethodBase __originalMethod)
		{
			if (Enabled && _ids.TryGetValue(__originalMethod, out var value))
			{
				SectionProfiler.Begin(value);
			}
		}

		private static void Fin(MethodBase __originalMethod)
		{
			if (Enabled && _ids.TryGetValue(__originalMethod, out var value))
			{
				SectionProfiler.End(value);
			}
		}
	}
	internal static class VanillaProbes
	{
		internal static volatile bool Enabled;

		private static bool _patched;

		private static readonly Dictionary<MethodBase, int> _ids = new Dictionary<MethodBase, int>();

		private static readonly List<string> _applied = new List<string>();

		private static readonly List<string> _failed = new List<string>();

		internal static void Enable()
		{
			EnsurePatched();
			Enabled = true;
			Instance log = Core.Log;
			if (log != null)
			{
				log.Msg("[snitch] vanilla probes ON. " + Status());
			}
		}

		internal static void Disable()
		{
			Enabled = false;
			Instance log = Core.Log;
			if (log != null)
			{
				log.Msg("[snitch] vanilla probes OFF (patches stay installed but dormant).");
			}
		}

		internal static string Status()
		{
			return $"enabled={Enabled} applied=[{string.Join(", ", _applied)}] failed=[{string.Join(", ", _failed)}]";
		}

		private static void EnsurePatched()
		{
			if (!_patched)
			{
				_patched = true;
				Patch(typeof(NPCMovement), "Update", "Vanilla.NPC.Movement.Update");
				Patch(typeof(NPCMovement), "FixedUpdate", "Vanilla.NPC.Movement.FixedUpdate");
				Patch(typeof(TimeManager), "Update", "Vanilla.Time.Update");
			}
		}

		private static void Patch(Type type, string method, string label)
		{
			//IL_0060: Unknown result type (might be due to invalid IL or missing references)
			//IL_007d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0088: Expected O, but got Unknown
			//IL_0088: Expected O, but got Unknown
			try
			{
				MethodInfo methodInfo = AccessTools.Method(type, method, Type.EmptyTypes, (Type[])null);
				if (methodInfo == null)
				{
					_failed.Add(label + "(method not found)");
					return;
				}
				int id = SectionProfiler.GetId(label);
				_ids[methodInfo] = id;
				Core.HarmonyInst.Patch((MethodBase)methodInfo, new HarmonyMethod(AccessTools.Method(typeof(VanillaProbes), "SharedPrefix", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(AccessTools.Method(typeof(VanillaProbes), "SharedFinalizer", (Type[])null, (Type[])null)), (HarmonyMethod)null);
				_applied.Add(label);
			}
			catch (Exception ex)
			{
				_failed.Add(label + "(" + ex.Message + ")");
			}
		}

		private static void SharedPrefix(MethodBase __originalMethod)
		{
			if (Enabled && _ids.TryGetValue(__originalMethod, out var value))
			{
				SectionProfiler.Begin(value);
			}
		}

		private static void SharedFinalizer(MethodBase __originalMethod)
		{
			if (Enabled && _ids.TryGetValue(__originalMethod, out var value))
			{
				SectionProfiler.End(value);
			}
		}
	}
}
namespace Snitch.UI
{
	internal static class ProfilerHud
	{
		internal static string BuildOverview()
		{
			FrameStats latestFrame = SnitchCore.LatestFrame;
			StringBuilder stringBuilder = new StringBuilder(512);
			if (!SnitchCore.Active)
			{
				stringBuilder.Append("<color=#fd5>idle</color> - run 'snitch start' (or auto-start) to sample.\n");
			}
			string value = ((latestFrame.MeanFps >= 50.0) ? "#5f5" : ((latestFrame.MeanFps >= 30.0) ? "#fd5" : "#f55"));
			stringBuilder.Append("<color=").Append(value).Append('>')
				.Append(latestFrame.MeanFps.ToString("F0"))
				.Append(" fps</color>  (min ")
				.Append(latestFrame.MinFps.ToString("F0"))
				.Append(")\n");
			stringBuilder.Append(latestFrame.MeanMs.ToString("F2")).Append(" ms   p95 ").Append(latestFrame.P95Ms.ToString("F2"))
				.Append("   gc0/1k ")
				.Append(latestFrame.Gc0Per1000.ToString("F1"));
			if (LanServer.Running)
			{
				stringBuilder.Append("\n<color=#8bf>phone</color> http://").Append(LanServer.Ip).Append(':')
					.Append(LanServer.Port)
					.Append("  token ")
					.Append(LanServer.Token);
			}
			AttributionStats latestAttribution = SnitchCore.LatestAttribution;
			if (latestAttribution.Samples > 0)
			{
				string value2 = (latestAttribution.PointsAtHiddenWork ? "#f55" : "#999");
				stringBuilder.Append("\n<color=").Append(value2).Append(">unattributed</color> ")
					.Append(latestAttribution.UnattributedMeanMs.ToString("F2"))
					.Append(" ms  ")
					.Append(latestAttribution.UnattributedPct.ToString("F0"))
					.Append('%');
				if (latestAttribution.PointsAtHiddenWork)
				{
					stringBuilder.Append("  <color=#f55>").Append(latestAttribution.SpikeFrames).Append(" bad frame(s) unexplained</color>");
				}
			}
			List<SectionRow> latestSections = SnitchCore.LatestSections;
			if (latestSections != null && latestSections.Count > 0)
			{
				stringBuilder.Append("\n<b>sections</b>");
				int num = Mathf.Min(8, latestSections.Count);
				for (int i = 0; i < num; i++)
				{
					SectionRow sectionRow = latestSections[i];
					stringBuilder.Append('\n').Append(sectionRow.Label).Append("   ")
						.Append(sectionRow.MsPerFrame.ToString("F2"))
						.Append(" ms  ")
						.Append(sectionRow.PctFrame.ToString("F0"))
						.Append('%');
				}
			}
			List<StateSnapshot> latestStates = SnitchCore.LatestStates;
			if (latestStates != null && latestStates.Count > 0)
			{
				stringBuilder.Append("\n<b>states</b>");
				foreach (StateSnapshot item in latestStates)
				{
					stringBuilder.Append('\n').Append(item.Title).Append(' ')
						.Append(item.EffectiveTotal())
						.Append(": ");
					for (int j = 0; j < item.Buckets.Count; j++)
					{
						if (j > 0)
						{
							stringBuilder.Append(' ');
						}
						stringBuilder.Append(item.Buckets[j].Name).Append('=').Append(item.Buckets[j].Count);
					}
				}
			}
			return stringBuilder.ToString();
		}

		internal static string BuildPanelMetrics(string panelId)
		{
			if (string.IsNullOrEmpty(panelId))
			{
				return "";
			}
			StringBuilder stringBuilder = new StringBuilder(128);
			string text = panelId + ".";
			List<CounterRow> latestCounters = SnitchCore.LatestCounters;
			if (latestCounters != null)
			{
				for (int i = 0; i < latestCounters.Count; i++)
				{
					CounterRow counterRow = latestCounters[i];
					if (!(counterRow.Id != panelId) || counterRow.Id.StartsWith(text))
					{
						string value = (counterRow.Id.StartsWith(text) ? counterRow.Id.Substring(text.Length) : counterRow.Id);
						Line(stringBuilder, $"{value} = {counterRow.Value:0.##} {counterRow.Unit}".TrimEnd());
					}
				}
			}
			List<StateSnapshot> latestStates = SnitchCore.LatestStates;
			if (latestStates != null)
			{
				for (int j = 0; j < latestStates.Count; j++)
				{
					StateSnapshot stateSnapshot = latestStates[j];
					if (stateSnapshot.Id != panelId && !stateSnapshot.Id.StartsWith(text))
					{
						continue;
					}
					StringBuilder stringBuilder2 = new StringBuilder();
					stringBuilder2.Append(stateSnapshot.Title).Append(' ').Append(stateSnapshot.EffectiveTotal())
						.Append(": ");
					for (int k = 0; k < stateSnapshot.Buckets.Count; k++)
					{
						if (k > 0)
						{
							stringBuilder2.Append(' ');
						}
						stringBuilder2.Append(stateSnapshot.Buckets[k].Name).Append('=').Append(stateSnapshot.Buckets[k].Count);
					}
					Line(stringBuilder, stringBuilder2.ToString());
				}
			}
			return stringBuilder.ToString();
		}

		private static void Line(StringBuilder sb, string s)
		{
			if (sb.Length > 0)
			{
				sb.Append('\n');
			}
			sb.Append(s);
		}
	}
	internal static class QrCode
	{
		private sealed class BitBuffer
		{
			private readonly List<byte> _bytes = new List<byte>();

			private int _bitLen;

			internal int Length => _bitLen;

			internal void Append(int value, int bits)
			{
				for (int num = bits - 1; num >= 0; num--)
				{
					if (_bitLen % 8 == 0)
					{
						_bytes.Add(0);
					}
					if (((value >> num) & 1) != 0)
					{
						_bytes[_bitLen / 8] |= (byte)(1 << 7 - _bitLen % 8);
					}
					_bitLen++;
				}
			}

			internal byte[] ToBytes()
			{
				return _bytes.ToArray();
			}
		}

		private static readonly int[][] EccL;

		private static readonly int[][] AlignPos;

		private static readonly int[] Exp;

		private static readonly int[] Log;

		internal static bool[,] Encode(string text)
		{
			byte[] bytes = Encoding.UTF8.GetBytes(text ?? "");
			int num = -1;
			for (int i = 1; i <= 10; i++)
			{
				int num2 = ((i <= 9) ? 8 : 16);
				int num3 = 4 + num2 + bytes.Length * 8;
				int[] array = EccL[i - 1];
				int num4 = array[1] * array[2] + array[3] * array[4];
				if (num3 <= num4 * 8)
				{
					num = i;
					break;
				}
			}
			if (num < 1)
			{
				return null;
			}
			byte[] cw = BuildCodewords(bytes, num);
			int num5 = 17 + 4 * num;
			int[,] array2 = new int[num5, num5];
			for (int j = 0; j < num5; j++)
			{
				for (int k = 0; k < num5; k++)
				{
					array2[j, k] = -1;
				}
			}
			bool[,] fn = new bool[num5, num5];
			DrawFunctionPatterns(array2, fn, num, num5);
			DrawCodewords(array2, fn, cw, num5);
			int num6 = int.MaxValue;
			int[,] array3 = null;
			for (int l = 0; l < 8; l++)
			{
				int[,] array4 = (int[,])array2.Clone();
				ApplyMask(array4, fn, l, num5);
				DrawFormatBits(array4, num, l, num5);
				int num7 = Penalty(array4, num5);
				if (num7 < num6)
				{
					num6 = num7;
					array3 = array4;
				}
			}
			bool[,] array5 = new bool[num5, num5];
			for (int m = 0; m < num5; m++)
			{
				for (int n = 0; n < num5; n++)
				{
					array5[m, n] = array3[m, n] == 1;
				}
			}
			return array5;
		}

		private static byte[] BuildCodewords(byte[] data, int version)
		{
			int[] obj = EccL[version - 1];
			int num = obj[0];
			int num2 = obj[1];
			int num3 = obj[2];
			int num4 = obj[3];
			int num5 = obj[4];
			int num6 = num2 + num4;
			int num7 = num2 * num3 + num4 * num5;
			BitBuffer bitBuffer = new BitBuffer();
			bitBuffer.Append(4, 4);
			bitBuffer.Append(data.Length, (version <= 9) ? 8 : 16);
			foreach (byte value in data)
			{
				bitBuffer.Append(value, 8);
			}
			int num8 = num7 * 8;
			int bits = Math.Min(4, num8 - bitBuffer.Length);
			bitBuffer.Append(0, bits);
			while (bitBuffer.Length % 8 != 0)
			{
				bitBuffer.Append(0, 1);
			}
			byte b = 236;
			while (bitBuffer.Length < num8)
			{
				bitBuffer.Append(b, 8);
				b = (byte)((b == 236) ? 17u : 236u);
			}
			byte[] sourceArray = bitBuffer.ToBytes();
			byte[][] array = new byte[num6][];
			byte[][] array2 = new byte[num6][];
			int[] gen = RsGenerator(num);
			int num9 = 0;
			for (int j = 0; j < num6; j++)
			{
				int num10 = ((j < num2) ? num3 : num5);
				byte[] array3 = new byte[num10];
				Array.Copy(sourceArray, num9, array3, 0, num10);
				num9 += num10;
				array[j] = array3;
				array2[j] = RsEncode(array3, gen);
			}
			List<byte> list = new List<byte>(num7 + num6 * num);
			int num11 = Math.Max(num3, num5);
			for (int k = 0; k < num11; k++)
			{
				for (int l = 0; l < num6; l++)
				{
					if (k < array[l].Length)
					{
						list.Add(array[l][k]);
					}
				}
			}
			for (int m = 0; m < num; m++)
			{
				for (int n = 0; n < num6; n++)
				{
					list.Add(array2[n][m]);
				}
			}
			return list.ToArray();
		}

		static QrCode()
		{
			EccL = new int[10][]
			{
				new int[5] { 7, 1, 19, 0, 0 },
				new int[5] { 10, 1, 34, 0, 0 },
				new int[5] { 15, 1, 55, 0, 0 },
				new int[5] { 20, 1, 80, 0, 0 },
				new int[5] { 26, 1, 108, 0, 0 },
				new int[5] { 18, 2, 68, 0, 0 },
				new int[5] { 20, 2, 78, 0, 0 },
				new int[5] { 24, 2, 97, 0, 0 },
				new int[5] { 30, 2, 116, 0, 0 },
				new int[5] { 18, 2, 68, 2, 69 }
			};
			AlignPos = new int[10][]
			{
				new int[0],
				new int[2] { 6, 18 },
				new int[2] { 6, 22 },
				new int[2] { 6, 26 },
				new int[2] { 6, 30 },
				new int[2] { 6, 34 },
				new int[3] { 6, 22, 38 },
				new int[3] { 6, 24, 42 },
				new int[3] { 6, 26, 46 },
				new int[3] { 6, 28, 50 }
			};
			Exp = new int[256];
			Log = new int[256];
			int num = 1;
			for (int i = 0; i < 256; i++)
			{
				Exp[i] = num;
				if (i < 255)
				{
					Log[num] = i;
				}
				num <<= 1;
				if ((num & 0x100) != 0)
				{
					num ^= 0x11D;
				}
			}
		}

		private static int Mul(int a, int b)
		{
			if (a != 0 && b != 0)
			{
				return Exp[(Log[a] + Log[b]) % 255];
			}
			return 0;
		}

		private static int[] RsGenerator(int degree)
		{
			int[] array = new int[1] { 1 };
			for (int i = 0; i < degree; i++)
			{
				array = MulPoly(array, new int[2]
				{
					1,
					Exp[i]
				});
			}
			return array;
		}

		private static int[] MulPoly(int[] a, int[] b)
		{
			int[] array = new int[a.Length + b.Length - 1];
			for (int i = 0; i < a.Length; i++)
			{
				for (int j = 0; j < b.Length; j++)
				{
					array[i + j] ^= Mul(a[i], b[j]);
				}
			}
			return array;
		}

		private static byte[] RsEncode(byte[] data, int[] gen)
		{
			int num = gen.Length - 1;
			int[] array = new int[data.Length + num];
			for (int i = 0; i < data.Length; i++)
			{
				array[i] = data[i];
			}
			for (int j = 0; j < data.Length; j++)
			{
				int num2 = array[j];
				if (num2 != 0)
				{
					for (int k = 0; k < gen.Length; k++)
					{
						array[j + k] ^= Mul(gen[k], num2);
					}
				}
			}
			byte[] array2 = new byte[num];
			for (int l = 0; l < num; l++)
			{
				array2[l] = (byte)array[data.Length + l];
			}
			return array2;
		}

		private static void DrawFunctionPatterns(int[,] m, bool[,] fn, int version, int size)
		{
			for (int i = 0; i < size; i++)
			{
				Set(m, fn, 6, i, i % 2 == 0);
				Set(m, fn, i, 6, i % 2 == 0);
			}
			DrawFinder(m, fn, 0, 0, size);
			DrawFinder(m, fn, size - 7, 0, size);
			DrawFinder(m, fn, 0, size - 7, size);
			int[] array = AlignPos[version - 1];
			for (int j = 0; j < array.Length; j++)
			{
				for (int k = 0; k < array.Length; k++)
				{
					int num = array[j];
					int num2 = array[k];
					if ((num != 6 || num2 != 6) && (num != 6 || num2 != size - 7) && (num != size - 7 || num2 != 6))
					{
						DrawAlignment(m, fn, num, num2);
					}
				}
			}
			Set(m, fn, size - 8, 8, dark: true);
			ReserveFormat(fn, size);
			if (version >= 7)
			{
				ReserveVersion(fn, size);
			}
		}

		private static void DrawFinder(int[,] m, bool[,] fn, int row, int col, int size)
		{
			for (int i = -1; i <= 7; i++)
			{
				for (int j = -1; j <= 7; j++)
				{
					int num = row + i;
					int num2 = col + j;
					if (num >= 0 && num < size && num2 >= 0 && num2 < size)
					{
						bool dark = i >= 0 && i <= 6 && j >= 0 && j <= 6 && (i == 0 || i == 6 || j == 0 || j == 6 || (i >= 2 && i <= 4 && j >= 2 && j <= 4));
						Set(m, fn, num, num2, dark);
					}
				}
			}
		}

		private static void DrawAlignment(int[,] m, bool[,] fn, int row, int col)
		{
			for (int i = -2; i <= 2; i++)
			{
				for (int j = -2; j <= 2; j++)
				{
					Set(m, fn, row + i, col + j, Math.Max(Math.Abs(i), Math.Abs(j)) != 1);
				}
			}
		}

		private static void ReserveFormat(bool[,] fn, int size)
		{
			for (int i = 0; i < 6; i++)
			{
				fn[i, 8] = true;
				fn[8, i] = true;
			}
			fn[7, 8] = true;
			fn[8, 7] = true;
			fn[8, 8] = true;
			for (int j = 0; j < 8; j++)
			{
				fn[size - 1 - j, 8] = true;
				fn[8, size - 1 - j] = true;
			}
		}

		private static void ReserveVersion(bool[,] fn, int size)
		{
			for (int i = 0; i < 6; i++)
			{
				for (int j = 0; j < 3; j++)
				{
					fn[size - 11 + j, i] = true;
					fn[i, size - 11 + j] = true;
				}
			}
		}

		private static void DrawCodewords(int[,] m, bool[,] fn, byte[] cw, int size)
		{
			int num = 0;
			int num2 = cw.Length * 8;
			for (int num3 = size - 1; num3 > 0; num3 -= 2)
			{
				if (num3 == 6)
				{
					num3 = 5;
				}
				for (int i = 0; i < size; i++)
				{
					int num4 = ((((num3 + 1) & 2) == 0) ? (size - 1 - i) : i);
					for (int j = 0; j < 2; j++)
					{
						int num5 = num3 - j;
						if (!fn[num4, num5])
						{
							bool flag = false;
							if (num < num2)
							{
								flag = ((cw[num >> 3] >> 7 - (num & 7)) & 1) == 1;
							}
							m[num4, num5] = (flag ? 1 : 0);
							num++;
						}
					}
				}
			}
		}

		private static void ApplyMask(int[,] m, bool[,] fn, int mask, int size)
		{
			for (int i = 0; i < size; i++)
			{
				for (int j = 0; j < size; j++)
				{
					if (!fn[i, j] && mask switch
					{
						0 => (i + j) % 2 == 0, 
						1 => i % 2 == 0, 
						2 => j % 3 == 0, 
						3 => (i + j) % 3 == 0, 
						4 => (i / 2 + j / 3) % 2 == 0, 
						5 => i * j % 2 + i * j % 3 == 0, 
						6 => (i * j % 2 + i * j % 3) % 2 == 0, 
						_ => ((i + j) % 2 + i * j % 3) % 2 == 0, 
					})
					{
						m[i, j] ^= 1;
					}
				}
			}
		}

		private static void DrawFormatBits(int[,] m, int version, int mask, int size)
		{
			int num = 8 | mask;
			int num2 = num;
			for (int i = 0; i < 10; i++)
			{
				num2 = (num2 << 1) ^ ((num2 >> 9) * 1335);
			}
			int value = ((num << 10) | num2) ^ 0x5412;
			for (int j = 0; j < 15; j++)
			{
				int v = Bit(value, j);
				if (j < 6)
				{
					SetVal(m, j, 8, v);
				}
				else if (j < 8)
				{
					SetVal(m, j + 1, 8, v);
				}
				else
				{
					SetVal(m, size - 15 + j, 8, v);
				}
				if (j < 8)
				{
					SetVal(m, 8, size - j - 1, v);
				}
				else if (j < 9)
				{
					SetVal(m, 8, 7, v);
				}
				else
				{
					SetVal(m, 8, 15 - j - 1, v);
				}
			}
			SetVal(m, size - 8, 8, 1);
			if (version >= 7)
			{
				int num3 = version;
				for (int k = 0; k < 12; k++)
				{
					num3 = (num3 << 1) ^ ((num3 >> 11) * 7973);
				}
				int value2 = (version << 12) | num3;
				for (int l = 0; l < 18; l++)
				{
					int v2 = Bit(value2, l);
					int num4 = l / 3;
					int num5 = l % 3;
					SetVal(m, size - 11 + num5, num4, v2);
					SetVal(m, num4, size - 11 + num5, v2);
				}
			}
		}

		private static int Penalty(int[,] m, int size)
		{
			int num = 0;
			for (int i = 0; i < size; i++)
			{
				int num2 = -1;
				int num3 = 0;
				for (int j = 0; j < size; j++)
				{
					int num4 = m[i, j];
					if (num4 == num2)
					{
						num3++;
						if (num3 == 5)
						{
							num += 3;
						}
						else if (num3 > 5)
						{
							num++;
						}
					}
					else
					{
						num2 = num4;
						num3 = 1;
					}
				}
			}
			for (int k = 0; k < size; k++)
			{
				int num5 = -1;
				int num6 = 0;
				for (int l = 0; l < size; l++)
				{
					int num7 = m[l, k];
					if (num7 == num5)
					{
						num6++;
						if (num6 == 5)
						{
							num += 3;
						}
						else if (num6 > 5)
						{
							num++;
						}
					}
					else
					{
						num5 = num7;
						num6 = 1;
					}
				}
			}
			for (int n = 0; n < size - 1; n++)
			{
				for (int num8 = 0; num8 < size - 1; num8++)
				{
					int num9 = m[n, num8];
					if (num9 == m[n, num8 + 1] && num9 == m[n + 1, num8] && num9 == m[n + 1, num8 + 1])
					{
						num += 3;
					}
				}
			}
			int[] pat = new int[11]
			{
				1, 0, 1, 1, 1, 0, 1, 0, 0, 0,
				0
			};
			int[] pat2 = new int[11]
			{
				0, 0, 0, 0, 1, 0, 1, 1, 1, 0,
				1
			};
			for (int num10 = 0; num10 < size; num10++)
			{
				for (int num11 = 0; num11 <= size - 11; num11++)
				{
					if (MatchPattern(m, num10, num11, horizontal: true, pat) || MatchPattern(m, num10, num11, horizontal: true, pat2))
					{
						num += 40;
					}
				}
			}
			for (int num12 = 0; num12 < size; num12++)
			{
				for (int num13 = 0; num13 <= size - 11; num13++)
				{
					if (MatchPattern(m, num13, num12, horizontal: false, pat) || MatchPattern(m, num13, num12, horizontal: false, pat2))
					{
						num += 40;
					}
				}
			}
			int num14 = 0;
			for (int num15 = 0; num15 < size; num15++)
			{
				for (int num16 = 0; num16 < size; num16++)
				{
					if (m[num15, num16] == 1)
					{
						num14++;
					}
				}
			}
			int num17 = size * size;
			int num18 = Math.Abs(num14 * 100 / num17 - 50) / 5;
			return num + num18 * 10;
		}

		private static bool MatchPattern(int[,] m, int r, int c, bool horizontal, int[] pat)
		{
			for (int i = 0; i < 11; i++)
			{
				if ((horizontal ? m[r, c + i] : m[r + i, c]) != pat[i])
				{
					return false;
				}
			}
			return true;
		}

		private static void Set(int[,] m, bool[,] fn, int r, int c, bool dark)
		{
			m[r, c] = (dark ? 1 : 0);
			fn[r, c] = true;
		}

		private static void SetVal(int[,] m, int r, int c, int v)
		{
			m[r, c] = v;
		}

		private static int Bit(int value, int i)
		{
			return (value >> i) & 1;
		}
	}
	internal static class QrImage
	{
		private const int Scale = 6;

		private const int Quiet = 4;

		private static int[] _cache;

		private static string _cacheUrl;

		internal static int[] Build()
		{
			if (!LanServer.Running)
			{
				_cache = null;
				_cacheUrl = null;
				return null;
			}
			string text = (WebAssets.HasBundledDashboard() ? ("&lan=" + LanServer.Ip + ":" + LanServer.Port) : "");
			string text2;
			if (RelayHost.Running && !string.IsNullOrEmpty(RelayHost.Code))
			{
				text2 = "https://snitch.doodesch.de/#join=" + RelayHost.Code + "&t=" + LanServer.Token + text;
			}
			else
			{
				if (text.Length <= 0)
				{
					_cache = null;
					_cacheUrl = null;
					return null;
				}
				text2 = "http://" + LanServer.Ip + ":" + LanServer.Port + "/#remote&t=" + LanServer.Token;
			}
			if (text2 == _cacheUrl && _cache != null)
			{
				return _cache;
			}
			bool[,] array = QrCode.Encode(text2);
			if (array == null)
			{
				return null;
			}
			_cache = Rasterize(array);
			_cacheUrl = text2;
			return _cache;
		}

		private static int[] Rasterize(bool[,] m)
		{
			int length = m.GetLength(0);
			int num = (length + 8) * 6;
			int[] array = new int[2 + num * num];
			array[0] = num;
			array[1] = num;
			for (int i = 0; i < num * num; i++)
			{
				array[2 + i] = -1;
			}
			for (int j = 0; j < length; j++)
			{
				for (int k = 0; k < length; k++)
				{
					if (!m[j, k])
					{
						continue;
					}
					int num2 = (k + 4) * 6;
					int num3 = (j + 4) * 6;
					for (int l = 0; l < 6; l++)
					{
						int num4 = 2 + (num3 + l) * num + num2;
						for (int n = 0; n < 6; n++)
						{
							array[num4 + n] = -16777216;
						}
					}
				}
			}
			return array;
		}
	}
}
namespace Snitch.Server
{
	internal static class LanServer
	{
		private const int MaxRequestBytes = 65536;

		private const int SocketTimeoutMs = 5000;

		private static TcpListener _listener;

		private static Thread _accept;

		private static volatile bool _running;

		private static int _port;

		private static string _ip = "127.0.0.1";

		private static string _token = "";

		private const string EmptySnapshot = "{\"type\":\"snapshot\",\"v\":1,\"frame\":{},\"sections\":[],\"counters\":[],\"states\":[],\"panels\":[],\"logs\":{\"timeline\":[]}}";

		internal static bool Running => _running;

		internal static int Port => _port;

		internal static string Ip => _ip;

		internal static string Token => _token;

		internal static void Start(int port)
		{
			if (_running)
			{
				return;
			}
			_port = port;
			_ip = DetectLanIp();
			_token = (string.IsNullOrEmpty(Preferences.ServerToken) ? Guid.NewGuid().ToString("N").Substring(0, 8) : Preferences.ServerToken);
			try
			{
				_listener = new TcpListener(IPAddress.Any, port);
				_listener.Start();
				_running = true;
				_accept = new Thread(AcceptLoop)
				{
					IsBackground = true,
					Name = "Snitch-LanServer"
				};
				_accept.Start();
				Instance log = Core.Log;
				if (log != null)
				{
					log.Msg($"[snitch] LAN remote on http://{_ip}:{port}/ (token {_token}). Scan the QR in the dashboard from your phone.");
				}
				Instance log2 = Core.Log;
				if (log2 != null)
				{
					log2.Msg($"[snitch] if the phone can't connect, allow Schedule I (or TCP port {port}) through Windows Firewall on your Private network.");
				}
			}
			catch (Exception ex)
			{
				_running = false;
				Instance log3 = Core.Log;
				if (log3 != null)
				{
					log3.Error($"[snitch] LAN remote failed to start on {port}: {ex.Message} (port in use? change LanPort)");
				}
			}
		}

		internal static void Stop()
		{
			_running = false;
			try
			{
				_listener?.Stop();
			}
			catch
			{
			}
			_listener = null;
		}

		internal static string LanInfoJson(bool includeToken)
		{
			if (!_running)
			{
				return "\"lan\":{\"enabled\":false}";
			}
			StringBuilder stringBuilder = new StringBuilder(160);
			stringBuilder.Append("\"lan\":{\"enabled\":true,\"ip\":\"").Append(_ip).Append("\",\"port\":")
				.Append(_port)
				.Append(",\"url\":\"http://")
				.Append(_ip)
				.Append(':')
				.Append(_port)
				.Append("/\"")
				.Append(",\"bundled\":")
				.Append(WebAssets.HasBundledDashboard() ? "true" : "false");
			if (includeToken)
			{
				stringBuilder.Append(",\"token\":\"").Append(_token).Append('"');
			}
			stringBuilder.Append('}');
			return stringBuilder.ToString();
		}

		private static void AcceptLoop()
		{
			while (_running)
			{
				TcpClient client;
				try
				{
					client = _listener.AcceptTcpClient();
				}
				catch
				{
					if (!_running)
					{
						break;
					}
					continue;
				}
				Task.Run(delegate
				{
					Handle(client);
				});
			}
		}

		private static void Handle(TcpClient client)
		{
			try
			{
				using (client)
				{
					using NetworkStream networkStream = client.GetStream();
					networkStream.ReadTimeout = 5000;
					networkStream.WriteTimeout = 5000;
					if (ReadRequest(networkStream, out var method, out var path, out var query, out var body))
					{
						Route(networkStream, method, path, ParseQuery(query), body);
					}
				}
			}
			catch
			{
			}
		}

		private static void Route(NetworkStream stream, string method, string path, Dictionary<string, string> q, string body)
		{
			path = path.ToLowerInvariant();
			if (method == "OPTIONS")
			{
				WriteStatus(stream, 204, "No Content");
				return;
			}
			switch (path)
			{
			case "/health":
				WriteJson(stream, WireProtocol.BuildHealth(SnitchCore.LastFrame, SnitchCore.LastScene, LanInfoJson(includeToken: false)));
				return;
			case "/snapshot":
			case "/caps":
			case "/control":
				if (!TokenOk(q))
				{
					WriteJson(stream, "{\"ok\":false,\"error\":\"token\"}", 401, "Unauthorized");
					return;
				}
				switch (path)
				{
				case "/snapshot":
					WriteJson(stream, SnitchCore.LatestJson ?? "{\"type\":\"snapshot\",\"v\":1,\"frame\":{},\"sections\":[],\"counters\":[],\"states\":[],\"panels\":[],\"logs\":{\"timeline\":[]}}");
					return;
				case "/caps":
					WriteJson(stream, SnitchCore.CapsJson ?? WireProtocol.BuildCaps());
					return;
				case "/control":
				{
					string value;
					string cmd = (q.TryGetValue("cmd", out value) ? value : "");
					string value2;
					string id = (q.TryGetValue("id", out value2) ? value2 : "");
					string value4;
					string value3 = (q.TryGetValue("value", out value4) ? value4 : "");
					WriteJson(stream, SnitchServer.ApplyControl(cmd, id, value3));
					return;
				}
				}
				break;
			}
			byte[] bytes;
			string contentType;
			byte[] bytes2;
			string contentType2;
			if (method != "GET")
			{
				WriteStatus(stream, 405, "Method Not Allowed");
			}
			else if (WebAssets.TryResolve(path, out bytes, out contentType))
			{
				WriteBytes(stream, bytes, contentType, path.StartsWith("/assets/"));
			}
			else if (WebAssets.TryResolve("/index.html", out bytes2, out contentType2))
			{
				WriteBytes(stream, bytes2, contentType2, cache: false);
			}
			else
			{
				WriteStatus(stream, 404, "Not Found");
			}
		}

		private static bool TokenOk(Dictionary<string, string> q)
		{
			if (q.TryGetValue("token", out var value) && value == _token)
			{
				return true;
			}
			if (q.TryGetValue("x-snitch-token", out var value2) && value2 == _token)
			{
				return true;
			}
			return false;
		}

		private static bool ReadRequest(NetworkStream stream, out string method, out string path, out string query, out string body)
		{
			method = (path = (query = (body = "")));
			MemoryStream memoryStream = new MemoryStream();
			byte[] array = new byte[4096];
			int num = -1;
			while (num < 0 && memoryStream.Length < 65536)
			{
				int num2;
				try
				{
					num2 = stream.Read(array, 0, array.Length);
				}
				catch
				{
					return false;
				}
				if (num2 <= 0)
				{
					break;
				}
				memoryStream.Write(array, 0, num2);
				num = IndexOfDoubleCrlf(memoryStream.GetBuffer(), (int)memoryStream.Length);
			}
			if (num < 0)
			{
				return false;
			}
			string[] array2 = Encoding.ASCII.GetString(memoryStream.GetBuffer(), 0, num).Split(new string[1] { "\r\n" }, StringSplitOptions.None);
			if (array2.Length == 0)
			{
				return false;
			}
			string[] array3 = array2[0].Split(' ');
			if (array3.Length < 2)
			{
				return false;
			}
			method = array3[0].ToUpperInvariant();
			string text = array3[1];
			int num3 = text.IndexOf('?');
			if (num3 >= 0)
			{
				path = text.Substring(0, num3);
				query = text.Substring(num3 + 1);
			}
			else
			{
				path = text;
			}
			int result = 0;
			for (int i = 1; i < array2.Length; i++)
			{
				int num4 = array2[i].IndexOf(':');
				if (num4 > 0)
				{
					string text2 = array2[i].Substring(0, num4).Trim().ToLowerInvariant();
					string text3 = array2[i].Substring(num4 + 1).Trim();
					if (text2 == "content-length")
					{
						int.TryParse(text3, out result);
					}
					else if (text2 == "x-snitch-token")
					{
						query = ((query.Length > 0) ? (query + "&") : "") + "x-snitch-token=" + Uri.EscapeDataString(text3);
					}
				}
			}
			if (result > 0)
			{
				int num5 = num + 4;
				int num6;
				for (int j = (int)memoryStream.Length - num5; j < result; j += num6)
				{
					if (memoryStream.Length >= 65536)
					{
						break;
					}
					try
					{
						num6 = stream.Read(array, 0, array.Length);
					}
					catch
					{
						break;
					}
					if (num6 <= 0)
					{
						break;
					}
					memoryStream.Write(array, 0, num6);
				}
				int num7 = Math.Min(result, (int)memoryStream.Length - num5);
				if (num7 > 0)
				{
					body = Encoding.UTF8.GetString(memoryStream.GetBuffer(), num5, num7);
				}
			}
			return true;
		}

		private static int IndexOfDoubleCrlf(byte[] b, int len)
		{
			for (int i = 0; i + 3 < len; i++)
			{
				if (b[i] == 13 && b[i + 1] == 10 && b[i + 2] == 13 && b[i + 3] == 10)
				{
					return i;
				}
			}
			return -1;
		}

		private static Dictionary<string, string> ParseQuery(string query)
		{
			Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
			if (string.IsNullOrEmpty(query))
			{
				return dictionary;
			}
			string[] array = query.Split('&');
			foreach (string text in array)
			{
				if (text.Length != 0)
				{
					int num = text.IndexOf('=');
					string text2 = ((num < 0) ? text : text.Substring(0, num));
					string text3 = ((num < 0) ? "" : text.Substring(num + 1));
					try
					{
						text2 = Uri.UnescapeDataString(text2);
						text3 = Uri.UnescapeDataString(text3);
					}
					catch
					{
					}
					dictionary[text2] = text3;
				}
			}
			return dictionary;
		}

		private static void WriteJson(NetworkStream stream, string json, int code = 200, string reason = "OK")
		{
			WriteResponse(stream, code, reason, "application/json", Encoding.UTF8.GetBytes(json ?? ""), cache: false);
		}

		private static void WriteBytes(NetworkStream stream, byte[] bytes, string contentType, bool cache)
		{
			WriteResponse(stream, 200, "OK", contentType, bytes, cache);
		}

		private static void WriteStatus(NetworkStream stream, int code, string reason)
		{
			WriteResponse(stream, code, reason, "text/plain; charset=utf-8", Encoding.UTF8.GetBytes(reason), cache: false);
		}

		private static void WriteResponse(NetworkStream stream, int code, string reason, string contentType, byte[] body, bool cache)
		{
			try
			{
				StringBuilder stringBuilder = new StringBuilder(256);
				stringBuilder.Append("HTTP/1.1 ").Append(code).Append(' ')
					.Append(reason)
					.Append("\r\n");
				stringBuilder.Append("Content-Type: ").Append(contentType).Append("\r\n");
				stringBuilder.Append("Content-Length: ").Append(body.Length).Append("\r\n");
				stringBuilder.Append(cache ? "Cache-Control: public, max-age=2592000, immutable\r\n" : "Cache-Control: no-store\r\n");
				stringBuilder.Append("Connection: close\r\n\r\n");
				byte[] bytes = Encoding.ASCII.GetBytes(stringBuilder.ToString());
				stream.Write(bytes, 0, bytes.Length);
				if (body.Length != 0)
				{
					stream.Write(body, 0, body.Length);
				}
				stream.Flush();
			}
			catch
			{
			}
		}

		private static string DetectLanIp()
		{
			try
			{
				using Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
				socket.Connect("8.8.8.8", 65530);
				if (socket.LocalEndPoint is IPEndPoint iPEndPoint && !IPAddress.IsLoopback(iPEndPoint.Address))
				{
					return iPEndPoint.Address.ToString();
				}
			}
			catch
			{
			}
			try
			{
				IPAddress[] hostAddresses = Dns.GetHostAddresses(Dns.GetHostName());
				foreach (IPAddress iPAddress in hostAddresses)
				{
					if (iPAddress.AddressFamily == AddressFamily.InterNetwork && !IPAddress.IsLoopback(iPAddress))
					{
						string text = iPAddress.ToString();
						if (!text.StartsWith("169.254"))
						{
							return text;
						}
					}
				}
			}
			catch
			{
			}
			return "127.0.0.1";
		}
	}
	internal static class RelayCrypto
	{
		private static byte[] DeriveKey(string token)
		{
			using SHA256 sHA = SHA256.Create();
			return sHA.ComputeHash(Encoding.UTF8.GetBytes("snitch-relay:v1:" + token));
		}

		internal static string Encrypt(string token, string json)
		{
			byte[] key = DeriveKey(token);
			byte[] array = new byte[12];
			RandomNumberGenerator.Fill(array);
			byte[] bytes = Encoding.UTF8.GetBytes(json);
			byte[] array2 = new byte[bytes.Length];
			byte[] array3 = new byte[16];
			using (AesGcm aesGcm = new AesGcm(key))
			{
				aesGcm.Encrypt(array, bytes, array2, array3);
			}
			byte[] array4 = new byte[12 + array2.Length + 16];
			Buffer.BlockCopy(array, 0, array4, 0, 12);
			Buffer.BlockCopy(array2, 0, array4, 12, array2.Length);
			Buffer.BlockCopy(array3, 0, array4, 12 + array2.Length, 16);
			return Convert.ToBase64String(array4);
		}

		internal static string Decrypt(string token, string b64)
		{
			byte[] array = Convert.FromBase64String(b64);
			if (array.Length < 28)
			{
				return null;
			}
			byte[] key = DeriveKey(token);
			byte[] array2 = new byte[12];
			byte[] array3 = new byte[16];
			int num = array.Length - 12 - 16;
			byte[] array4 = new byte[num];
			Buffer.BlockCopy(array, 0, array2, 0, 12);
			Buffer.BlockCopy(array, 12, array4, 0, num);
			Buffer.BlockCopy(array, 12 + num, array3, 0, 16);
			byte[] array5 = new byte[num];
			using (AesGcm aesGcm = new AesGcm(key))
			{
				aesGcm.Decrypt(array2, array4, array3, array5);
			}
			return Encoding.UTF8.GetString(array5);
		}
	}
	internal static class RelayHost
	{
		private const string RelayBase = "wss://relay.doodesch.de/?app=snitch&role=host&code=";

		private static volatile bool _running;

		private static CancellationTokenSource _cts;

		private static string _code = "";

		private static volatile int _clients;

		internal static bool Running => _running;

		internal static string Code => _code;

		internal static void Start(string code)
		{
			if (!_running)
			{
				_code = code ?? "";
				_clients = 0;
				_running = true;
				_cts = new CancellationTokenSource();
				Task.Run(() => Loop(_cts.Token));
				Instance log = Core.Log;
				if (log != null)
				{
					log.Msg("[snitch] relay host on (code " + _code + ") - phone can connect via relay.doodesch.de from any network.");
				}
			}
		}

		internal static void Stop()
		{
			_running = false;
			_clients = 0;
			try
			{
				_cts?.Cancel();
			}
			catch
			{
			}
			_cts = null;
		}

		private static async Task Loop(CancellationToken ct)
		{
			while (_running && !ct.IsCancellationRequested)
			{
				try
				{
					await Session(ct);
				}
				catch (Exception ex)
				{
					if (_running)
					{
						Instance log = Core.Log;
						if (log != null)
						{
							log.Warning("[snitch] relay host dropped: " + ex.Message);
						}
					}
				}
				if (_running && !ct.IsCancellationRequested)
				{
					try
					{
						await Task.Delay(3000, ct);
					}
					catch
					{
					}
				}
			}
		}

		private static async Task Session(CancellationToken ct)
		{
			using ClientWebSocket ws = new ClientWebSocket();
			await ws.ConnectAsync(new Uri("wss://relay.doodesch.de/?app=snitch&role=host&code=" + Uri.EscapeDataString(_code)), ct);
			_clients = 0;
			Task task = ReceiveLoop(ws, ct);
			Task task2 = SendLoop(ws, ct);
			await Task.WhenAny(task, task2);
			try
			{
				await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "bye", CancellationToken.None);
			}
			catch
			{
			}
		}

		private static async Task SendLoop(ClientWebSocket ws, CancellationToken ct)
		{
			while (!ct.IsCancellationRequested && ws.State == WebSocketState.Open)
			{
				if (_clients > 0)
				{
					string latestJson = SnitchCore.LatestJson;
					if (!string.IsNullOrEmpty(latestJson))
					{
						string s = "{\"d\":\"" + RelayCrypto.Encrypt(LanServer.Token, latestJson) + "\"}";
						byte[] bytes = Encoding.UTF8.GetBytes(s);
						await ws.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, endOfMessage: true, ct);
					}
				}
				await Task.Delay(300, ct);
			}
		}

		private static async Task ReceiveLoop(ClientWebSocket ws, CancellationToken ct)
		{
			byte[] buf = new byte[16384];
			StringBuilder sb = new StringBuilder();
			while (!ct.IsCancellationRequested && ws.State == WebSocketState.Open)
			{
				sb.Clear();
				WebSocketReceiveResult webSocketReceiveResult;
				do
				{
					webSocketReceiveResult = await ws.ReceiveAsync(new ArraySegment<byte>(buf), ct);
					if (webSocketReceiveResult.MessageType == WebSocketMessageType.Close)
					{
						return;
					}
					sb.Append(Encoding.UTF8.GetString(buf, 0, webSocketReceiveResult.Count));
				}
				while (!webSocketReceiveResult.EndOfMessage);
				HandleFrame(sb.ToString());
			}
		}

		private static void HandleFrame(string text)
		{
			if (text.IndexOf("__relay", StringComparison.Ordinal) >= 0)
			{
				if (text.IndexOf("nohost", StringComparison.Ordinal) >= 0)
				{
					_clients = 0;
				}
				else if (text.IndexOf("join", StringComparison.Ordinal) >= 0 || text.IndexOf("leave", StringComparison.Ordinal) >= 0)
				{
					_clients = Math.Max(0, ExtractInt(text, "n"));
				}
				return;
			}
			string text2 = ExtractField(text, "d");
			if (string.IsNullOrEmpty(text2))
			{
				return;
			}
			try
			{
				string text3 = RelayCrypto.Decrypt(LanServer.Token, text2);
				if (!string.IsNullOrEmpty(text3))
				{
					SnitchServer.ApplyControl(ExtractField(text3, "cmd"), ExtractField(text3, "id"), ExtractField(text3, "value"));
				}
			}
			catch (Exception ex)
			{
				Instance log = Core.Log;
				if (log != null)
				{
					log.Warning("[snitch] relay control failed: " + ex.Message);
				}
			}
		}

		private static string ExtractField(string body, string key)
		{
			if (string.IsNullOrEmpty(body))
			{
				return null;
			}
			int num = body.IndexOf("\"" + key + "\"", StringComparison.Ordinal);
			if (num < 0)
			{