Decompiled source of Snitch v1.0.2

Snitch.dll

Decompiled a day 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.WebSockets;
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using HarmonyLib;
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.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.0.2", "DooDesch", "https://github.com/DooDesch-Mods/ScheduleOne-Snitch")]
[assembly: MelonGame("TVGS", "Schedule I")]
[assembly: MelonOptionalDependencies(new string[] { "ModManager&PhoneApp" })]
[assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")]
[assembly: AssemblyCompany("Snitch")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.2.0")]
[assembly: AssemblyInformationalVersion("1.0.2+c0e4bda18e7560435ec60f3e79d62ca9be6667d5")]
[assembly: AssemblyProduct("Snitch")]
[assembly: AssemblyTitle("Snitch")]
[assembly: NeutralResourcesLanguage("en-US")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.0.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 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();
			BridgeHost.Install();
			try
			{
				((MelonBase)this).HarmonyInstance.PatchAll();
			}
			catch (Exception ex)
			{
				Log.Warning("[Snitch] Harmony patch failed: " + ex.Message);
			}
			if (Preferences.Enabled && Preferences.ServerEnabled)
			{
				SnitchServer.Start(Preferences.ServerPort, Preferences.ServerToken, Preferences.AllowedOrigins);
			}
			Log.Msg("Snitch v1.0.2 - 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();
				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();
			}
		}

		public override void OnGUI()
		{
			if (_inWorld && SnitchCore.Active && Preferences.ShowHud)
			{
				ProfilerHud.Draw();
			}
		}

		public override void OnApplicationQuit()
		{
			SnitchServer.Stop();
		}

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

		private static string _lastSig = "";

		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;
			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 "hud":
					Preferences.SetShowHud(BoolArg(p, 2, !Preferences.ShowHud));
					Log("HUD = " + Preferences.ShowHud);
					break;
				case "vanilla":
					Vanilla(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: start | stop | status | frame | top [n] | sections | states [id] | counters | hud [on|off] | vanilla [on|off] | ablate <lever> | levers | report [md|csv|all]");
		}

		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} hud={Preferences.ShowHud} 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 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 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);
			}
		}
	}
	[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
	{
		private static GUIStyle _box;

		private static string _cached = "";

		private static float _nextRebuild;

		internal static void Draw()
		{
			//IL_0011: Unknown result type (might be due to invalid IL or missing references)
			//IL_0016: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0025: Unknown result type (might be due to invalid IL or missing references)
			//IL_002c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0031: Unknown result type (might be due to invalid IL or missing references)
			//IL_003b: Expected O, but got Unknown
			//IL_0040: Expected O, but got Unknown
			//IL_004a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0084: Unknown result type (might be due to invalid IL or missing references)
			//IL_008e: Expected O, but got Unknown
			//IL_0089: Unknown result type (might be due to invalid IL or missing references)
			//IL_008e: Unknown result type (might be due to invalid IL or missing references)
			//IL_009e: Unknown result type (might be due to invalid IL or missing references)
			//IL_00af: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bb: Unknown result type (might be due to invalid IL or missing references)
			if (_box == null)
			{
				_box = new GUIStyle(GUI.skin.box)
				{
					alignment = (TextAnchor)0,
					fontSize = 12,
					richText = true,
					padding = new RectOffset(8, 8, 6, 6)
				};
				_box.normal.textColor = Color.white;
			}
			if (Time.unscaledTime >= _nextRebuild)
			{
				_cached = Build();
				_nextRebuild = Time.unscaledTime + 0.1f;
			}
			Vector2 val = _box.CalcSize(new GUIContent(_cached));
			GUI.Box(new Rect(8f, 8f, Mathf.Max(280f, val.x + 6f), val.y + 6f), _cached, _box);
		}

		private static string Build()
		{
			FrameStats latestFrame = SnitchCore.LatestFrame;
			StringBuilder stringBuilder = new StringBuilder(512);
			string value = ((latestFrame.MeanFps >= 50.0) ? "#5f5" : ((latestFrame.MeanFps >= 30.0) ? "#fd5" : "#f55"));
			stringBuilder.Append("<b>Snitch</b>   <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"))
				.Append('\n');
			List<SectionRow> latestSections = SnitchCore.LatestSections;
			if (latestSections != null && latestSections.Count > 0)
			{
				stringBuilder.Append("<b>sections</b>\n");
				int num = Mathf.Min(6, latestSections.Count);
				for (int i = 0; i < num; i++)
				{
					SectionRow sectionRow = latestSections[i];
					stringBuilder.Append(sectionRow.Label).Append("   ").Append(sectionRow.MsPerFrame.ToString("F2"))
						.Append(" ms  ")
						.Append(sectionRow.PctFrame.ToString("F0"))
						.Append("%\n");
				}
			}
			List<StateSnapshot> latestStates = SnitchCore.LatestStates;
			if (latestStates != null && latestStates.Count > 0)
			{
				stringBuilder.Append("<b>states</b>\n");
				foreach (StateSnapshot item in latestStates)
				{
					stringBuilder.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);
					}
					stringBuilder.Append('\n');
				}
			}
			return stringBuilder.ToString().TrimEnd();
		}
	}
}
namespace Snitch.Server
{
	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 string _wwwroot;

		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 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);
			_wwwroot = Path.Combine(Directory.GetCurrentDirectory(), "Mods", "Snitch", "wwwroot");
			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));
					break;
				case "/snapshot":
					if (!TokenOk(request))
					{
						response.StatusCode = 401;
						response.Close();
					}
					else
					{
						WriteJson(response, SnitchCore.LatestJson ?? "{\"type\":\"snapshot\",\"v\":1,\"frame\":{},\"sections\":[],\"counters\":[],\"states\":[]}");
					}
					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 text = req.QueryString["cmd"];
			if (string.IsNullOrEmpty(text))
			{
				try
				{
					using StreamReader streamReader = new StreamReader(req.InputStream, Encoding.UTF8);
					text = ExtractCmd(streamReader.ReadToEnd());
				}
				catch
				{
				}
			}
			text = (text ?? "").Trim().ToLowerInvariant();
			switch (text)
			{
			case "start":
				_mainQueue.Enqueue(SnitchCore.Start);
				break;
			case "stop":
				_mainQueue.Enqueue(SnitchCore.Stop);
				break;
			case "reset":
				_mainQueue.Enqueue(delegate
				{
					SnitchCore.Stop();
					SnitchCore.Start();
				});
				break;
			default:
				WriteJson(res, "{\"ok\":false,\"error\":\"unknown cmd\"}");
				return;
			}
			WriteJson(res, "{\"ok\":true,\"cmd\":\"" + text + "\"}");
		}

		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 (path == "/" || string.IsNullOrEmpty(path))
			{
				path = "/index.html";
			}
			string path2 = path.TrimStart('/').Replace('/', Path.DirectorySeparatorChar);
			string text = ((_wwwroot != null) ? Path.GetFullPath(Path.Combine(_wwwroot, path2)) : null);
			if (text == null || !text.StartsWith(_wwwroot, StringComparison.OrdinalIgnoreCase) || !File.Exists(text))
			{
				if (path == "/index.html")
				{
					WriteHtml(res, PlaceholderHtml());
					return;
				}
				res.StatusCode = 404;
				res.Close();
				return;
			}
			try
			{
				byte[] array = File.ReadAllBytes(text);
				res.StatusCode = 200;
				res.ContentType = ContentType(text);
				res.ContentLength64 = array.Length;
				res.OutputStream.Write(array, 0, array.Length);
				res.OutputStream.Close();
				res.Close();
			}
			catch
			{
				try
				{
					res.StatusCode = 500;
					res.Close();
				}
				catch
				{
				}
			}
		}

		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 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", 
				".woff2" => "font/woff2", 
				_ => "application/octet-stream", 
			};
		}

		private static string ExtractCmd(string body)
		{
			int num = body.IndexOf("\"cmd\"", StringComparison.OrdinalIgnoreCase);
			if (num < 0)
			{
				return null;
			}
			int num2 = body.IndexOf(':', num);
			if (num2 < 0)
			{
				return null;
			}
			int num3 = body.IndexOf('"', num2 + 1);
			if (num3 < 0)
			{
				return null;
			}
			int num4 = body.IndexOf('"', num3 + 1);
			if (num4 < 0)
			{
				return null;
			}
			return body.Substring(num3 + 1, num4 - num3 - 1);
		}

		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 WireProtocol
	{
		internal const int Version = 1;

		private static readonly CultureInfo Inv = CultureInfo.InvariantCulture;

		internal static string BuildSnapshot(int frame, string scene)
		{
			FrameStats latestFrame = SnitchCore.LatestFrame;
			StringBuilder stringBuilder = new StringBuilder(2048);
			stringBuilder.Append("{\"type\":\"snapshot\",\"v\":").Append(1).Append(",\"t\":")
				.Append(frame)
				.Append(',');
			stringBuilder.Append("\"meta\":{\"mod\":\"Snitch\",\"version\":\"1.0.2\",\"scene\":\"").Append(Esc(scene)).Append("\",\"active\":")
				.Append(SnitchCore.Active ? "true" : "false")
				.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("]}");
			return stringBuilder.ToString();
		}

		internal static string BuildHealth(int frame, string scene)
		{
			return "{\"ok\":true,\"mod\":\"Snitch\",\"version\":\"1.0.2\",\"active\":" + (SnitchCore.Active ? "true" : "false") + ",\"scene\":\"" + Esc(scene) + "\",\"frame\":" + frame + "}";
		}

		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 stability gate.\"}";
		}

		private static void Num(StringBuilder sb, string key, double v)
		{
			sb.Append('"').Append(key).Append("\":")
				.Append(F(v))
				.Append(',');
		}

		private static string F(double v)
		{
			if (double.IsNaN(v) || double.IsInfinity(v))
			{
				return "0";
			}
			return v.ToString("0.###", Inv);
		}

		private static string Esc(string s)
		{
			if (string.IsNullOrEmpty(s))
			{
				return "";
			}
			StringBuilder stringBuilder = null;
			for (int i = 0; i < s.Length; i++)
			{
				char c = s[i];
				if (c == '"' || c == '\\' || c < ' ')
				{
					if (stringBuilder == null)
					{
						stringBuilder = new StringBuilder(s.Length + 8);
						stringBuilder.Append(s, 0, i);
					}
					switch (c)
					{
					case '"':
						stringBuilder.Append("\\\"");
						continue;
					case '\\':
						stringBuilder.Append("\\\\");
						continue;
					}
					StringBuilder stringBuilder2 = stringBuilder.Append("\\u");
					int num = c;
					stringBuilder2.Append(num.ToString("x4"));
				}
				else
				{
					stringBuilder?.Append(c);
				}
			}
			return stringBuilder?.ToString() ?? s;
		}
	}
}
namespace Snitch.Sections
{
	internal struct SectionRow
	{
		public string Group;

		public string Label;

		public double MsPerFrame;

		public double MaxMs;

		public double Calls;

		public double PctFrame;
	}
	internal static class SectionProfiler
	{
		private sealed class Accumulator
		{
			public readonly string Label;

			public readonly string Group;

			public long TicksThisFrame;

			public int CallsThisFrame;

			public int Depth;

			public long StartTs;

			public readonly double[] MsRing = new double[120];

			public readonly int[] CallRing = new int[120];

			public int Head;

			public int Count;

			public Accumulator(string label)
			{
				Label = label;
				int num = label.IndexOf('.');
				Group = ((num > 0) ? label.Substring(0, num) : "(ungrouped)");
			}
		}

		private const int Window = 120;

		private static readonly Dictionary<string, int> _idByLabel = new Dictionary<string, int>(64);

		private static readonly List<Accumulator> _all = new List<Accumulator>(64);

		private static readonly double TickToMs = 1000.0 / (double)Stopwatch.Frequency;

		internal static int LabelCount => _all.Count;

		internal static int GetId(string label)
		{
			if (string.IsNullOrEmpty(label))
			{
				label = "(unnamed)";
			}
			if (_idByLabel.TryGetValue(label, out var value))
			{
				return value;
			}
			value = _all.Count;
			_all.Add(new Accumulator(label));
			_idByLabel[label] = value;
			return value;
		}

		internal static void Begin(int id)
		{
			if ((uint)id < (uint)_all.Count)
			{
				Accumulator accumulator = _all[id];
				if (accumulator.Depth++ == 0)
				{
					accumulator.StartTs = Stopwatch.GetTimestamp();
				}
			}
		}

		internal static void End(int id)
		{
			if ((uint)id < (uint)_all.Count)
			{
				Accumulator accumulator = _all[id];
				if (--accumulator.Depth == 0)
				{
					accumulator.TicksThisFrame += Stopwatch.GetTimestamp() - accumulator.StartTs;
					accumulator.CallsThisFrame++;
				}
				else if (accumulator.Depth < 0)
				{
					accumulator.Depth = 0;
				}
			}
		}

		internal static void Begin(string label)
		{
			Begin(GetId(label));
		}

		internal static void End(string label)
		{
			End(GetId(label));
		}

		internal static Scope Sample(string label)
		{
			int id = GetId(label);
			Begin(id);
			return new Scope(id);
		}

		internal static void Flush()
		{
			for (int i = 0; i < _all.Count; i++)
			{
				Accumulator accumulator = _all[i];
				accumulator.MsRing[accumulator.Head] = (double)accumulator.TicksThisFrame * TickToMs;
				accumulator.CallRing[accumulator.Head] = accumulator.CallsThisFrame;
				accumulator.Head = (accumulator.Head + 1) % 120;
				if (accumulator.Count < 120)
				{
					accumulator.Count++;
				}
				accumulator.TicksThisFrame = 0L;
				accumulator.CallsThisFrame = 0;
				accumulator.Depth = 0;
			}
		}

		internal static void Reset()
		{
			for (int i = 0; i < _all.Count; i++)
			{
				Accumulator accumulator = _all[i];
				accumulator.Head = 0;
				accumulator.Count = 0;
				accumulator.TicksThisFrame = 0L;
				accumulator.CallsThisFrame = 0;
				accumulator.Depth = 0;
			}
		}

		internal static List<SectionRow> Report(double frameMeanMs)
		{
			List<SectionRow> list = new List<SectionRow>(_all.Count);
			for (int i = 0; i < _all.Count; i++)
			{
				Accumulator accumulator = _all[i];
				if (accumulator.Count == 0)
				{
					continue;
				}
				double num = 0.0;
				double num2 = 0.0;
				long num3 = 0L;
				for (int j = 0; j < accumulator.Count; j++)
				{
					num += accumulator.MsRing[j];
					if (accumulator.MsRing[j] > num2)
					{
						num2 = accumulator.MsRing[j];
					}
					num3 += accumulator.CallRing[j];
				}
				double num4 = num / (double)accumulator.Count;
				if (!(num4 <= 0.0) || num3 != 0L)
				{
					list.Add(new SectionRow
					{
						Group = accumulator.Group,
						Label = accumulator.Label,
						MsPerFrame = num4,
						MaxMs = num2,
						Calls = (double)num3 / (double)accumulator.Count,
						PctFrame = ((frameMeanMs > 0.0) ? (num4 / frameMeanMs * 100.0) : 0.0)
					});
				}
			}
			list.Sort((SectionRow x, SectionRow y) => y.MsPerFrame.CompareTo(x.MsPerFrame));
			return list;
		}
	}
	internal readonly struct Scope : IDisposable
	{
		private readonly int _id;

		internal Scope(int id)
		{
			_id = id;
		}

		public void Dispose()
		{
			SectionProfiler.End(_id);
		}
	}
}
namespace Snitch.Reporting
{
	internal static class ReportWriter
	{
		private static readonly CultureInfo Inv = CultureInfo.InvariantCulture;

		internal static string Write(string fmt)
		{
			string text = Path.Combine(Directory.GetCurrentDirectory(), "Mods", "Snitch", "runs");
			Directory.CreateDirectory(text);
			string text2 = DateTime.Now.ToString("yyyyMMdd_HHmmss", Inv);
			List<string> list = new List<string>();
			if (fmt == "md" || fmt == "all")
			{
				string text3 = Path.Combine(text, "report_" + text2 + ".md");
				File.WriteAllText(text3, BuildMarkdown(), Encoding.UTF8);
				list.Add(text3);
			}
			if (fmt == "csv" || fmt == "all")
			{
				list.Add(WriteCsv(text, "sections_" + text2 + ".csv", SectionsCsv()));
				list.Add(WriteCsv(text, "counters_" + text2 + ".csv", CountersCsv()));
				list.Add(WriteCsv(text, "states_" + text2 + ".csv", StatesCsv()));
			}
			return string.Join(" | ", list);
		}

		private static string WriteCsv(string dir, string name, string content)
		{
			string text = Path.Combine(dir, name);
			File.WriteAllText(text, content, Encoding.UTF8);
			return text;
		}

		private static string BuildMarkdown()
		{
			FrameStats latestFrame = SnitchCore.LatestFrame;
			StringBuilder stringBuilder = new StringBuilder(4096);
			stringBuilder.AppendLine("# Snitch profiler report");
			stringBuilder.AppendLine();
			stringBuilder.AppendLine("- generated: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", Inv));
			stringBuilder.AppendLine("- scene: `" + SnitchCore.LastScene + "`   active: " + SnitchCore.Active);
			stringBuilder.AppendLine();
			stringBuilder.AppendLine("## Frame time (load-bearing)");
			stringBuilder.AppendLine();
			stringBuilder.AppendLine("| mean ms | median | p95 | p99 | min | max | mean fps | min fps | gc0/1k | gc1/1k | samples |");
			stringBuilder.AppendLine("|---|---|---|---|---|---|---|---|---|---|---|");
			StringBuilder stringBuilder2 = stringBuilder;
			StringBuilder stringBuilder3 = stringBuilder2;
			StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(34, 11, stringBuilder2);
			handler.AppendLiteral("| ");
			handler.AppendFormatted(F(latestFrame.MeanMs));
			handler.AppendLiteral(" | ");
			handler.AppendFormatted(F(latestFrame.MedianMs));
			handler.AppendLiteral(" | ");
			handler.AppendFormatted(F(latestFrame.P95Ms));
			handler.AppendLiteral(" | ");
			handler.AppendFormatted(F(latestFrame.P99Ms));
			handler.AppendLiteral(" | ");
			handler.AppendFormatted(F(latestFrame.MinMs));
			handler.AppendLiteral(" | ");
			handler.AppendFormatted(F(latestFrame.MaxMs));
			handler.AppendLiteral(" | ");
			handler.AppendFormatted(F(latestFrame.MeanFps));
			handler.AppendLiteral(" | ");
			handler.AppendFormatted(F(latestFrame.MinFps));
			handler.AppendLiteral(" | ");
			handler.AppendFormatted(F(latestFrame.Gc0Per1000));
			handler.AppendLiteral(" | ");
			handler.AppendFormatted(F(latestFrame.Gc1Per1000));
			handler.AppendLiteral(" | ");
			handler.AppendFormatted(latestFrame.Samples);
			handler.AppendLiteral(" |");
			stringBuilder3.AppendLine(ref handler);
			stringBuilder.AppendLine();
			stringBuilder.AppendLine("## Sections (by ms/frame)");
			stringBuilder.AppendLine();
			List<SectionRow> latestSections = SnitchCore.LatestSections;
			if (latestSections == null || latestSections.Count == 0)
			{
				stringBuilder.AppendLine("_none_");
			}
			else
			{
				stringBuilder.AppendLine("| label | group | ms/frame | % frame | calls/frame | max ms |");
				stringBuilder.AppendLine("|---|---|---|---|---|---|");
				foreach (SectionRow item in latestSections)
				{
					stringBuilder2 = stringBuilder;
					StringBuilder stringBuilder4 = stringBuilder2;
					handler = new StringBuilder.AppendInterpolatedStringHandler(21, 6, stringBuilder2);
					handler.AppendLiteral("| `");
					handler.AppendFormatted(item.Label);
					handler.AppendLiteral("` | ");
					handler.AppendFormatted(item.Group);
					handler.AppendLiteral(" | ");
					handler.AppendFormatted(F(item.MsPerFrame));
					handler.AppendLiteral(" | ");
					handler.AppendFormatted(F(item.PctFrame));
					handler.AppendLiteral(" | ");
					handler.AppendFormatted(F(item.Calls));
					handler.AppendLiteral(" | ");
					handler.AppendFormatted(F(item.MaxMs));
					handler.AppendLiteral(" |");
					stringBuilder4.AppendLine(ref handler);
				}
			}
			stringBuilder.AppendLine();
			stringBuilder.AppendLine("## Counters");
			stringBuilder.AppendLine();
			List<CounterRow> latestCounters = SnitchCore.LatestCounters;
			if (latestCounters == null || latestCounters.Count == 0)
			{
				stringBuilder.AppendLine("_none_");
			}
			else
			{
				stringBuilder.AppendLine("| id | value | unit | state |");
				stringBuilder.AppendLine("|---|---|---|---|");
				foreach (CounterRow item2 in latestCounters)
				{
					stringBuilder2 = stringBuilder;
					StringBuilder stringBuilder5 = stringBuilder2;
					handler = new StringBuilder.AppendInterpolatedStringHandler(15, 4, stringBuilder2);
					handler.AppendLiteral("| `");
					handler.AppendFormatted(item2.Id);
					handler.AppendLiteral("` | ");
					handler.AppendFormatted(F(item2.Value));
					handler.AppendLiteral(" | ");
					handler.AppendFormatted(item2.Unit);
					handler.AppendLiteral(" | ");
					handler.AppendFormatted(item2.State);
					handler.AppendLiteral(" |");
					stringBuilder5.AppendLine(ref handler);
				}
			}
			stringBuilder.AppendLine();
			stringBuilder.AppendLine("## State distributions");
			stringBuilder.AppendLine();
			List<StateSnapshot> latestStates = SnitchCore.LatestStates;
			if (latestStates == null || latestStates.Count == 0)
			{
				stringBuilder.AppendLine("_none_");
			}
			else
			{
				foreach (StateSnapshot item3 in latestStates)
				{
					stringBuilder.Append("- **").Append(item3.Title).Append("** (total ")
						.Append(item3.EffectiveTotal())
						.Append("): ");
					for (int i = 0; i < item3.Buckets.Count; i++)
					{
						if (i > 0)
						{
							stringBuilder.Append(", ");
						}
						stringBuilder.Append(item3.Buckets[i].Name).Append('=').Append(item3.Buckets[i].Count);
					}
					stringBuilder.AppendLine();
				}
			}
			stringBuilder.AppendLine();
			stringBuilder.AppendLine("---");
			stringBuilder.AppendLine("_ProfilerRecorder engine counters are inert in this IL2CPP build; frame-time + GC are the truth. Vanilla section costs are self-measured (only wrapped methods) and include a small patch overhead._");
			return stringBuilder.ToString();
		}

		private static string SectionsCsv()
		{
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.AppendLine("label,group,msPerFrame,pctFrame,callsPerFrame,maxMs");
			List<SectionRow> latestSections = SnitchCore.LatestSections;
			if (latestSections != null)
			{
				foreach (SectionRow item in latestSections)
				{
					StringBuilder stringBuilder2 = stringBuilder;
					StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(5, 6, stringBuilder2);
					handler.AppendFormatted(Csv(item.Label));
					handler.AppendLiteral(",");
					handler.AppendFormatted(Csv(item.Group));
					handler.AppendLiteral(",");
					handler.AppendFormatted(F(item.MsPerFrame));
					handler.AppendLiteral(",");
					handler.AppendFormatted(F(item.PctFrame));
					handler.AppendLiteral(",");
					handler.AppendFormatted(F(item.Calls));
					handler.AppendLiteral(",");
					handler.AppendFormatted(F(item.MaxMs));
					stringBuilder2.AppendLine(ref handler);
				}
			}
			return stringBuilder.ToString();
		}

		private static string CountersCsv()
		{
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.AppendLine("id,value,unit,state");
			List<CounterRow> latestCounters = SnitchCore.LatestCounters;
			if (latestCounters != null)
			{
				foreach (CounterRow item in latestCounters)
				{
					StringBuilder stringBuilder2 = stringBuilder;
					StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(3, 4, stringBuilder2);
					handler.AppendFormatted(Csv(item.Id));
					handler.AppendLiteral(",");
					handler.AppendFormatted(F(item.Value));
					handler.AppendLiteral(",");
					handler.AppendFormatted(Csv(item.Unit));
					handler.AppendLiteral(",");
					handler.AppendFormatted(Csv(item.State));
					stringBuilder2.AppendLine(ref handler);
				}
			}
			return stringBuilder.ToString();
		}

		private static string StatesCsv()
		{
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.AppendLine("provider,title,total,bucket,count");
			List<StateSnapshot> latestStates = SnitchCore.LatestStates;
			if (latestStates != null)
			{
				foreach (StateSnapshot item in latestStates)
				{
					for (int i = 0; i < item.Buckets.Count; i++)
					{
						StringBuilder stringBuilder2 = stringBuilder;
						StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(4, 5, stringBuilder2);
						handler.AppendFormatted(Csv(item.Id));
						handler.AppendLiteral(",");
						handler.AppendFormatted(Csv(item.Title));
						handler.AppendLiteral(",");
						handler.AppendFormatted(item.EffectiveTotal());
						handler.AppendLiteral(",");
						handler.AppendFormatted(Csv(item.Buckets[i].Name));
						handler.AppendLiteral(",");
						handler.AppendFormatted(item.Buckets[i].Count);
						stringBuilder2.AppendLine(ref handler);
					}
				}
			}
			return stringBuilder.ToString();
		}

		private static string F(double v)
		{
			if (double.IsNaN(v) || double.IsInfinity(v))
			{
				return "0";
			}
			return v.ToString("0.###", Inv);
		}

		private static string Csv(string s)
		{
			if (string.IsNullOrEmpty(s))
			{
				return "";
			}
			if (s.IndexOf(',') >= 0 || s.IndexOf('"') >= 0)
			{
				return "\"" + s.Replace("\"", "\"\"") + "\"";
			}
			return s;
		}
	}
}
namespace Snitch.Registries
{
	internal sealed class StateSnapshot
	{
		public string Id;

		public string Title;

		public int Total;

		public readonly List<StateBucket> Buckets = new List<StateBucket>(16);

		public StateSnapshot Add(string name, int count)
		{
			Buckets.Add(new StateBucket(name, count));
			return this;
		}

		public void Clear()
		{
			Total = 0;
			Buckets.Clear();
		}

		public int EffectiveTotal()
		{
			if (Total != 0)
			{
				return Total;
			}
			int num = 0;
			for (int i = 0; i < Buckets.Count; i++)
			{
				num += Buckets[i].Count;
			}
			return num;
		}
	}
	internal readonly struct StateBucket
	{
		public readonly string Name;

		public readonly int Count;

		public StateBucket(string name, int count)
		{
			Name = name;
			Count = count;
		}
	}
	internal interface IStateProvider
	{
		string Id { get; }

		StateSnapshot Poll();
	}
	internal interface ICounterSource
	{
		string Id { get; }

		string Unit { get; }

		double Read();
	}
	internal struct CounterRow
	{
		public string Id;

		public string Unit;

		public double Value;

		public string State;
	}
	internal static class StateRegistry
	{
		private sealed class DelegateStateProvider : IStateProvider
		{
			private readonly Func<StateSnapshot> _poll;

			public string Id { get; }

			public DelegateStateProvider(string id, Func<StateSnapshot> poll)
			{
				Id = id;
				_poll = poll;
			}

			public StateSnapshot Poll()
			{
				return _poll();
			}
		}

		private static readonly List<IStateProvider> _providers = new List<IStateProvider>(16);

		internal static int Count => _providers.Count;

		internal static void Register(IStateProvider p)
		{
			if (p != null)
			{
				Unregister(p.Id);
				_providers.Add(p);
			}
		}

		internal static void RegisterDelegate(string id, Func<StateSnapshot> poll)
		{
			if (!string.IsNullOrEmpty(id) && poll != null)
			{
				Register(new DelegateStateProvider(id, poll));
			}
		}

		internal static void Unregister(string id)
		{
			for (int num = _providers.Count - 1; num >= 0; num--)
			{
				if (_providers[num].Id == id)
				{
					_providers.RemoveAt(num);
				}
			}
		}

		internal static void Clear()
		{
			_providers.Clear();
		}

		internal static List<StateSnapshot> PollAll()
		{
			List<StateSnapshot> list = new List<StateSnapshot>(_providers.Count);
			for (int i = 0; i < _providers.Count; i++)
			{
				try
				{
					StateSnapshot stateSnapshot = _providers[i].Poll();
					if (stateSnapshot != null)
					{
						stateSnapshot.Id = _providers[i].Id;
						list.Add(stateSnapshot);
					}
				}
				catch (Exception ex)
				{
					Instance log = Core.Log;
					if (log != null)
					{
						log.Warning("[Snitch] state provider '" + _providers[i].Id + "' threw: " + ex.Message);
					}
				}
			}
			return list;
		}
	}
	internal static class CounterRegistry
	{
		private sealed class DelegateCounter : ICounterSource
		{
			private readonly Func<double> _read;

			public string Id { get; }

			public string Unit { get; }

			public DelegateCounter(string id, Func<double> read, string unit)
			{
				Id = id;
				_read = read;
				Unit = unit;
			}

			public double Read()
			{
				return _read();
			}
		}

		private static readonly List<ICounterSource> _sources = new List<ICounterSource>(16);

		internal static int Count => _sources.Count;

		internal static void Register(ICounterSource c)
		{
			if (c != null)
			{
				Unregister(c.Id);
				_sources.Add(c);
			}
		}

		internal static void RegisterDelegate(string id, Func<double> read, string unit)
		{
			if (!string.IsNullOrEmpty(id) && read != null)
			{
				Register(new DelegateCounter(id, read, unit ?? ""));
			}
		}

		internal static void Unregister(string id)
		{
			for (int num = _sources.Count - 1; num >= 0; num--)
			{
				if (_sources[num].Id == id)
				{
					_sources.RemoveAt(num);
				}
			}
		}

		internal static void Clear()
		{
			_sources.Clear();
		}

		internal static List<CounterRow> ReadAll()
		{
			List<CounterRow> list = new List<CounterRow>(_sources.Count);
			for (int i = 0; i < _sources.Count; i++)
			{
				CounterRow item = new CounterRow
				{
					Id = _sources[i].Id,
					Unit = _sources[i].Unit,
					State = "OK"
				};
				try
				{
					item.Value = _sources[i].Read();
				}
				catch (Exception ex)
				{
					item.State = "UNAVAILABLE";
					Instance log = Core.Log;
					if (log != null)
					{
						log.Warning("[Snitch] counter '" + _sources[i].Id + "' threw: " + ex.Message);
					}
				}
				list.Add(item);
			}
			return list;
		}
	}
}
namespace Snitch.Providers
{
	internal sealed class NpcStateProvider : IStateProvider
	{
		private readonly StateSnapshot _snap = new StateSnapshot();

		public string Id => "Vanilla.NPCs";

		public StateSnapshot Poll()
		{
			_snap.Clear();
			_snap.Title = "NPCs";
			int total = 0;
			int num = 0;
			int num2 = 0;
			int num3 = 0;
			int num4 = 0;
			int num5 = 0;
			try
			{
				List<NPC> nPCRegistry = NPCManager.NPCRegistry;
				if (nPCRegistry != null)
				{
					int count = nPCRegistry.Count;
					total = count;
					for (int i = 0; i < count; i++)
					{
						NPC val;
						try
						{
							val = nPCRegistry[i];
						}
						catch
						{
							continue;
						}
						if ((Object)(object)val == (Object)null)
						{
							continue;
						}
						try
						{
							if (!val.IsConscious)
							{
								num5++;
							}
						}
						catch
						{
						}
						try
						{
							if (!val.isVisible)
							{
								num4++;
							}
						}
						catch
						{
						}
						bool flag = false;
						bool flag2 = false;
						try
						{
							NPCMovement movement = val.Movement;
							if ((Object)(object)movement != (Object)null)
							{
								flag = movement.IsPaused;
								flag2 = movement.IsMoving;
							}
						}
						catch
						{
						}
						if (flag)
						{
							num3++;
						}
						else if (flag2)
						{
							num++;
						}
						else
						{
							num2++;
						}
					}
				}
			}
			catch
			{
			}
			_snap.Total = total;
			_snap.Add("moving", num).Add("idle", num2).Add("paused", num3)
				.Add("hidden", num4)
				.Add("unconscious", num5);
			return _snap;
		}
	}
	internal sealed class TrashStateProvider : IStateProvider
	{
		private const int Cap = 8000;

		private readonly StateSnapshot _snap = new StateSnapshot();

		public string Id => "Vanilla.Trash";

		public StateSnapshot Poll()
		{
			_snap.Clear();
			_snap.Title = "Trash";
			int total = 0;
			int num = 0;
			int num2 = 0;
			int num3 = 0;
			try
			{
				TrashManager instance = NetworkSingleton<TrashManager>.Instance;
				if ((Object)(object)instance == (Object)null)
				{
					_snap.Title = "Trash (no manager)";
					return _snap;
				}
				List<TrashItem> trashItems = instance.trashItems;
				if (trashItems != null)
				{
					int count = trashItems.Count;
					total = count;
					int num4 = ((count < 8000) ? count : 8000);
					for (int i = 0; i < num4; i++)
					{
						TrashItem val;
						try
						{
							val = trashItems[i];
						}
						catch
						{
							continue;
						}
						if ((Object)(object)val == (Object)null)
						{
							continue;
						}
						try
						{
							Rigidbody rigidbody = val.Rigidbody;
							if ((Object)(object)rigidbody == (Object)null || rigidbody.isKinematic)
							{
								num3++;
							}
							else if (rigidbody.IsSleeping())
							{
								num2++;
							}
							else
							{
								num++;
							}
						}
						catch
						{
						}
					}
					if (count > 8000)
					{
						_snap.Title = "Trash (states sampled, first 8000)";
					}
				}
			}
			catch
			{
			}
			_snap.Total = total;
			_snap.Add("awake", num).Add("sleeping", num2).Add("kinematic", num3);
			return _snap;
		}
	}
	internal sealed class QuestStateProvider : IStateProvider
	{
		private readonly StateSnapshot _snap = new StateSnapshot();

		public string Id => "Vanilla.Quests";

		public StateSnapshot Poll()
		{
			//IL_0069: Unknown result type (might be due to invalid IL or missing references)
			//IL_006e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0075: Unknown result type (might be due to invalid IL or missing references)
			//IL_0094: Expected I4, but got Unknown
			_snap.Clear();
			_snap.Title = "Quests";
			int total = 0;
			int num = 0;
			int num2 = 0;
			int num3 = 0;
			int num4 = 0;
			int num5 = 0;
			int num6 = 0;
			try
			{
				List<Quest> quests = Quest.Quests;
				if (quests != null)
				{
					int count = quests.Count;
					total = count;
					for (int i = 0; i < count; i++)
					{
						Quest val;
						try
						{
							val = quests[i];
						}
						catch
						{
							continue;
						}
						if (!((Object)(object)val == (Object)null))
						{
							EQuestState state;
							try
							{
								state = val.State;
							}
							catch
							{
								continue;
							}
							switch ((int)state)
							{
							case 0:
								num++;
								break;
							case 1:
								num2++;
								break;
							case 2:
								num3++;
								break;
							case 3:
								num4++;
								break;
							case 4:
								num5++;
								break;
							case 5:
								num6++;
								break;
							}
						}
					}
				}
			}
			catch
			{
			}
			_snap.Total = total;
			_snap.Add("active", num2).Add("inactive", num).Add("completed", num3)
				.Add("failed", num4)
				.Add("expired", num5)
				.Add("cancelled", num6);
			return _snap;
		}
	}
}
namespace Snitch.Engine
{
	internal struct FrameStats
	{
		public int Samples;

		public double MeanMs;

		public double MedianMs;

		public double P95Ms;

		public double P99Ms;

		public double MinMs;

		public double MaxMs;

		public double StdDevMs;

		public double Gc0Per1000;

		public double Gc1Per1000;

		public double MinFps
		{
			get
			{
				if (!(MaxMs > 0.0))
				{
					return 0.0;
				}
				return 1000.0 / MaxMs;
			}
		}

		public double MeanFps
		{
			get
			{
				if (!(MeanMs > 0.0))
				{
					return 0.0;
				}
				return 1000.0 / MeanMs;
			}
		}
	}
	internal static class FrameSampler
	{
		private const int Window = 120;

		private static readonly double[] _ring = new double[120];

		private static int _count;

		private static int _head;

		private static int _gc0Base;

		private static int _gc1Base;

		private static int _gcFrames;

		private static bool _gcInit;

		private static int _savedVSync = -999;

		private static int _savedTarget = -999;

		internal static void Tick()
		{
			double num = (double)Time.unscaledDeltaTime * 1000.0;
			_ring[_head] = num;
			_head = (_head + 1) % 120;
			if (_count < 120)
			{
				_count++;
			}
			_gcFrames++;
		}

		internal static FrameStats Snapshot()
		{
			FrameStats result = new FrameStats
			{
				Samples = _count
			};
			if (_count == 0)
			{
				return result;
			}
			double[] array = new double[_count];
			double num = 0.0;
			double num2 = double.MaxValue;
			double num3 = 0.0;
			for (int i = 0; i < _count; i++)
			{
				double num4 = (array[i] = _ring[i]);
				num += num4;
				if (num4 < num2)
				{
					num2 = num4;
				}
				if (num4 > num3)
				{
					num3 = num4;
				}
			}
			double num5 = num / (double)_count;
			double num6 = 0.0;
			for (int j = 0; j < _count; j++)
			{
				double num7 = array[j] - num5;
				num6 += num7 * num7;
			}
			Array.Sort(array);
			result.MeanMs = num5;
			result.MinMs = num2;
			result.MaxMs = num3;
			result.StdDevMs = Math.Sqrt(num6 / (double)_count);
			result.MedianMs = Percentile(array, 0.5);
			result.P95Ms = Percentile(array, 0.95);
			result.P99Ms = Percentile(array, 0.99);
			result.Gc0Per1000 = Gc0Per1000Frames();
			result.Gc1Per1000 = Gc1Per1000Frames();
			return result;
		}

		internal static double RelativeNoise()
		{
			FrameStats frameStats = Snapshot();
			if (!(frameStats.MeanMs > 0.0))
			{
				return 1.0;
			}
			return frameStats.StdDevMs / frameStats.MeanMs;
		}

		internal static double RelativeNoiseCheap()
		{
			if (_count == 0)
			{
				return 1.0;
			}
			double num = 0.0;
			for (int i = 0; i < _count; i++)
			{
				num += _ring[i];
			}
			double num2 = num / (double)_count;
			if (num2 <= 0.0)
			{
				return 1.0;
			}
			double num3 = 0.0;
			for (int j = 0; j < _count; j++)
			{
				double num4 = _ring[j] - num2;
				num3 += num4 * num4;
			}
			return Math.Sqrt(num3 / (double)_count) / num2;
		}

		private static double Percentile(double[] sorted, double p)
		{
			if (sorted.Length == 0)
			{
				return 0.0;
			}
			int num = (int)Math.Ceiling(p * (double)sorted.Length) - 1;
			if (num < 0)
			{
				num = 0;
			}
			if (num >= sorted.Length)
			{
				num = sorted.Length - 1;
			}
			return sorted[num];
		}

		internal static void ResetGcWindow()
		{
			_gc0Base = GC.CollectionCount(0);
			_gc1Base = SafeCount(1);
			_gcFrames = 0;
			_gcInit = true;
		}

		internal static double Gc0Per1000Frames()
		{
			if (!_gcInit || _gcFrames <= 0)
			{
				return 0.0;
			}
			return (double)(GC.CollectionCount(0) - _gc0Base) * 1000.0 / (double)_gcFrames;
		}

		internal static double Gc1Per1000Frames()
		{
			if (!_gcInit || _gcFrames <= 0)
			{
				return 0.0;
			}
			return (double)(SafeCount(1) - _gc1Base) * 1000.0 / (double)_gcFrames;
		}

		private static int SafeCount(int gen)
		{
			try
			{
				return GC.CollectionCount(gen);
			}
			catch
			{
				return 0;
			}
		}

		internal static void UncapFramerate()
		{
			try
			{
				if (_savedVSync == -999)
				{
					_savedVSync = QualitySettings.vSyncCount;
					_savedTarget = Application.targetFrameRate;
				}
				QualitySettings.vSyncCount = 0;
				Application.targetFrameRate = -1;
			}
			catch (Exception ex)
			{
				Instance log = Core.Log;
				if (log != null)
				{
					log.Warning("[Snitch] uncap failed: " + ex.Message);
				}
			}
		}

		internal static void RestoreFramerate()
		{
			try
			{
				if (_savedVSync != -999)
				{
					QualitySettings.vSyncCount = _savedVSync;
					Application.targetFrameRate = _savedTarget;
					_savedVSync = -999;
					_savedTarget = -999;
				}
			}
			catch
			{
			}
		}
	}
	internal static class SnitchCore
	{
		private static bool _active;

		private static bool _registered;

		private static float _pollAccum;

		private static int _selfId = -1;

		internal static FrameStats LatestFrame;

		internal static List<SectionRow> LatestSections = new List<SectionRow>();

		internal static List<StateSnapshot> LatestStates = new List<StateSnapshot>();

		internal static List<CounterRow> LatestCounters = new List<CounterRow>();

		internal static volatile string LatestJson;

		internal static volatile string CapsJson;

		internal static volatile int LastFrame;

		internal static volatile string LastScene = "";

		internal static bool Active => _active;

		internal static void RegisterBuiltins()
		{
			if (!_registered)
			{
				_registered = true;
				StateRegistry.Register(new NpcStateProvider());
				StateRegistry.Register(new TrashStateProvider());
				StateRegistry.Register(new QuestStateProvider());
			}
		}

		internal static void Start()
		{
			RegisterBuiltins();
			if (CapsJson == null)
			{
				CapsJson = WireProtocol.BuildCaps();
			}
			_active = true;
			FrameSampler.ResetGcWindow();
			SectionProfiler.Reset();
			AutoInstrument.DiscoverProbes();
			if (Preferences.AutoInstrument)
			{
				AutoInstrument.Enable();
			}
			_pollAccum = 999f;
			Instance log = Core.Log;
			if (log != null)
			{
				log.Msg("[snitch] sampling started.");
			}
		}

		internal static void Stop()
		{
			_active = false;
			AutoInstrument.Disable();
			Instance log = Core.Log;
			if (log != null)
			{
				log.Msg("[snitch] sampling stopped.");
			}
		}

		internal static void Tick()
		{
			if (_active)
			{
				if (_selfId < 0)
				{
					_selfId = SectionProfiler.GetId("Snitch.Self");
				}
				SectionProfiler.Begin(_selfId);
				FrameSampler.Tick();
				LastFrame = Time.frameCount;
				_pollAccum += Time.unscaledDeltaTime;
				float num = 1f / Preferences.PollHz;
				if (_pollAccum >= num)
				{
					_pollAccum = 0f;
					Poll();
				}
				AblationEngine.Tick();
				SectionProfiler.End(_selfId);
				SectionProfiler.Flush();
			}
		}

		private static void Poll()
		{
			LatestFrame = FrameSampler.Snapshot();
			LatestSections = SectionProfiler.Report(LatestFrame.MeanMs);
			LatestStates = StateRegistry.PollAll();
			LatestCounters = CounterRegistry.ReadAll();
			LatestJson = WireProtocol.BuildSnapshot(LastFrame, LastScene);
			SnitchServer.Broadcast(LatestJson);
		}
	}
}
namespace Snitch.Config
{
	internal static class Preferences
	{
		private const string CategoryId = "Snitch_01_Main";

		private static MelonPreferences_Category _category;

		private static MelonPreferences_Entry<bool> _enabled;

		private static MelonPreferences_Entry<bool> _enableInMp;

		private static MelonPreferences_Entry<bool> _autoStart;

		private static MelonPreferences_Entry<bool> _autoInstrument;

		private static MelonPreferences_Entry<bool> _showHud;

		private static MelonPreferences_Entry<float> _pollHz;

		private static MelonPreferences_Entry<bool> _serverEnabled;

		private static MelonPreferences_Entry<int> _serverPort;

		private static MelonPreferences_Entry<string> _serverToken;

		private static MelonPreferences_Entry<string> _allowedOrigins;

		internal static bool Enabled => _enabled?.Value ?? true;

		internal static bool EnableInMultiplayer => _enableInMp?.Value ?? true;

		internal static bool AutoStart => _autoStart?.Value ?? false;

		internal static bool AutoInstrument => _autoInstrument?.Value ?? true;

		internal static bool ShowHud => _showHud?.Value ?? false;

		internal static float PollHz => Mathf.Clamp(_pollHz?.Value ?? 4f, 1f, 30f);

		internal static bool ServerEnabled => _serverEnabled?.Value ?? true;

		internal static int ServerPort => Mathf.Clamp(_serverPort?.Value ?? 6140, 1024, 65535);

		internal static string ServerToken => _serverToken?.Value ?? "";

		internal static string AllowedOrigins => _allowedOrigins?.Value ?? "https://snitch.doodesch.de";

		internal static void Initialize()
		{
			if (_category == null)
			{
				_category = MelonPreferences.CreateCategory("Snitch_01_Main", "Snitch (Profiler)");
				_enabled = Create("Enabled", def: true, "Enable Snitch", "Master switch. When OFF, Snitch does nothing at all. When ON, the profiler is available but stays idle (near-zero cost) until you arm it with the in-game console command 'snitch start' or auto-start below.");
				_enableInMp = Create("EnableInMultiplayer", def: true, "Enable in multiplayer", "ON (default): profiling/measurement runs locally on every peer (safe - read-only). State-mutating features (the ablation A/B harness, NPC/trash 'off' levers) always stay host-only regardless. OFF: do nothing in MP.");
				_autoStart = Create("AutoStart", def: false, "Auto-start sampling on world load", "OFF (default): you arm sampling manually with 'snitch start'. ON: begin sampling automatically when you enter the world. Leave OFF unless you want the profiler always running.");
				_autoInstrument = Create("AutoInstrument", def: true, "Auto-instrument other mods", "ON (default): while sampling, every other loaded mod's per-frame methods (OnUpdate etc.) are timed automatically and shown as '<Mod>.OnUpdate' - so any mod's frame cost appears with no code on its side. Turn OFF to only show sections that mods (or Snitch's vanilla probes) register explicitly.");
				_showHud = Create("ShowHud", def: false, "Show profiler HUD", "On-screen overlay with frame stats, top section costs, counters and state distributions. OFF by default; toggle live with 'snitch hud' or the F6 hotkey. Only draws while sampling is armed.");
				_pollHz = Create("PollHz", 4f, "Provider poll rate (Hz)", "How often the entity STATE providers and counters are sampled (the expensive part). 4 Hz is plenty for distributions and keeps the profiler's own cost flat. Frame-time itself is always sampled every frame. Clamped 1-30.", (ValueValidator)(object)new ValueRange<float>(1f, 30f));
				_serverEnabled = Create("ServerEnabled", def: true, "Enable local data server", "ON (default): run a loopback HTTP + WebSocket server so the SnitchWeb dashboard (hosted or the bundled offline copy) can show live data. Binds 127.0.0.1 only - nothing is exposed to your network.");
				_serverPort = Create("ServerPort", 6140, "Local server port", "The loopback port for the data server + dashboard. Change only if 6140 clashes with another tool. Clamped 1024-65535.", (ValueValidator)(object)new ValueRange<int>(1024, 65535));
				_serverToken = Create("ServerToken", "", "Pairing token (optional)", "Optional shared secret the dashboard must send to connect. Empty (default) = no token; safe because the server is loopback-only and checks the browser Origin. Set a value for stricter pairing; it is shown in the log/HUD.");
				_allowedOrigins = Create("AllowedOrigins", "https://snitch.doodesch.de", "Allowed dashboard origins", "Comma-separated list of web origins permitted to connect from the browser (in addition to localhost, which is always allowed). Defaults to the hosted dashboard. Used for CORS + WebSocket Origin checks.");
			}
		}

		private static MelonPreferences_Entry<T> Create<T>(string id, T def, string name, string desc = null, ValueValidator validator = null)
		{
			if (validator != null)
			{
				return _category.CreateEntry<T>(id, def, name, desc, false, false, validator);
			}
			return _category.CreateEntry<T>(id, def, name, desc, false, false, (ValueValidator)null, (string)null);
		}

		internal static void SetShowHud(bool v)
		{
			if (_showHud != null)
			{
				_showHud.Value = v;
			}
		}
	}
}
namespace Snitch.Compat
{
	internal static class Net
	{
		internal static bool IsMultiplayer()
		{
			try
			{
				Lobby instance = Singleton<Lobby>.Instance;
				if ((Object)(object)instance == (Object)null)
				{
					return false;
				}
				return instance.IsInLobby && instance.PlayerCount > 1;
			}
			catch
			{
				return false;
			}
		}

		internal static bool IsAuthoritative()
		{
			try
			{
				Lobby instance = Singleton<Lobby>.Instance;
				if ((Object)(object)instance == (Object)null || !instance.IsInLobby)
				{
					return true;
				}
				return instance.IsHost;
			}
			catch
			{
				return true;
			}
		}
	}
}
namespace Snitch.Bridge
{
	internal static class BridgeHost
	{
		private static readonly List<string> _recentMarks = new List<string>(32);

		internal static void Install()
		{
			SnitchBridge.IsEnabled = () => SnitchCore.Active;
			SnitchBridge.BeginScope = delegate(string label)
			{
				if (!SnitchCore.Active)
				{
					return 0;
				}
				int id = SectionProfiler.GetId(label);
				SectionProfiler.Begin(id);
				return id + 1;
			};
			SnitchBridge.EndScope = delegate(int token)
			{
				if (token > 0)
				{
					SectionProfiler.End(token - 1);
				}
			};
			SnitchBridge.BeginLabel = delegate(string label)
			{
				if (SnitchCore.Active)
				{
					SectionProfiler.Begin(label);
				}
			};
			SnitchBridge.EndLabel = delegate(string label)
			{
				if (SnitchCore.Active)
				{
					SectionProfiler.End(label);
				}
			};
			SnitchBridge.RegisterCounter = delegate(string id, Func<double> read, string unit)
			{
				CounterRegistry.RegisterDelegate(id, read, unit);
			};
			SnitchBridge.UnregisterCounter = delegate(string id)
			{
				CounterRegistry.Unregister(id);
			};
			SnitchBridge.RegisterStateProvider = delegate(string id, Func<object[]> poll)
			{
				StateSnapshot cached = new StateSnapshot();
				StateRegistry.RegisterDelegate(id, delegate
				{
					cached.Clear();
					object[] array = poll();
					if (array != null && array.Length >= 4)
					{
						cached.Title = (array[0] as string) ?? id;
						cached.Total = ((array[3] is int num) ? num : 0);
						string[] array2 = array[1] as string[];
						int[] array3 = array[2] as int[];
						if (array2 != null && array3 != null)
						{
							int num2 = Math.Min(array2.Length, array3.Length);
							for (int i = 0; i < num2; i++)
							{
								cached.Add(array2[i], array3[i]);
							}
						}
					}
					return cached;
				});
			};
			SnitchBridge.UnregisterStateProvider = delegate(string id)
			{
				StateRegistry.Unregister(id);
			};
			SnitchBridge.Mark = delegate(string label)
			{
				if (!string.IsNullOrEmpty(label))
				{
					_recentMarks.Add(label);
					if (_recentMarks.Count > 32)
					{
						_recentMarks.RemoveAt(0);
					}
				}
			};
			SnitchBridge.RegisterAblationLever = delegate(string name, Action apply, Action restore)
			{
				LeverRegistry.RegisterDelegate(name, apply, restore);
			};
		}
	}
	public static class SnitchBridge
	{
		public const int AbiVersion = 1;

		public static Func<bool> IsEnabled;

		public static Func<string, int> BeginScope;

		public static Action<int> EndScope;

		public static Action<string> BeginLabel;

		public static Action<string> EndLabel;

		public static Action<string, Func<double>, string> RegisterCounter;

		public static Action<string> UnregisterCounter;

		public static Action<string, Func<object[]>> RegisterStateProvider;

		public static Action<string> UnregisterStateProvider;

		public static Action<string> Mark;

		public static Action<string, Action, Action> RegisterAblationLever;
	}
}
namespace Snitch.Ablation
{
	internal sealed class AblationLever
	{
		public string Name;

		public Func<bool> CanApply;

		public Action Apply;

		public Action Restore;
	}
	internal static class LeverRegistry
	{
		private static readonly Dictionary<string, AblationLever> _levers = new Dictionary<string, AblationLever>(StringComparer.OrdinalIgnoreCase);

		private static bool _builtins;

		internal static IEnumerable<string> Names
		{
			get
			{
				EnsureBuiltins();
				return _levers.Keys;
			}
		}

		internal static void Register(AblationLever l)
		{
			if (l != null && !string.IsNullOrEmpty(l.Name))
			{
				_levers[l.Name] = l;
			}
		}

		internal static void RegisterDelegate(string name, Action apply, Action restore)
		{
			Register(new AblationLever
			{
				Name = name,
				Apply = apply,
				Restore = restore,
				CanApply = () => true
			});
		}

		internal static AblationLever Get(string name)
		{
			EnsureBuiltins();
			if (!_levers.TryGetValue(name, out var value))
			{
				return null;
			}
			return value;
		}

		private static void EnsureBuiltins()
		{
			if (_builtins)
			{
				return;
			}
			_builtins = true;
			Register(new AblationLever
			{
				Name = "npc",
				CanApply = () => Net.IsAuthoritative(),
				Apply = delegate
				{
					ForEachNpcMovement(delegate(NPCMovement mv)
					{
						mv.PauseMovement();
					});
				},
				Restore = delegate
				{
					ForEachNpcMovement(delegate(NPCMovement mv)
					{
						mv.ResumeMovement();
					});
				}
			});
		}

		private static void ForEachNpcMovement(Action<NPCMovement> act)
		{
			try
			{
				List<NPC> nPCRegistry = NPCManager.NPCRegistry;
				if (nPCRegistry == null)
				{
					return;
				}
				int count = nPCRegistry.Count;
				for (int i = 0; i < count; i++)
				{
					NPC val;
					try
					{
						val = nPCRegistry[i];
					}
					catch
					{
						continue;
					}
					if ((Object)(object)val == (Object)null)
					{
						continue;
					}
					try
					{
						NPCMovement movement = val.Movement;
						if ((Object)(object)movement != (Object)null)
						{
							act(movement);
						}
					}
					catch
					{
					}
				}
			}
			catch
			{
			}
		}
	}
	internal static class AblationEngine
	{
		private enum S
		{
			Idle,
			BaseWarm,
			OffWarm
		}

		private const int WarmupFrames = 120;

		private const int MaxExtraFrames = 600;

		private const double NoiseThreshold = 0.22;

		private static S _state = S.Idle;

		private static int _timer;

		private static int _extra;

		private static double _baseMs;

		private static AblationLever _lever;

		internal static bool Active => _state != S.Idle;

		internal static string Status { get; private set; } = "idle";

		internal static void Start(string name)
		{
			if (Active)
			{
				Instance log = Core.Log;
				if (log != null)
				{
					log.Warning("[snitch] ablation already running.");
				}
				return;
			}
			AblationLever ablationLever = LeverRegistry.Get(name);
			if (ablationLever == null)
			{
				Instance log2 = Core.Log;
				if (log2 != null)
				{
					log2.Warning("[snitch] no lever '" + name + "'. Available: " + string.Join(", ", LeverRegistry.Names));
				}
				return;
			}
			if (ablationLever.CanApply != null && !ablationLever.CanApply())
			{
				Instance log3 = Core.Log;
				if (log3 != null)
				{
					log3.Warning("[snitch] lever '" + name + "' not applicable here (host-only?).");
				}
				return;
			}
			if (!SnitchCore.Active)
			{
				SnitchCore.Start();
			}
			_lever = ablationLever;
			FrameSampler.UncapFramerate();
			EnterGate();
			_state = S.BaseWarm;
			Status = name + ": baseline";
			Instance log4 = Core.Log;
			if (log4 != null)
			{
				log4.Msg("[snitch] ablation '" + name + "' started - settling all-on baseline (uncapped). 'snitch ablate' status via 'snitch status'.");
			}
		}

		internal static void Abort(string why)
		{
			if (Active)
			{
				try
				{
					_lever?.Restore?.Invoke();
				}
				catch
				{
				}
				FrameSampler.RestoreFramerate();
				_state = S.Idle;
				Status = "idle";
				Instance log = Core.Log;
				if (log != null)
				{
					log.Warning("[snitch] ablation aborted: " + why);
				}
			}
		}

		internal static void Tick()
		{
			if (_state == S.Idle)
			{
				return;
			}
			switch (_state)
			{
			case S.BaseWarm:
				if (!GateReady())
				{
					break;
				}
				_baseMs = FrameSampler.Snapshot().MeanMs;
				try
				{
					_lever.Apply?.Invoke();
				}
				catch (Exception ex)
				{
					Instance log2 = Core.Log;
					if (log2 != null)
					{
						log2.Warning("[snitch] lever apply failed: " + ex.Message);
					}
					Abort("apply failed");
					break;
				}
				EnterGate();
				_state = S.OffWarm;
				Status = _lever.Name + ": off";
				break;
			case S.OffWarm:
				if (GateReady())
				{
					double meanMs = FrameSampler.Snapshot().MeanMs;
					try
					{
						_lever.Restore?.Invoke();
					}
					catch
					{
					}
					FrameSampler.RestoreFramerate();
					double num = _baseMs - meanMs;
					double num2 = ((_baseMs > 0.0) ? (num / _baseMs * 100.0) : 0.0);
					Instance log = Core.Log;
					if (log != null)
					{
						log.Msg($"[snitch] ablation '{_lever.Name}': baseline={_baseMs:F2}ms  off={meanMs:F2}ms  => cost ~= {num:F2} ms/frame ({num2:F0}% of frame).");
					}
					WriteCsv(_lever.Name, _baseMs, meanMs, num, num2);
					_state = S.Idle;
					Status = "idle";
				}
				break;
			}
		}

		private static void EnterGate()
		{
			_timer = 120;
			_extra = 600;
		}

		private static bool GateReady()
		{
			if (_timer > 0)
			{
				_timer--;
				return false;
			}
			if (FrameSampler.RelativeNoiseCheap() <= 0.22 || _extra <= 0)
			{
				return true;
			}
			_extra--;
			return false;
		}

		private static void WriteCsv(string lever, double baseMs, double offMs, double delta, double pct)
		{
			try
			{
				string text = Path.Combine(Directory.GetCurrentDirectory(), "Mods", "Snitch", "runs");
				Directory.CreateDirectory(text);
				string value = DateTime.Now.ToString("yyyyMMdd_HHmmss", CultureInfo.InvariantCulture);
				string text2 = Path.Combine(text, $"ablate_{lever}_{value}.csv");
				StringBuilder stringBuilder = new StringBuilder();
				stringBuilder.AppendLine("lever,baselineMs,offMs,deltaMs,pctOfFrame");
				StringBuilder stringBuilder2 = stringBuilder;
				StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(4, 5, stringBuilder2);
				handler.AppendFormatted(lever);
				handler.AppendLiteral(",");
				handler.AppendFormatted(F(baseMs));
				handler.AppendLiteral(",");
				handler.AppendFormatted(F(offMs));
				handler.AppendLiteral(",");
				handler.AppendFormatted(F(delta));
				handler.AppendLiteral(",");
				handler.AppendFormatted(F(pct));
				stringBuilder2.AppendLine(ref handler);
				File.WriteAllText(text2, stringBuilder.ToString());
				Instance log = Core.Log;
				if (log != null)
				{
					log.Msg("[snitch] ablation CSV: " + text2);
				}
			}
			catch (Exception ex)
			{
				Instance log2 = Core.Log;
				if (log2 != null)
				{
					log2.Warning("[snitch] ablation CSV failed: " + ex.Message);
				}
			}
		}

		private static string F(double v)
		{
			return v.ToString("0.###", CultureInfo.InvariantCulture);
		}
	}
}