Some mods target the Mono version of the game, which is available by opting into the Steam beta branch "alternate"
Decompiled source of Snitch v1.0.2
Snitch.dll
Decompiled a day agousing 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); } } }