Decompiled source of Snitch v1.6.2

Snitch.dll

Decompiled 20 hours 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.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.6.2", "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.6.2.0")]
[assembly: AssemblyInformationalVersion("1.6.2+7ee24a1f75979e8f353e0f0f8d3aaa4ee60473aa")]
[assembly: AssemblyProduct("Snitch")]
[assembly: AssemblyTitle("Snitch")]
[assembly: NeutralResourcesLanguage("en-US")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.6.2.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.6.2";
	}
}
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, states, report, lan", "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 "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 | 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");
			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}");
		}

		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];
				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;
				try
				{
					MelonPreferences.Save();
				}
				catch
				{
				}
				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;
				try
				{
					MelonPreferences.Save();
				}
				catch
				{
				}
				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 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;
			}
		}

		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
			{
				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
			{
				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;
			}
			catch
			{
				return null;
			}
			if (types == null)
			{
				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
			{
			}
			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;
					}
				}
				catch
				{
				}
			}
			return null;
		}

		private static string ModName(MelonMod mod)
		{
			string text = null;
			try
			{
				MelonInfoAttribute info = ((MelonBase)mod).Info;
				text = ((info != null) ? info.Name : null);
			}
			catch
			{
			}
			if (string.IsNullOrEmpty(text))
			{
				text = ((object)mod).GetType().Namespace ?? ((object)mod).GetType().Name;
			}
			return text.Replace('.', '_').Replace(' ', '_');
		}
	}
	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);
			}
			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)
			{
				return null;
			}
			int num2 = body.IndexOf(':', num + key.Length + 2);
			if (num2 < 0)
			{
				return null;
			}
			int i;
			for (i = num2 + 1; i < body.Length && (body[i] == ' ' || body[i] == '\t'); i++)
			{
			}
			if (i >= body.Length)
			{
				return null;
			}
			if (body[i] == '"')
			{
				int num3 = body.IndexOf('"', i + 1);
				if (num3 < 0)
				{
					return null;
				}
				return body.Substring(i + 1, num3 - i - 1);
			}
			int j;
			for (j = i; j < body.Length && body[j] != ',' && body[j] != '}' && body[j] != ']'; j++)
			{
			}
			return body.Substring(i, j - i).Trim();
		}

		private static int ExtractInt(string body, string key)
		{
			if (!int.TryParse(ExtractField(body, key), out var result))
			{
				return 0;
			}
			return result;
		}
	}
	internal static class SnitchServer
	{
		private sealed class Session
		{
			internal readonly WebSocket Ws;

			internal readonly SemaphoreSlim Gate = new SemaphoreSlim(1, 1);

			internal Session(WebSocket ws)
			{
				Ws = ws;
			}
		}

		private const int MaxClients = 8;

		private const int SendTimeoutMs = 10000;

		private static HttpListener _listener;

		private static Thread _accept;

		private static volatile bool _running;

		private static CancellationTokenSource _cts;

		private static int _port;

		private static string _token = "";

		private static string[] _origins = Array.Empty<string>();

		private static readonly List<Session> _sockets = new List<Session>();

		private static readonly object _lock = new object();

		private static readonly ConcurrentQueue<Action> _mainQueue = new ConcurrentQueue<Action>();

		internal static bool Running => _running;

		internal static int Port => _port;

		internal static int SocketCount
		{
			get
			{
				lock (_lock)
				{
					return _sockets.Count;
				}
			}
		}

		internal static void Start(int port, string token, string allowedOrigins)
		{
			if (_running)
			{
				return;
			}
			_port = port;
			_token = token ?? "";
			_origins = ParseOrigins(allowedOrigins);
			try
			{
				_listener = new HttpListener();
				_listener.Prefixes.Add($"http://127.0.0.1:{port}/");
				_listener.Start();
				_running = true;
				_cts = new CancellationTokenSource();
				_accept = new Thread(AcceptLoop)
				{
					IsBackground = true,
					Name = "Snitch-Server"
				};
				_accept.Start();
				Instance log = Core.Log;
				if (log != null)
				{
					log.Msg($"[snitch] data server on http://127.0.0.1:{port}/ (ws://127.0.0.1:{port}/stream). token {((_token.Length > 0) ? "on" : "off")}.");
				}
			}
			catch (Exception ex)
			{
				_running = false;
				Instance log2 = Core.Log;
				if (log2 != null)
				{
					log2.Error($"[snitch] data server failed to start on {port}: {ex.Message} (port in use? change ServerPort)");
				}
			}
		}

		internal static void Stop()
		{
			_running = false;
			try
			{
				_cts?.Cancel();
			}
			catch
			{
			}
			lock (_lock)
			{
				foreach (Session socket in _sockets)
				{
					try
					{
						socket.Ws.Abort();
					}
					catch
					{
					}
					try
					{
						socket.Gate.Dispose();
					}
					catch
					{
					}
				}
				_sockets.Clear();
			}
			try
			{
				_listener?.Stop();
				_listener?.Close();
			}
			catch
			{
			}
			try
			{
				_cts?.Dispose();
			}
			catch
			{
			}
			_cts = null;
			_listener = null;
		}

		internal static void Pump()
		{
			int num = 0;
			Action result;
			while (num++ < 8 && _mainQueue.TryDequeue(out result))
			{
				try
				{
					result();
				}
				catch (Exception ex)
				{
					Instance log = Core.Log;
					if (log != null)
					{
						log.Warning("[snitch] control failed: " + ex.Message);
					}
				}
			}
		}

		internal static void Broadcast(string json)
		{
			if (_running && !string.IsNullOrEmpty(json))
			{
				byte[] bytes = Encoding.UTF8.GetBytes(json);
				Session[] array;
				lock (_lock)
				{
					array = _sockets.ToArray();
				}
				for (int i = 0; i < array.Length; i++)
				{
					SendFireAndForget(array[i], bytes);
				}
			}
		}

		private static void SendFireAndForget(Session s, byte[] bytes)
		{
			if (s.Ws.State != WebSocketState.Open)
			{
				Remove(s);
			}
			else if (s.Gate.Wait(0))
			{
				SendAndRelease(s, bytes);
			}
		}

		private static async Task SendAndRelease(Session s, byte[] bytes)
		{
			try
			{
				await SendWithTimeout(s.Ws, bytes, _cts?.Token ?? CancellationToken.None).ConfigureAwait(continueOnCapturedContext: false);
			}
			catch
			{
				Remove(s);
				try
				{
					s.Ws.Abort();
				}
				catch
				{
				}
			}
			finally
			{
				try
				{
					s.Gate.Release();
				}
				catch
				{
				}
			}
		}

		private static void Remove(Session s)
		{
			lock (_lock)
			{
				_sockets.Remove(s);
			}
		}

		private static void AcceptLoop()
		{
			while (_running)
			{
				HttpListenerContext ctx;
				try
				{
					ctx = _listener.GetContext();
				}
				catch
				{
					if (!_running)
					{
						break;
					}
					continue;
				}
				Task.Run(() => HandleAsync(ctx));
			}
		}

		private static async Task HandleAsync(HttpListenerContext ctx)
		{
			try
			{
				HttpListenerRequest request = ctx.Request;
				HttpListenerResponse response = ctx.Response;
				string origin = request.Headers["Origin"];
				ApplyCors(response, origin);
				if (request.HttpMethod == "OPTIONS")
				{
					response.StatusCode = 204;
					response.Close();
					return;
				}
				string text = request.Url.AbsolutePath.ToLowerInvariant();
				if (request.IsWebSocketRequest)
				{
					if (!OriginAllowed(origin) || !TokenOk(request))
					{
						response.StatusCode = 403;
						response.Close();
					}
					else
					{
						await HandleWsAsync(ctx).ConfigureAwait(continueOnCapturedContext: false);
					}
					return;
				}
				switch (text)
				{
				case "/health":
					WriteJson(response, WireProtocol.BuildHealth(SnitchCore.LastFrame, SnitchCore.LastScene, LanServer.LanInfoJson(includeToken: true)));
					break;
				case "/snapshot":
					if (!TokenOk(request))
					{
						response.StatusCode = 401;
						response.Close();
					}
					else
					{
						WriteJson(response, SnitchCore.LatestJson ?? "{\"type\":\"snapshot\",\"v\":1,\"frame\":{},\"sections\":[],\"counters\":[],\"states\":[],\"panels\":[],\"logs\":{\"timeline\":[]}}");
					}
					break;
				case "/caps":
					if (!TokenOk(request))
					{
						response.StatusCode = 401;
						response.Close();
					}
					else
					{
						WriteJson(response, SnitchCore.CapsJson ?? WireProtocol.BuildCaps());
					}
					break;
				case "/control":
					if (!TokenOk(request))
					{
						response.StatusCode = 401;
						response.Close();
					}
					else
					{
						HandleControl(request, response);
					}
					break;
				default:
					ServeStatic(text, response);
					break;
				}
			}
			catch (Exception ex)
			{
				Instance log = Core.Log;
				if (log != null)
				{
					log.Warning("[snitch] request error: " + ex.Message);
				}
				try
				{
					ctx.Response.Abort();
				}
				catch
				{
				}
			}
		}

		private static void HandleControl(HttpListenerRequest req, HttpListenerResponse res)
		{
			string body = "";
			try
			{
				using StreamReader streamReader = new StreamReader(req.InputStream, Encoding.UTF8);
				body = streamReader.ReadToEnd();
			}
			catch
			{
			}
			string cmd = req.QueryString["cmd"] ?? ExtractField(body, "cmd");
			string id = req.QueryString["id"] ?? ExtractField(body, "id");
			string value = req.QueryString["value"] ?? ExtractField(body, "value");
			WriteJson(res, ApplyControl(cmd, id, value));
		}

		internal static string ApplyControl(string cmd, string id, string value)
		{
			cmd = (cmd ?? "").Trim().ToLowerInvariant();
			switch (cmd)
			{
			case "start":
				_mainQueue.Enqueue(SnitchCore.Start);
				break;
			case "stop":
				_mainQueue.Enqueue(SnitchCore.Stop);
				break;
			case "reset":
				_mainQueue.Enqueue(delegate
				{
					SnitchCore.Stop();
					SnitchCore.Start();
				});
				break;
			case "report":
				_mainQueue.Enqueue(delegate
				{
					try
					{
						ReportWriter.Write("all");
					}
					catch (Exception ex)
					{
						Instance log = Core.Log;
						if (log != null)
						{
							log.Warning("[snitch] report failed: " + ex.Message);
						}
					}
				});
				break;
			case "action":
				if (string.IsNullOrEmpty(id))
				{
					return "{\"ok\":false,\"error\":\"missing id\"}";
				}
				_mainQueue.Enqueue(delegate
				{
					PanelRegistry.Invoke(id);
				});
				break;
			case "toggle":
			{
				if (string.IsNullOrEmpty(id))
				{
					return "{\"ok\":false,\"error\":\"missing id\"}";
				}
				bool val = value == "true" || value == "1" || value == "on";
				_mainQueue.Enqueue(delegate
				{
					PanelRegistry.SetToggle(id, val);
				});
				break;
			}
			case "slider":
			{
				if (string.IsNullOrEmpty(id))
				{
					return "{\"ok\":false,\"error\":\"missing id\"}";
				}
				if (!double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var sv))
				{
					return "{\"ok\":false,\"error\":\"bad value\"}";
				}
				_mainQueue.Enqueue(delegate
				{
					PanelRegistry.SetSlider(id, sv);
				});
				break;
			}
			default:
				return "{\"ok\":false,\"error\":\"unknown cmd\"}";
			}
			return "{\"ok\":true,\"cmd\":\"" + cmd + "\"}";
		}

		private static async Task HandleWsAsync(HttpListenerContext ctx)
		{
			lock (_lock)
			{
				if (_sockets.Count >= 8)
				{
					try
					{
						ctx.Response.StatusCode = 503;
						ctx.Response.Close();
						return;
					}
					catch
					{
						return;
					}
				}
			}
			WebSocket ws;
			try
			{
				ws = (await ctx.AcceptWebSocketAsync(null).ConfigureAwait(continueOnCapturedContext: false)).WebSocket;
			}
			catch (Exception ex)
			{
				Instance log = Core.Log;
				if (log != null)
				{
					log.Warning("[snitch] ws upgrade failed: " + ex.Message);
				}
				return;
			}
			CancellationToken ct = _cts?.Token ?? CancellationToken.None;
			Session session = new Session(ws);
			try
			{
				string latestJson = SnitchCore.LatestJson;
				if (!string.IsNullOrEmpty(latestJson))
				{
					await SendWithTimeout(ws, Encoding.UTF8.GetBytes(latestJson), ct).ConfigureAwait(continueOnCapturedContext: false);
				}
			}
			catch
			{
			}
			lock (_lock)
			{
				_sockets.Add(session);
			}
			byte[] buf = new byte[4096];
			try
			{
				while (!ct.IsCancellationRequested && ws.State == WebSocketState.Open && (await ws.ReceiveAsync(new ArraySegment<byte>(buf), ct).ConfigureAwait(continueOnCapturedContext: false)).MessageType != WebSocketMessageType.Close)
				{
				}
			}
			catch
			{
			}
			finally
			{
				Remove(session);
				try
				{
					ws.Abort();
				}
				catch
				{
				}
				try
				{
					session.Gate.Dispose();
				}
				catch
				{
				}
			}
		}

		private static async Task SendWithTimeout(WebSocket ws, byte[] bytes, CancellationToken serverCt)
		{
			using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(serverCt);
			cts.CancelAfter(10000);
			await ws.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, endOfMessage: true, cts.Token).ConfigureAwait(continueOnCapturedContext: false);
		}

		private static void ServeStatic(string path, HttpListenerResponse res)
		{
			if (WebAssets.TryResolve(path, out var bytes, out var contentType))
			{
				try
				{
					res.StatusCode = 200;
					res.ContentType = contentType;
					res.ContentLength64 = bytes.Length;
					res.OutputStream.Write(bytes, 0, bytes.Length);
					res.OutputStream.Close();
					res.Close();
					return;
				}
				catch
				{
					try
					{
						res.StatusCode = 500;
						res.Close();
						return;
					}
					catch
					{
						return;
					}
				}
			}
			if (path == "/" || string.IsNullOrEmpty(path) || path == "/index.html")
			{
				WriteHtml(res, PlaceholderHtml());
				return;
			}
			res.StatusCode = 404;
			res.Close();
		}

		private static void ApplyCors(HttpListenerResponse res, string origin)
		{
			try
			{
				res.Headers["Access-Control-Allow-Origin"] = ((!OriginAllowed(origin)) ? "null" : (string.IsNullOrEmpty(origin) ? "*" : origin));
				res.Headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS";
				res.Headers["Access-Control-Allow-Headers"] = "content-type, x-snitch-token";
				res.Headers["Access-Control-Allow-Private-Network"] = "true";
				res.Headers["Vary"] = "Origin";
			}
			catch
			{
			}
		}

		private static bool OriginAllowed(string origin)
		{
			if (string.IsNullOrEmpty(origin))
			{
				return true;
			}
			if (origin.StartsWith("http://localhost", StringComparison.OrdinalIgnoreCase) || origin.StartsWith("http://127.0.0.1", StringComparison.OrdinalIgnoreCase) || origin.StartsWith("https://localhost", StringComparison.OrdinalIgnoreCase))
			{
				return true;
			}
			for (int i = 0; i < _origins.Length; i++)
			{
				if (string.Equals(_origins[i], origin, StringComparison.OrdinalIgnoreCase))
				{
					return true;
				}
			}
			return false;
		}

		private static bool TokenOk(HttpListenerRequest req)
		{
			if (_token.Length == 0)
			{
				return true;
			}
			string text = req.Headers["x-snitch-token"];
			if (string.IsNullOrEmpty(text))
			{
				text = req.QueryString["token"];
			}
			return text == _token;
		}

		private static string[] ParseOrigins(string csv)
		{
			if (string.IsNullOrWhiteSpace(csv))
			{
				return Array.Empty<string>();
			}
			string[] array = csv.Split(',');
			List<string> list = new List<string>(array.Length);
			string[] array2 = array;
			for (int i = 0; i < array2.Length; i++)
			{
				string text = array2[i].Trim().TrimEnd('/');
				if (text.Length > 0)
				{
					list.Add(text);
				}
			}
			return list.ToArray();
		}

		private static void WriteJson(HttpListenerResponse res, string json)
		{
			Write(res, "application/json", Encoding.UTF8.GetBytes(json));
		}

		private static void WriteHtml(HttpListenerResponse res, string html)
		{
			Write(res, "text/html; charset=utf-8", Encoding.UTF8.GetBytes(html));
		}

		private static void Write(HttpListenerResponse res, string type, byte[] bytes)
		{
			try
			{
				res.StatusCode = 200;
				res.ContentType = type;
				res.ContentLength64 = bytes.Length;
				res.OutputStream.Write(bytes, 0, bytes.Length);
				res.OutputStream.Close();
			}
			catch
			{
			}
			finally
			{
				try
				{
					res.Close();
				}
				catch
				{
				}
			}
		}

		private static string ExtractField(string body, string key)
		{
			if (string.IsNullOrEmpty(body))
			{
				return null;
			}
			int num = body.IndexOf("\"" + key + "\"", StringComparison.OrdinalIgnoreCase);
			if (num < 0)
			{
				return null;
			}
			int num2 = body.IndexOf(':', num + key.Length + 2);
			if (num2 < 0)
			{
				return null;
			}
			int i;
			for (i = num2 + 1; i < body.Length && (body[i] == ' ' || body[i] == '\t'); i++)
			{
			}
			if (i >= body.Length)
			{
				return null;
			}
			if (body[i] == '"')
			{
				int num3 = body.IndexOf('"', i + 1);
				if (num3 < 0)
				{
					return null;
				}
				return body.Substring(i + 1, num3 - i - 1);
			}
			int j;
			for (j = i; j < body.Length && body[j] != ',' && body[j] != '}' && body[j] != ']'; j++)
			{
			}
			return body.Substring(i, j - i).Trim();
		}

		private static string PlaceholderHtml()
		{
			return "<!doctype html><html><head><meta charset=utf-8><title>Snitch</title><style>body{font:14px system-ui;background:#0b0e14;color:#cdd6f4;padding:40px;max-width:640px;margin:auto}code{background:#1e2230;padding:2px 6px;border-radius:4px}a{color:#89b4fa}</style></head><body><h1>Snitch data server</h1><p>This loopback endpoint is live. The offline dashboard isn't bundled in this build yet.</p><p>Open the hosted dashboard and it will auto-connect to <code>ws://127.0.0.1:" + _port + "/stream</code>, or fetch <code>/snapshot</code> / <code>/health</code> / <code>/caps</code> directly.</p><p>Support: <a href=\"https://support.doodesch.de\">support.doodesch.de</a></p></body></html>";
		}
	}
	internal static class WebAssets
	{
		private static string _wwwroot;

		internal static string Wwwroot => _wwwroot ?? (_wwwroot = Path.Combine(Directory.GetCurrentDirectory(), "Mods", "Snitch", "wwwroot"));

		internal static bool HasBundledDashboard()
		{
			try
			{
				return File.Exists(Path.Combine(Wwwroot, "index.html"));
			}
			catch
			{
				return false;
			}
		}

		internal static bool TryResolve(string path, out byte[] bytes, out string contentType)
		{
			bytes = null;
			contentType = null;
			if (string.IsNullOrEmpty(path) || path == "/")
			{
				path = "/index.html";
			}
			string wwwroot = Wwwroot;
			string path2 = path.TrimStart('/').Replace('/', Path.DirectorySeparatorChar);
			string fullPath;
			try
			{
				fullPath = Path.GetFullPath(Path.Combine(wwwroot, path2));
			}
			catch
			{
				return false;
			}
			if (!fullPath.StartsWith(wwwroot, StringComparison.OrdinalIgnoreCase) || !File.Exists(fullPath))
			{
				return false;
			}
			try
			{
				bytes = File.ReadAllBytes(fullPath);
				contentType = ContentType(fullPath);
				return true;
			}
			catch
			{
				return false;
			}
		}

		internal static string ContentType(string path)
		{
			return Path.GetExtension(path).ToLowerInvariant() switch
			{
				".html" => "text/html; charset=utf-8", 
				".js" => "text/javascript", 
				".css" => "text/css", 
				".json" => "application/json", 
				".svg" => "image/svg+xml", 
				".png" => "image/png", 
				".ico" => "image/x-icon", 
				".woff2" => "font/woff2", 
				_ => "application/octet-stream", 
			};
		}
	}
	internal static class WireProtocol
	{
		internal const int Version = 1;

		internal const string CapsArray = "[\"panels\",\"logs\",\"phone-remote\"]";

		private static readonly CultureInfo Inv = CultureInfo.InvariantCulture;

		internal static string BuildSnapshot(int frame, string scene)
		{
			FrameStats latestFrame = SnitchCore.LatestFrame;
			StringBuilder stringBuilder = new StringBuilder(8192);
			stringBuilder.Append("{\"type\":\"snapshot\",\"v\":").Append(1).Append(",\"t\":")
				.Append(frame)
				.Append(',');
			stringBuilder.Append("\"meta\":{\"mod\":\"Snitch\",\"version\":\"1.5.1\",\"scene\":\"").Append(Esc(scene)).Append("\",\"active\":")
				.Append(SnitchCore.Active ? "true" : "false")
				.Append(",\"caps\":")
				.Append("[\"panels\",\"logs\",\"phone-remote\"]")
				.Append("},");
			stringBuilder.Append("\"frame\":{");
			Num(stringBuilder, "meanMs", latestFrame.MeanMs);
			Num(stringBuilder, "medianMs", latestFrame.MedianMs);
			Num(stringBuilder, "p95Ms", latestFrame.P95Ms);
			Num(stringBuilder, "p99Ms", latestFrame.P99Ms);
			Num(stringBuilder, "minMs", latestFrame.MinMs);
			Num(stringBuilder, "maxMs", latestFrame.MaxMs);
			Num(stringBuilder, "meanFps", latestFrame.MeanFps);
			Num(stringBuilder, "minFps", latestFrame.MinFps);
			Num(stringBuilder, "gc0", latestFrame.Gc0Per1000);
			Num(stringBuilder, "gc1", latestFrame.Gc1Per1000);
			stringBuilder.Append("\"samples\":").Append(latestFrame.Samples).Append("},");
			stringBuilder.Append("\"sections\":[");
			List<SectionRow> latestSections = SnitchCore.LatestSections;
			if (latestSections != null)
			{
				for (int i = 0; i < latestSections.Count; i++)
				{
					if (i > 0)
					{
						stringBuilder.Append(',');
					}
					SectionRow sectionRow = latestSections[i];
					stringBuilder.Append("{\"group\":\"").Append(Esc(sectionRow.Group)).Append("\",\"label\":\"")
						.Append(Esc(sectionRow.Label))
						.Append("\",");
					Num(stringBuilder, "ms", sectionRow.MsPerFrame);
					Num(stringBuilder, "max", sectionRow.MaxMs);
					Num(stringBuilder, "calls", sectionRow.Calls);
					stringBuilder.Append("\"pct\":").Append(F(sectionRow.PctFrame)).Append('}');
				}
			}
			stringBuilder.Append("],");
			stringBuilder.Append("\"counters\":[");
			List<CounterRow> latestCounters = SnitchCore.LatestCounters;
			if (latestCounters != null)
			{
				for (int j = 0; j < latestCounters.Count; j++)
				{
					if (j > 0)
					{
						stringBuilder.Append(',');
					}
					CounterRow counterRow = latestCounters[j];
					stringBuilder.Append("{\"id\":\"").Append(Esc(counterRow.Id)).Append("\",\"value\":")
						.Append(F(counterRow.Value))
						.Append(",\"unit\":\"")
						.Append(Esc(counterRow.Unit))
						.Append("\",\"state\":\"")
						.Append(Esc(counterRow.State))
						.Append("\"}");
				}
			}
			stringBuilder.Append("],");
			stringBuilder.Append("\"states\":[");
			List<StateSnapshot> latestStates = SnitchCore.LatestStates;
			if (latestStates != null)
			{
				for (int k = 0; k < latestStates.Count; k++)
				{
					if (k > 0)
					{
						stringBuilder.Append(',');
					}
					StateSnapshot stateSnapshot = latestStates[k];
					stringBuilder.Append("{\"id\":\"").Append(Esc(stateSnapshot.Id)).Append("\",\"title\":\"")
						.Append(Esc(stateSnapshot.Title))
						.Append("\",\"total\":")
						.Append(stateSnapshot.EffectiveTotal())
						.Append(",\"buckets\":[");
					for (int l = 0; l < stateSnapshot.Buckets.Count; l++)
					{
						if (l > 0)
						{
							stringBuilder.Append(',');
						}
						stringBuilder.Append("{\"name\":\"").Append(Esc(stateSnapshot.Buckets[l].Name)).Append("\",\"count\":")
							.Append(stateSnapshot.Buckets[l].Count)
							.Append('}');
					}
					stringBuilder.Append("]}");
				}
			}
			stringBuilder.Append("],");
			AppendPanels(stringBuilder);
			stringBuilder.Append(',');
			AppendLogs(stringBuilder);
			stringBuilder.Append('}');
			return stringBuilder.ToString();
		}

		private static void AppendPanels(StringBuilder sb)
		{
			sb.Append("\"panels\":[");
			IReadOnlyList<PanelModel> all = PanelRegistry.All;
			bool flag = true;
			for (int i = 0; i < all.Count; i++)
			{
				PanelModel panelModel = all[i];
				if (!flag)
				{
					sb.Append(',');
				}
				flag = false;
				sb.Append("{\"id\":\"").Append(Esc(panelModel.Id)).Append("\",\"title\":\"")
					.Append(Esc(panelModel.Title))
					.Append("\",\"hasLog\":")
					.Append(panelModel.HasLog ? "true" : "false")
					.Append(",\"text\":\"");
				sb.Append(Esc(EvalText(panelModel))).Append("\",\"actions\":[");
				for (int j = 0; j < panelModel.Actions.Count; j++)
				{
					if (j > 0)
					{
						sb.Append(',');
					}
					sb.Append("{\"id\":\"").Append(Esc(panelModel.Actions[j].Id)).Append("\",\"label\":\"")
						.Append(Esc(panelModel.Actions[j].Label))
						.Append("\"}");
				}
				sb.Append("],\"toggles\":[");
				for (int k = 0; k < panelModel.Toggles.Count; k++)
				{
					if (k > 0)
					{
						sb.Append(',');
					}
					bool flag2 = false;
					try
					{
						flag2 = panelModel.Toggles[k].Get != null && panelModel.Toggles[k].Get();
					}
					catch
					{
					}
					sb.Append("{\"id\":\"").Append(Esc(panelModel.Toggles[k].Id)).Append("\",\"label\":\"")
						.Append(Esc(panelModel.Toggles[k].Label))
						.Append("\",\"value\":")
						.Append(flag2 ? "true" : "false")
						.Append('}');
				}
				sb.Append("],\"sliders\":[");
				for (int l = 0; l < panelModel.Sliders.Count; l++)
				{
					if (l > 0)
					{
						sb.Append(',');
					}
					SliderItem sliderItem = panelModel.Sliders[l];
					sb.Append("{\"id\":\"").Append(Esc(sliderItem.Id)).Append("\",\"label\":\"")
						.Append(Esc(sliderItem.Label))
						.Append("\",\"unit\":\"")
						.Append(Esc(sliderItem.Unit))
						.Append("\",\"min\":")
						.Append(F(sliderItem.Min))
						.Append(",\"max\":")
						.Append(F(sliderItem.Max))
						.Append(",\"step\":")
						.Append(F(sliderItem.Step))
						.Append(",\"value\":")
						.Append(F(sliderItem.Read()))
						.Append('}');
				}
				sb.Append("]}");
			}
			sb.Append(']');
		}

		private static string EvalText(PanelModel p)
		{
			if (p.Texts.Count == 0)
			{
				return "";
			}
			StringBuilder stringBuilder = new StringBuilder(128);
			for (int i = 0; i < p.Texts.Count; i++)
			{
				string value = null;
				try
				{
					value = p.Texts[i]?.Invoke();
				}
				catch
				{
				}
				if (!string.IsNullOrEmpty(value))
				{
					if (stringBuilder.Length > 0)
					{
						stringBuilder.Append('\n');
					}
					stringBuilder.Append(value);
				}
			}
			return stringBuilder.ToString();
		}

		private static void AppendLogs(StringBuilder sb)
		{
			sb.Append("\"logs\":{\"timeline\":[");
			List<LogEntry> list = LogHub.Timeline(200);
			for (int i = 0; i < list.Count; i++)
			{
				if (i > 0)
				{
					sb.Append(',');
				}
				LogEntry logEntry = list[i];
				sb.Append("{\"seq\":").Append(logEntry.Seq).Append(",\"t\":\"")
					.Append(Esc(logEntry.Time))
					.Append("\",\"ch\":\"")
					.Append(Esc(logEntry.Ch))
					.Append("\",\"lvl\":")
					.Append(logEntry.Lvl)
					.Append(",\"msg\":\"")
					.Append(Esc(logEntry.Msg))
					.Append("\"}");
			}
			sb.Append("]}");
		}

		internal static string BuildHealth(int frame, string scene, string lanJson = null)
		{
			string text = "{\"ok\":true,\"mod\":\"Snitch\",\"version\":\"1.5.1\",\"caps\":[\"panels\",\"logs\",\"phone-remote\"],\"active\":" + (SnitchCore.Active ? "true" : "false") + ",\"scene\":\"" + Esc(scene) + "\",\"frame\":" + frame;
			if (!string.IsNullOrEmpty(lanJson))
			{
				text = text + "," + lanJson;
			}
			return text + "}";
		}

		internal static string BuildCaps()
		{
			return "{\"type\":\"caps\",\"v\":" + 1 + ",\"frameTime\":\"load-bearing\",\"gc\":\"load-bearing\",\"engineCounters\":\"unavailable\",\"perEntityAttribution\":\"viable\",\"note\":\"ProfilerRecorder is inert in this IL2CPP build; frame-time + GC are the truth. Per-entity vanilla cost attribution is viable. Causal subsystem cost uses the ablation stabili