using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Net; using System.Net.Sockets; using System.Net.WebSockets; using System.Reflection; using System.Resources; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Cryptography; using System.Security.Permissions; using System.Text; using System.Threading; using System.Threading.Tasks; using HarmonyLib; using Hash.Api; using Hotline.Api; using Il2CppScheduleOne; using Il2CppScheduleOne.DevUtilities; using Il2CppScheduleOne.GameTime; using Il2CppScheduleOne.NPCs; using Il2CppScheduleOne.Networking; using Il2CppScheduleOne.Quests; using Il2CppScheduleOne.Trash; using Il2CppSystem.Collections.Generic; using MelonLoader; using MelonLoader.Preferences; using Microsoft.CodeAnalysis; using Snitch; using Snitch.Ablation; using Snitch.Bridge; using Snitch.Compat; using Snitch.Config; using Snitch.Engine; using Snitch.Logging; using Snitch.Panels; using Snitch.Providers; using Snitch.Registries; using Snitch.Reporting; using Snitch.Sections; using Snitch.Server; using Snitch.UI; using Snitch.Vanilla; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: MelonInfo(typeof(Core), "Snitch", "1.6.2", "DooDesch", "https://github.com/DooDesch-Mods/ScheduleOne-Snitch")] [assembly: MelonGame("TVGS", "Schedule I")] [assembly: MelonOptionalDependencies(new string[] { "Hotline" })] [assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")] [assembly: AssemblyCompany("DooDesch")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyCopyright("Copyright © DooDesch")] [assembly: AssemblyFileVersion("1.6.2.0")] [assembly: AssemblyInformationalVersion("1.6.2+7ee24a1f75979e8f353e0f0f8d3aaa4ee60473aa")] [assembly: AssemblyProduct("Snitch")] [assembly: AssemblyTitle("Snitch")] [assembly: NeutralResourcesLanguage("en-US")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.6.2.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace DooDesch { internal static class ModVersion { internal const string Current = "1.6.2"; } } namespace Hash.Api { public static class HashCommands { private const string BridgeTypeName = "Hash.Bridge.HashBridge, Hash"; private static readonly List _pending = new List(); private static Action _declare; private static bool _bound; public static bool Available { get { Bind(); return _bound; } } private static string Owner { get { try { return Assembly.GetExecutingAssembly().GetName().Name ?? ""; } catch { return ""; } } } public static void Add(string word, string description, string example = null) { if (!string.IsNullOrEmpty(word)) { Bind(); if (_declare != null) { Safely(word, description, example); return; } _pending.Add(new string[3] { word, description ?? "", example ?? "" }); } } private static void Safely(string word, string description, string example) { try { _declare(word, description ?? "", example ?? "", Owner); } catch { } } private static void Bind() { if (_bound) { return; } try { Type type = Type.GetType("Hash.Bridge.HashBridge, Hash", throwOnError: false); if (type == null) { return; } _declare = type.GetField("Declare", BindingFlags.Static | BindingFlags.Public)?.GetValue(null) as Action; if (_declare == null) { return; } _bound = true; foreach (string[] item in _pending) { Safely(item[0], item[1], item[2]); } _pending.Clear(); } catch { } } } } namespace Hotline.Api { public static class Hud { private static bool _bound; private static bool _autoDone; private static int _probeAttempts; private static readonly List _pending = new List(); private static Action _registerPanel; private static Action _registerAction; private static Action, Action> _registerToggle; private static Action> _registerText; private static Action> _registerImage; private static Action, Action> _registerSlider; private static Action _bindPanelLog; private static Action _log; private static Action _registerHotkey; private static Action _showOverlay; private static Action _showPanel; private static Func _isPanelVisible; public static bool Available { get { EnsureBound(); return _bound; } } public static Panel RegisterPanel(string id, string title = null) { Panel result = new Panel(id); if (string.IsNullOrEmpty(id)) { return result; } string t = title; EnsureBound(); if (_registerPanel != null) { _registerPanel(id, t); } else { _pending.Add(delegate { _registerPanel?.Invoke(id, t); }); } return result; } public static void RegisterAction(string panelId, string label, Action run) { if (run == null || string.IsNullOrEmpty(label)) { return; } string actionId = panelId + ":" + Slug(label); EnsureBound(); if (_registerAction != null) { _registerAction(panelId, actionId, label, run); return; } _pending.Add(delegate { _registerAction?.Invoke(panelId, actionId, label, run); }); } public static void RegisterToggle(string panelId, string label, Func get, Action set) { if (get == null || set == null || string.IsNullOrEmpty(label)) { return; } string toggleId = panelId + ":" + Slug(label); EnsureBound(); if (_registerToggle != null) { _registerToggle(panelId, toggleId, label, get, set); return; } _pending.Add(delegate { _registerToggle?.Invoke(panelId, toggleId, label, get, set); }); } public static void RegisterSlider(string panelId, string label, double min, double max, Func get, Action set, double step = 0.0, string unit = null) { if (get == null || set == null || string.IsNullOrEmpty(label) || max <= min) { return; } string sliderId = panelId + ":" + Slug(label); string u = unit ?? ""; EnsureBound(); if (_registerSlider != null) { _registerSlider(panelId, sliderId, label, min, max, step, u, get, set); return; } _pending.Add(delegate { _registerSlider?.Invoke(panelId, sliderId, label, min, max, step, u, get, set); }); } public static void RegisterText(string panelId, Func provider) { if (provider == null) { return; } EnsureBound(); if (_registerText != null) { _registerText(panelId, provider); return; } _pending.Add(delegate { _registerText?.Invoke(panelId, provider); }); } public static void ShowOverlay(bool show) { EnsureBound(); _showOverlay?.Invoke(show); } public static void ShowPanel(string panelId, bool show) { if (!string.IsNullOrEmpty(panelId)) { EnsureBound(); _showPanel?.Invoke(panelId, show); } } public static bool IsPanelVisible(string panelId) { if (string.IsNullOrEmpty(panelId)) { return false; } EnsureBound(); try { return _isPanelVisible != null && _isPanelVisible(panelId); } catch { return false; } } public static void RegisterImage(string panelId, Func provider) { if (provider == null) { return; } EnsureBound(); if (_registerImage != null) { _registerImage(panelId, provider); return; } _pending.Add(delegate { _registerImage?.Invoke(panelId, provider); }); } public static void BindPanelLog(string panelId) { EnsureBound(); if (_bindPanelLog != null) { _bindPanelLog(panelId); return; } _pending.Add(delegate { _bindPanelLog?.Invoke(panelId); }); } public static void Log(string channel, string message, LogLevel level = LogLevel.Info) { if (string.IsNullOrEmpty(message)) { return; } int lv = (int)level; EnsureBound(); if (_log != null) { _log(channel, lv, message); return; } _pending.Add(delegate { _log?.Invoke(channel, lv, message); }); } public static void RegisterHotkey(string ownerId, string label, HotlineKey key, Action run) { if (run == null || string.IsNullOrEmpty(label)) { return; } int k = (int)key; EnsureBound(); if (_registerHotkey != null) { _registerHotkey(ownerId, label, k, run); return; } _pending.Add(delegate { _registerHotkey?.Invoke(ownerId, label, k, run); }); } public static void AutoRegister() { EnsureBound(); if (_bound) { RunAutoRegister(); } } private static void RunAutoRegister() { if (_autoDone) { return; } _autoDone = true; try { Assembly assembly = typeof(Hud).Assembly; ((assembly.GetType("HotlineProbe", throwOnError: false) ?? FindByLeafName(assembly, "HotlineProbe"))?.GetMethod("Register", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null))?.Invoke(null, null); } catch { } } private static Type FindByLeafName(Assembly asm, string leaf) { Type[] types; try { types = asm.GetTypes(); } catch (ReflectionTypeLoadException ex) { types = ex.Types; } catch { return null; } if (types == null) { return null; } Type[] array = types; foreach (Type type in array) { if (type != null && type.IsClass && type.IsAbstract && type.IsSealed && type.Name == leaf) { return type; } } return null; } private static string Slug(string s) { if (string.IsNullOrEmpty(s)) { return ""; } StringBuilder stringBuilder = new StringBuilder(s.Length); foreach (char c in s) { if (char.IsLetterOrDigit(c)) { stringBuilder.Append(char.ToLowerInvariant(c)); } else if (stringBuilder.Length > 0 && stringBuilder[stringBuilder.Length - 1] != '-') { stringBuilder.Append('-'); } } return stringBuilder.ToString().Trim('-'); } private static void EnsureBound() { if (_bound) { return; } try { Type type = FindBridge(_probeAttempts++ % 30 == 0); if (type == null || (type.GetField("AbiVersion", BindingFlags.Static | BindingFlags.Public)?.GetValue(null) is int num && num < 1)) { return; } _registerPanel = Get>(type, "RegisterPanel"); _registerAction = Get>(type, "RegisterAction"); _registerToggle = Get, Action>>(type, "RegisterToggle"); _registerText = Get>>(type, "RegisterText"); _registerImage = Get>>(type, "RegisterImage"); _registerSlider = Get, Action>>(type, "RegisterSlider"); _bindPanelLog = Get>(type, "BindPanelLog"); _log = Get>(type, "Log"); _registerHotkey = Get>(type, "RegisterHotkey"); _showOverlay = Get>(type, "ShowOverlay"); _showPanel = Get>(type, "ShowPanel"); _isPanelVisible = Get>(type, "IsPanelVisible"); if (_registerPanel == null) { return; } _bound = true; for (int i = 0; i < _pending.Count; i++) { try { _pending[i](); } catch { } } _pending.Clear(); RunAutoRegister(); } catch { } } private static T Get(Type t, string field) where T : class { return t.GetField(field, BindingFlags.Static | BindingFlags.Public)?.GetValue(null) as T; } private static Type FindBridge(bool scan) { Type type = Type.GetType("Hotline.Bridge.HotlineBridge, Hotline", throwOnError: false); if (type != null || !scan) { return type; } Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { try { type = assembly.GetType("Hotline.Bridge.HotlineBridge", throwOnError: false); if (type != null) { return type; } } catch { } } return null; } } public enum LogLevel { Info, Warning, Error } public enum HotlineKey { None = 0, F1 = 282, F2 = 283, F3 = 284, F4 = 285, F5 = 286, F6 = 287, F7 = 288, F8 = 289, F9 = 290, F10 = 291, F11 = 292, F12 = 293 } public sealed class Panel { private readonly string _id; public string Id => _id; internal Panel(string id) { _id = id ?? ""; } public Panel Text(Func provider) { Hud.RegisterText(_id, provider); return this; } public Panel Image(Func provider) { Hud.RegisterImage(_id, provider); return this; } public Panel Action(string label, Action run) { Hud.RegisterAction(_id, label, run); return this; } public Panel Toggle(string label, Func get, Action set) { Hud.RegisterToggle(_id, label, get, set); return this; } public Panel Slider(string label, double min, double max, Func get, Action set, double step = 0.0, string unit = null) { Hud.RegisterSlider(_id, label, min, max, get, set, step, unit); return this; } public Panel Hotkey(string label, HotlineKey key, Action run) { Hud.RegisterHotkey(_id, label, key, run); return this; } public Panel Log() { Hud.BindPanelLog(_id); return this; } public void Write(string message, LogLevel level = LogLevel.Info) { Hud.Log(_id, message, level); } } } namespace Snitch { public sealed class Core : MelonMod { private bool _inWorld; public static Core Instance { get; private set; } public static Instance Log { get; private set; } internal static Harmony HarmonyInst { get; private set; } public override void OnInitializeMelon() { Instance = this; Log = ((MelonBase)this).LoggerInstance; HarmonyInst = ((MelonBase)this).HarmonyInstance; Preferences.Initialize(); LogHub.Install(); BridgeHost.Install(); Hud.RegisterPanel("Snitch", "Snitch (Profiler)").Text(ProfilerHud.BuildOverview).Action("Start sampling", SnitchCore.Start) .Action("Stop sampling", SnitchCore.Stop) .Action("Reset", delegate { SnitchCore.Stop(); SnitchCore.Start(); }) .Action("Open dashboard", OpenDashboard) .Toggle("Phone remote (scan the QR)", () => LanServer.Running, SetLanRemote) .Image(QrImage.Build); HashCommands.Add("snitch", "profiler: start, stop, top, states, report, lan", "snitch start"); try { ((MelonBase)this).HarmonyInstance.PatchAll(); } catch (Exception ex) { Log.Warning("Harmony patch failed: " + ex.Message); } if (Preferences.Enabled && Preferences.ServerEnabled) { SnitchServer.Start(Preferences.ServerPort, Preferences.ServerToken, Preferences.AllowedOrigins); } if (Preferences.Enabled && Preferences.ServerEnabled && Preferences.LanAccess) { LanServer.Start(Preferences.LanPort); RelayHost.Start(Guid.NewGuid().ToString("N").Substring(0, 12)); } Instance log = Log; MelonAssembly melonAssembly = ((MelonBase)this).MelonAssembly; log.Msg("Snitch v" + (((melonAssembly == null) ? null : melonAssembly.Assembly?.GetName()?.Version?.ToString(3)) ?? "?") + " - profiler. Console: 'snitch start' to begin, 'snitch help' for commands."); } public override void OnSceneWasLoaded(int buildIndex, string sceneName) { _inWorld = sceneName == "Main"; SnitchCore.LastScene = sceneName; if (_inWorld && Preferences.Enabled) { SnitchCore.RegisterBuiltins(); AutoInstrument.DiscoverProbes(); if (Preferences.AutoStart) { SnitchCore.Start(); } } } public override void OnSceneWasUnloaded(int buildIndex, string sceneName) { _inWorld = false; } public override void OnUpdate() { SnitchServer.Pump(); if (_inWorld && Preferences.Enabled) { SnitchCore.Tick(); } } internal static string DashboardUrl() { if (SnitchServer.Running && WebAssets.HasBundledDashboard()) { return "http://127.0.0.1:" + SnitchServer.Port + "/"; } return "https://snitch.doodesch.de"; } internal static void OpenDashboard() { if (!SnitchServer.Running) { Log.Warning("[snitch] the local data server is off - a dashboard would have nothing to connect to. Set ServerEnabled in MelonPreferences and restart."); return; } string text = DashboardUrl(); try { Application.OpenURL(text); Log.Msg("[snitch] opened " + text + (text.StartsWith("http://127") ? " (bundled dashboard)" : " (hosted dashboard, connects back to loopback)")); } catch (Exception ex) { Log.Warning("[snitch] could not open a browser (" + ex.Message + "). Open this yourself: " + text); } } private static void SetLanRemote(bool on) { Preferences.LanAccess = on; try { MelonPreferences.Save(); } catch { } if (on) { if (!LanServer.Running) { LanServer.Start(Preferences.LanPort); } if (!RelayHost.Running) { RelayHost.Start(Guid.NewGuid().ToString("N").Substring(0, 12)); } } else { RelayHost.Stop(); LanServer.Stop(); } } public override void OnApplicationQuit() { SnitchServer.Stop(); RelayHost.Stop(); LanServer.Stop(); LogHub.Uninstall(); } public override void OnDeinitializeMelon() { SnitchServer.Stop(); RelayHost.Stop(); LanServer.Stop(); LogHub.Uninstall(); } } internal static class SnitchConsole { private static int _lastFrame = -1; private static string _lastSig = ""; internal const string PanelId = "Snitch"; internal static bool TryHandle(string raw) { if (string.IsNullOrWhiteSpace(raw)) { return false; } return Dispatch(raw.Trim().Split(new char[2] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries)); } internal static bool TryHandle(List args) { if (args == null || args.Count == 0) { return false; } string[] array = new string[args.Count]; for (int i = 0; i < args.Count; i++) { array[i] = args[i]; } return Dispatch(array); } private static bool Dispatch(string[] p) { if (p.Length == 0 || !p[0].Equals("snitch", StringComparison.OrdinalIgnoreCase)) { return false; } string text = string.Join(" ", p); int frameCount = Time.frameCount; if (frameCount == _lastFrame && text == _lastSig) { return true; } _lastFrame = frameCount; _lastSig = text; LogHub.Write("Console", 0, text); string text2 = ((p.Length > 1) ? p[1].ToLowerInvariant() : "status"); try { switch (text2) { case "start": SnitchCore.Start(); break; case "stop": SnitchCore.Stop(); break; case "status": Status(); break; case "frame": Frame(); break; case "top": case "sections": Top(IntArg(p, 2, 8), text2 == "sections"); break; case "states": States((p.Length > 2) ? p[2] : null); break; case "counters": Counters(); break; case "panels": PanelsList(); break; case "act": ActCmd(p); break; case "toggle": ToggleCmd(p); break; case "slider": SliderCmd(p); break; case "open": OverlayCmd(show: true, p); break; case "close": OverlayCmd(show: false, p); break; case "dashboard": Core.OpenDashboard(); break; case "log": LogCmd(p); break; case "vanilla": Vanilla(p); break; case "lan": Lan(p); break; case "report": Report((p.Length > 2) ? p[2].ToLowerInvariant() : "all"); break; case "ablate": Ablate(p); break; case "levers": Log("ablation levers: " + string.Join(", ", LeverRegistry.Names)); break; case "help": Help(); break; default: Log("unknown '" + text2 + "'. Try 'snitch help'."); break; } } catch (Exception ex) { Log("error: " + ex.Message); } return true; } private static void Help() { Log("commands: open [all] | close [all] | start | stop | status | frame | top [n] | sections | states [id] | counters | panels | act | toggle [on|off] | slider [value] | dashboard | log [|all] [n] | vanilla [on|off] | lan [on|off] | ablate | levers | report [md|csv|all] ('open' shows the Snitch panel in the Hotline overlay; 'open all' shows the whole overlay)"); } private static void OverlayCmd(bool show, string[] p) { bool flag = p.Length > 2 && p[2].ToLowerInvariant() == "all"; if (!Hud.Available) { Log("the in-game overlay needs the Hotline mod - install it, or use 'snitch dashboard' for the web view."); } else if (flag) { Hud.ShowOverlay(show); Log("overlay " + (show ? "shown" : "hidden") + "."); } else { Hud.ShowPanel("Snitch", show); Log("Snitch panel " + (show ? "shown" : "hidden") + (show ? " (use 'snitch open all' for every panel)." : ".")); } } private static void PanelsList() { IReadOnlyList all = PanelRegistry.All; if (all.Count == 0) { Log("no mod panels registered yet (enter the world; panels register on probe discovery)."); return; } Log($"{all.Count} panel(s) (toggle their windows in the Hotline overlay):"); for (int i = 0; i < all.Count; i++) { PanelModel panelModel = all[i]; Log($" {panelModel.Id,-16} actions={panelModel.Actions.Count} toggles={panelModel.Toggles.Count} sliders={panelModel.Sliders.Count} title=\"{panelModel.Title}\""); } } private static void ActCmd(string[] p) { if (p.Length <= 2) { Log("usage: snitch act (see the panel; ids look like 'Siesta:force-cosmetic')."); } else { Log(PanelRegistry.Invoke(p[2]) ? ("ran " + p[2]) : ("no action '" + p[2] + "'")); } } private static void ToggleCmd(string[] p) { if (p.Length <= 2) { Log("usage: snitch toggle [on|off] (omit to flip)."); return; } string text = p[2]; bool value = BoolArg(p, 3, !PanelRegistry.GetToggle(text)); Log(PanelRegistry.SetToggle(text, value) ? $"{text} = {value}" : ("no toggle '" + text + "'")); } private static void SliderCmd(string[] p) { if (p.Length <= 2) { Log("usage: snitch slider [value] (omit the value to read it). 'snitch panels' lists panels."); return; } string text = p[2]; SliderItem slider = PanelRegistry.GetSlider(text); double result; if (slider == null) { Log("no slider '" + text + "'"); } else if (p.Length <= 3) { Log($"{text} = {slider.Read():0.###} {slider.Unit} (range {slider.Min:0.###}..{slider.Max:0.###}, step {slider.Step:0.###})".Replace(" ", " ").TrimEnd()); } else if (!double.TryParse(p[3], NumberStyles.Float, CultureInfo.InvariantCulture, out result)) { Log("not a number: " + p[3]); } else { PanelRegistry.SetSlider(text, result); Log($"{text} = {slider.Read():0.###} {slider.Unit}".TrimEnd()); } } private static void LogCmd(string[] p) { string text = ((p.Length > 2) ? p[2] : "all"); int n = IntArg(p, 3, 25); List list = (text.Equals("all", StringComparison.OrdinalIgnoreCase) ? LogHub.Timeline(n) : LogHub.Channel(text, n)); if (list.Count == 0) { Log($"log '{text}': no entries (channels: {string.Join(", ", LogHub.Channels())})."); return; } Log($"log '{text}' (last {list.Count}):"); foreach (LogEntry item in list) { string value = ((item.Lvl == 2) ? "E" : ((item.Lvl == 1) ? "W" : "I")); Log($" {item.Time} {value} [{item.Ch}] {item.Msg}"); } } private static void Status() { FrameStats latestFrame = SnitchCore.LatestFrame; Log($"active={SnitchCore.Active} fps={latestFrame.MeanFps:F0} (min {latestFrame.MinFps:F0}) frame={latestFrame.MeanMs:F2}ms p95={latestFrame.P95Ms:F2}ms sections={SectionProfiler.LabelCount} states={StateRegistry.Count} counters={CounterRegistry.Count} poll={Preferences.PollHz:F0}Hz"); if (!SnitchCore.Active) { Log("(idle - run 'snitch start' to begin sampling)"); } } private static void Frame() { FrameStats latestFrame = SnitchCore.LatestFrame; Log($"frame: mean={latestFrame.MeanMs:F2}ms median={latestFrame.MedianMs:F2} p95={latestFrame.P95Ms:F2} p99={latestFrame.P99Ms:F2} min={latestFrame.MinMs:F2} max={latestFrame.MaxMs:F2} | fps mean={latestFrame.MeanFps:F0} min={latestFrame.MinFps:F0} | gc0/1000f={latestFrame.Gc0Per1000:F1} gc1/1000f={latestFrame.Gc1Per1000:F1} samples={latestFrame.Samples}"); } private static void Top(int n, bool all) { List 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 latestStates = SnitchCore.LatestStates; if (latestStates == null || latestStates.Count == 0) { Log("states: none yet (start sampling first)."); return; } foreach (StateSnapshot item in latestStates) { if (filter != null && item.Title.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0) { continue; } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append(" ").Append(item.Title).Append(" (total ") .Append(item.EffectiveTotal()) .Append("): "); for (int i = 0; i < item.Buckets.Count; i++) { if (i > 0) { stringBuilder.Append(" "); } stringBuilder.Append(item.Buckets[i].Name).Append('=').Append(item.Buckets[i].Count); } Log(stringBuilder.ToString()); } } private static void Vanilla(string[] p) { string text = ((p.Length > 2) ? p[2].ToLowerInvariant() : "status"); if (text == "on") { VanillaProbes.Enable(); } else if (text == "off") { VanillaProbes.Disable(); } else { Log("vanilla probes: " + VanillaProbes.Status() + " (use 'snitch vanilla on|off')"); } } private static void Lan(string[] p) { string text = ((p.Length > 2) ? p[2].ToLowerInvariant() : "status"); if (text == "on") { if (LanServer.Running) { Log("phone remote already on - " + LanUrl()); return; } Preferences.LanAccess = true; try { MelonPreferences.Save(); } catch { } LanServer.Start(Preferences.LanPort); if (!RelayHost.Running) { RelayHost.Start(Guid.NewGuid().ToString("N").Substring(0, 12)); } Log(LanServer.Running ? ("phone remote ON - " + LanUrl() + " (+ relay for other networks; scan the QR from your phone)") : "phone remote failed to start - see the log (port in use? change LanPort)."); } else if (text == "off") { Preferences.LanAccess = false; try { MelonPreferences.Save(); } catch { } RelayHost.Stop(); LanServer.Stop(); Log("phone remote OFF."); } else { Log(LanServer.Running ? ("LAN remote ON - " + LanUrl()) : "LAN remote OFF (use 'snitch lan on'). Lets a phone on your Wi-Fi open the dashboard as a remote."); } } private static string LanUrl() { return $"http://{LanServer.Ip}:{LanServer.Port}/ (token {LanServer.Token})"; } private static void Report(string fmt) { if (fmt != "md" && fmt != "csv" && fmt != "all") { fmt = "all"; } try { string text = ReportWriter.Write(fmt); Log("report written: " + text); } catch (Exception ex) { Log("report failed: " + ex.Message); } } private static void Ablate(string[] p) { if (p.Length <= 2) { Log("usage: snitch ablate . levers: " + string.Join(", ", LeverRegistry.Names)); } else { AblationEngine.Start(p[2].ToLowerInvariant()); } } private static void Counters() { List latestCounters = SnitchCore.LatestCounters; if (latestCounters == null || latestCounters.Count == 0) { Log("counters: none registered."); return; } foreach (CounterRow item in latestCounters) { Log($" {item.Id,-28} {item.Value,12:F2} {item.Unit} [{item.State}]"); } } private static int IntArg(string[] p, int idx, int def) { if (p.Length > idx && int.TryParse(p[idx], out var result)) { return result; } return def; } private static float FloatArg(string[] p, int idx, float def) { if (p.Length > idx && float.TryParse(p[idx], NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { return result; } return def; } private static bool BoolArg(string[] p, int idx, bool toggleDefault) { if (p.Length <= idx) { return toggleDefault; } switch (p[idx].ToLowerInvariant()) { case "on": case "true": case "1": case "yes": return true; case "off": case "false": case "0": case "no": return false; default: return toggleDefault; } } internal static void Log(string msg) { Instance log = Core.Log; if (log != null) { log.Msg("[snitch] " + msg); } LogHub.Write("Snitch", 0, msg); } } [HarmonyPatch(typeof(Console), "SubmitCommand", new Type[] { typeof(string) })] internal static class Snitch_Console_SubmitCommand_String_Patch { private static bool Prefix(string args) { try { return !SnitchConsole.TryHandle(args); } catch { return true; } } } [HarmonyPatch(typeof(Console), "SubmitCommand", new Type[] { typeof(List) })] internal static class Snitch_Console_SubmitCommand_List_Patch { private static bool Prefix(List 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 _ids = new Dictionary(); 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 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 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 RegisteredMods() { try { return MelonTypeBase.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 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 _ids = new Dictionary(); private static readonly List _applied = new List(); private static readonly List _failed = new List(); internal static void Enable() { EnsurePatched(); Enabled = true; Instance log = Core.Log; if (log != null) { log.Msg("[snitch] vanilla probes ON. " + Status()); } } internal static void Disable() { Enabled = false; Instance log = Core.Log; if (log != null) { log.Msg("[snitch] vanilla probes OFF (patches stay installed but dormant)."); } } internal static string Status() { return $"enabled={Enabled} applied=[{string.Join(", ", _applied)}] failed=[{string.Join(", ", _failed)}]"; } private static void EnsurePatched() { if (!_patched) { _patched = true; Patch(typeof(NPCMovement), "Update", "Vanilla.NPC.Movement.Update"); Patch(typeof(NPCMovement), "FixedUpdate", "Vanilla.NPC.Movement.FixedUpdate"); Patch(typeof(TimeManager), "Update", "Vanilla.Time.Update"); } } private static void Patch(Type type, string method, string label) { //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Expected O, but got Unknown //IL_0088: Expected O, but got Unknown try { MethodInfo methodInfo = AccessTools.Method(type, method, Type.EmptyTypes, (Type[])null); if (methodInfo == null) { _failed.Add(label + "(method not found)"); return; } int id = SectionProfiler.GetId(label); _ids[methodInfo] = id; Core.HarmonyInst.Patch((MethodBase)methodInfo, new HarmonyMethod(AccessTools.Method(typeof(VanillaProbes), "SharedPrefix", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(AccessTools.Method(typeof(VanillaProbes), "SharedFinalizer", (Type[])null, (Type[])null)), (HarmonyMethod)null); _applied.Add(label); } catch (Exception ex) { _failed.Add(label + "(" + ex.Message + ")"); } } private static void SharedPrefix(MethodBase __originalMethod) { if (Enabled && _ids.TryGetValue(__originalMethod, out var value)) { SectionProfiler.Begin(value); } } private static void SharedFinalizer(MethodBase __originalMethod) { if (Enabled && _ids.TryGetValue(__originalMethod, out var value)) { SectionProfiler.End(value); } } } } namespace Snitch.UI { internal static class ProfilerHud { internal static string BuildOverview() { FrameStats latestFrame = SnitchCore.LatestFrame; StringBuilder stringBuilder = new StringBuilder(512); if (!SnitchCore.Active) { stringBuilder.Append("idle - run 'snitch start' (or auto-start) to sample.\n"); } string value = ((latestFrame.MeanFps >= 50.0) ? "#5f5" : ((latestFrame.MeanFps >= 30.0) ? "#fd5" : "#f55")); stringBuilder.Append("') .Append(latestFrame.MeanFps.ToString("F0")) .Append(" fps (min ") .Append(latestFrame.MinFps.ToString("F0")) .Append(")\n"); stringBuilder.Append(latestFrame.MeanMs.ToString("F2")).Append(" ms p95 ").Append(latestFrame.P95Ms.ToString("F2")) .Append(" gc0/1k ") .Append(latestFrame.Gc0Per1000.ToString("F1")); if (LanServer.Running) { stringBuilder.Append("\nphone http://").Append(LanServer.Ip).Append(':') .Append(LanServer.Port) .Append(" token ") .Append(LanServer.Token); } List latestSections = SnitchCore.LatestSections; if (latestSections != null && latestSections.Count > 0) { stringBuilder.Append("\nsections"); int num = Mathf.Min(8, latestSections.Count); for (int i = 0; i < num; i++) { SectionRow sectionRow = latestSections[i]; stringBuilder.Append('\n').Append(sectionRow.Label).Append(" ") .Append(sectionRow.MsPerFrame.ToString("F2")) .Append(" ms ") .Append(sectionRow.PctFrame.ToString("F0")) .Append('%'); } } List latestStates = SnitchCore.LatestStates; if (latestStates != null && latestStates.Count > 0) { stringBuilder.Append("\nstates"); foreach (StateSnapshot item in latestStates) { stringBuilder.Append('\n').Append(item.Title).Append(' ') .Append(item.EffectiveTotal()) .Append(": "); for (int j = 0; j < item.Buckets.Count; j++) { if (j > 0) { stringBuilder.Append(' '); } stringBuilder.Append(item.Buckets[j].Name).Append('=').Append(item.Buckets[j].Count); } } } return stringBuilder.ToString(); } internal static string BuildPanelMetrics(string panelId) { if (string.IsNullOrEmpty(panelId)) { return ""; } StringBuilder stringBuilder = new StringBuilder(128); string text = panelId + "."; List latestCounters = SnitchCore.LatestCounters; if (latestCounters != null) { for (int i = 0; i < latestCounters.Count; i++) { CounterRow counterRow = latestCounters[i]; if (!(counterRow.Id != panelId) || counterRow.Id.StartsWith(text)) { string value = (counterRow.Id.StartsWith(text) ? counterRow.Id.Substring(text.Length) : counterRow.Id); Line(stringBuilder, $"{value} = {counterRow.Value:0.##} {counterRow.Unit}".TrimEnd()); } } } List latestStates = SnitchCore.LatestStates; if (latestStates != null) { for (int j = 0; j < latestStates.Count; j++) { StateSnapshot stateSnapshot = latestStates[j]; if (stateSnapshot.Id != panelId && !stateSnapshot.Id.StartsWith(text)) { continue; } StringBuilder stringBuilder2 = new StringBuilder(); stringBuilder2.Append(stateSnapshot.Title).Append(' ').Append(stateSnapshot.EffectiveTotal()) .Append(": "); for (int k = 0; k < stateSnapshot.Buckets.Count; k++) { if (k > 0) { stringBuilder2.Append(' '); } stringBuilder2.Append(stateSnapshot.Buckets[k].Name).Append('=').Append(stateSnapshot.Buckets[k].Count); } Line(stringBuilder, stringBuilder2.ToString()); } } return stringBuilder.ToString(); } private static void Line(StringBuilder sb, string s) { if (sb.Length > 0) { sb.Append('\n'); } sb.Append(s); } } internal static class QrCode { private sealed class BitBuffer { private readonly List _bytes = new List(); private int _bitLen; internal int Length => _bitLen; internal void Append(int value, int bits) { for (int num = bits - 1; num >= 0; num--) { if (_bitLen % 8 == 0) { _bytes.Add(0); } if (((value >> num) & 1) != 0) { _bytes[_bitLen / 8] |= (byte)(1 << 7 - _bitLen % 8); } _bitLen++; } } internal byte[] ToBytes() { return _bytes.ToArray(); } } private static readonly int[][] EccL; private static readonly int[][] AlignPos; private static readonly int[] Exp; private static readonly int[] Log; internal static bool[,] Encode(string text) { byte[] bytes = Encoding.UTF8.GetBytes(text ?? ""); int num = -1; for (int i = 1; i <= 10; i++) { int num2 = ((i <= 9) ? 8 : 16); int num3 = 4 + num2 + bytes.Length * 8; int[] array = EccL[i - 1]; int num4 = array[1] * array[2] + array[3] * array[4]; if (num3 <= num4 * 8) { num = i; break; } } if (num < 1) { return null; } byte[] cw = BuildCodewords(bytes, num); int num5 = 17 + 4 * num; int[,] array2 = new int[num5, num5]; for (int j = 0; j < num5; j++) { for (int k = 0; k < num5; k++) { array2[j, k] = -1; } } bool[,] fn = new bool[num5, num5]; DrawFunctionPatterns(array2, fn, num, num5); DrawCodewords(array2, fn, cw, num5); int num6 = int.MaxValue; int[,] array3 = null; for (int l = 0; l < 8; l++) { int[,] array4 = (int[,])array2.Clone(); ApplyMask(array4, fn, l, num5); DrawFormatBits(array4, num, l, num5); int num7 = Penalty(array4, num5); if (num7 < num6) { num6 = num7; array3 = array4; } } bool[,] array5 = new bool[num5, num5]; for (int m = 0; m < num5; m++) { for (int n = 0; n < num5; n++) { array5[m, n] = array3[m, n] == 1; } } return array5; } private static byte[] BuildCodewords(byte[] data, int version) { int[] obj = EccL[version - 1]; int num = obj[0]; int num2 = obj[1]; int num3 = obj[2]; int num4 = obj[3]; int num5 = obj[4]; int num6 = num2 + num4; int num7 = num2 * num3 + num4 * num5; BitBuffer bitBuffer = new BitBuffer(); bitBuffer.Append(4, 4); bitBuffer.Append(data.Length, (version <= 9) ? 8 : 16); foreach (byte value in data) { bitBuffer.Append(value, 8); } int num8 = num7 * 8; int bits = Math.Min(4, num8 - bitBuffer.Length); bitBuffer.Append(0, bits); while (bitBuffer.Length % 8 != 0) { bitBuffer.Append(0, 1); } byte b = 236; while (bitBuffer.Length < num8) { bitBuffer.Append(b, 8); b = (byte)((b == 236) ? 17u : 236u); } byte[] sourceArray = bitBuffer.ToBytes(); byte[][] array = new byte[num6][]; byte[][] array2 = new byte[num6][]; int[] gen = RsGenerator(num); int num9 = 0; for (int j = 0; j < num6; j++) { int num10 = ((j < num2) ? num3 : num5); byte[] array3 = new byte[num10]; Array.Copy(sourceArray, num9, array3, 0, num10); num9 += num10; array[j] = array3; array2[j] = RsEncode(array3, gen); } List list = new List(num7 + num6 * num); int num11 = Math.Max(num3, num5); for (int k = 0; k < num11; k++) { for (int l = 0; l < num6; l++) { if (k < array[l].Length) { list.Add(array[l][k]); } } } for (int m = 0; m < num; m++) { for (int n = 0; n < num6; n++) { list.Add(array2[n][m]); } } return list.ToArray(); } static QrCode() { EccL = new int[10][] { new int[5] { 7, 1, 19, 0, 0 }, new int[5] { 10, 1, 34, 0, 0 }, new int[5] { 15, 1, 55, 0, 0 }, new int[5] { 20, 1, 80, 0, 0 }, new int[5] { 26, 1, 108, 0, 0 }, new int[5] { 18, 2, 68, 0, 0 }, new int[5] { 20, 2, 78, 0, 0 }, new int[5] { 24, 2, 97, 0, 0 }, new int[5] { 30, 2, 116, 0, 0 }, new int[5] { 18, 2, 68, 2, 69 } }; AlignPos = new int[10][] { new int[0], new int[2] { 6, 18 }, new int[2] { 6, 22 }, new int[2] { 6, 26 }, new int[2] { 6, 30 }, new int[2] { 6, 34 }, new int[3] { 6, 22, 38 }, new int[3] { 6, 24, 42 }, new int[3] { 6, 26, 46 }, new int[3] { 6, 28, 50 } }; Exp = new int[256]; Log = new int[256]; int num = 1; for (int i = 0; i < 256; i++) { Exp[i] = num; if (i < 255) { Log[num] = i; } num <<= 1; if ((num & 0x100) != 0) { num ^= 0x11D; } } } private static int Mul(int a, int b) { if (a != 0 && b != 0) { return Exp[(Log[a] + Log[b]) % 255]; } return 0; } private static int[] RsGenerator(int degree) { int[] array = new int[1] { 1 }; for (int i = 0; i < degree; i++) { array = MulPoly(array, new int[2] { 1, Exp[i] }); } return array; } private static int[] MulPoly(int[] a, int[] b) { int[] array = new int[a.Length + b.Length - 1]; for (int i = 0; i < a.Length; i++) { for (int j = 0; j < b.Length; j++) { array[i + j] ^= Mul(a[i], b[j]); } } return array; } private static byte[] RsEncode(byte[] data, int[] gen) { int num = gen.Length - 1; int[] array = new int[data.Length + num]; for (int i = 0; i < data.Length; i++) { array[i] = data[i]; } for (int j = 0; j < data.Length; j++) { int num2 = array[j]; if (num2 != 0) { for (int k = 0; k < gen.Length; k++) { array[j + k] ^= Mul(gen[k], num2); } } } byte[] array2 = new byte[num]; for (int l = 0; l < num; l++) { array2[l] = (byte)array[data.Length + l]; } return array2; } private static void DrawFunctionPatterns(int[,] m, bool[,] fn, int version, int size) { for (int i = 0; i < size; i++) { Set(m, fn, 6, i, i % 2 == 0); Set(m, fn, i, 6, i % 2 == 0); } DrawFinder(m, fn, 0, 0, size); DrawFinder(m, fn, size - 7, 0, size); DrawFinder(m, fn, 0, size - 7, size); int[] array = AlignPos[version - 1]; for (int j = 0; j < array.Length; j++) { for (int k = 0; k < array.Length; k++) { int num = array[j]; int num2 = array[k]; if ((num != 6 || num2 != 6) && (num != 6 || num2 != size - 7) && (num != size - 7 || num2 != 6)) { DrawAlignment(m, fn, num, num2); } } } Set(m, fn, size - 8, 8, dark: true); ReserveFormat(fn, size); if (version >= 7) { ReserveVersion(fn, size); } } private static void DrawFinder(int[,] m, bool[,] fn, int row, int col, int size) { for (int i = -1; i <= 7; i++) { for (int j = -1; j <= 7; j++) { int num = row + i; int num2 = col + j; if (num >= 0 && num < size && num2 >= 0 && num2 < size) { bool dark = i >= 0 && i <= 6 && j >= 0 && j <= 6 && (i == 0 || i == 6 || j == 0 || j == 6 || (i >= 2 && i <= 4 && j >= 2 && j <= 4)); Set(m, fn, num, num2, dark); } } } } private static void DrawAlignment(int[,] m, bool[,] fn, int row, int col) { for (int i = -2; i <= 2; i++) { for (int j = -2; j <= 2; j++) { Set(m, fn, row + i, col + j, Math.Max(Math.Abs(i), Math.Abs(j)) != 1); } } } private static void ReserveFormat(bool[,] fn, int size) { for (int i = 0; i < 6; i++) { fn[i, 8] = true; fn[8, i] = true; } fn[7, 8] = true; fn[8, 7] = true; fn[8, 8] = true; for (int j = 0; j < 8; j++) { fn[size - 1 - j, 8] = true; fn[8, size - 1 - j] = true; } } private static void ReserveVersion(bool[,] fn, int size) { for (int i = 0; i < 6; i++) { for (int j = 0; j < 3; j++) { fn[size - 11 + j, i] = true; fn[i, size - 11 + j] = true; } } } private static void DrawCodewords(int[,] m, bool[,] fn, byte[] cw, int size) { int num = 0; int num2 = cw.Length * 8; for (int num3 = size - 1; num3 > 0; num3 -= 2) { if (num3 == 6) { num3 = 5; } for (int i = 0; i < size; i++) { int num4 = ((((num3 + 1) & 2) == 0) ? (size - 1 - i) : i); for (int j = 0; j < 2; j++) { int num5 = num3 - j; if (!fn[num4, num5]) { bool flag = false; if (num < num2) { flag = ((cw[num >> 3] >> 7 - (num & 7)) & 1) == 1; } m[num4, num5] = (flag ? 1 : 0); num++; } } } } } private static void ApplyMask(int[,] m, bool[,] fn, int mask, int size) { for (int i = 0; i < size; i++) { for (int j = 0; j < size; j++) { if (!fn[i, j] && mask switch { 0 => (i + j) % 2 == 0, 1 => i % 2 == 0, 2 => j % 3 == 0, 3 => (i + j) % 3 == 0, 4 => (i / 2 + j / 3) % 2 == 0, 5 => i * j % 2 + i * j % 3 == 0, 6 => (i * j % 2 + i * j % 3) % 2 == 0, _ => ((i + j) % 2 + i * j % 3) % 2 == 0, }) { m[i, j] ^= 1; } } } } private static void DrawFormatBits(int[,] m, int version, int mask, int size) { int num = 8 | mask; int num2 = num; for (int i = 0; i < 10; i++) { num2 = (num2 << 1) ^ ((num2 >> 9) * 1335); } int value = ((num << 10) | num2) ^ 0x5412; for (int j = 0; j < 15; j++) { int v = Bit(value, j); if (j < 6) { SetVal(m, j, 8, v); } else if (j < 8) { SetVal(m, j + 1, 8, v); } else { SetVal(m, size - 15 + j, 8, v); } if (j < 8) { SetVal(m, 8, size - j - 1, v); } else if (j < 9) { SetVal(m, 8, 7, v); } else { SetVal(m, 8, 15 - j - 1, v); } } SetVal(m, size - 8, 8, 1); if (version >= 7) { int num3 = version; for (int k = 0; k < 12; k++) { num3 = (num3 << 1) ^ ((num3 >> 11) * 7973); } int value2 = (version << 12) | num3; for (int l = 0; l < 18; l++) { int v2 = Bit(value2, l); int num4 = l / 3; int num5 = l % 3; SetVal(m, size - 11 + num5, num4, v2); SetVal(m, num4, size - 11 + num5, v2); } } } private static int Penalty(int[,] m, int size) { int num = 0; for (int i = 0; i < size; i++) { int num2 = -1; int num3 = 0; for (int j = 0; j < size; j++) { int num4 = m[i, j]; if (num4 == num2) { num3++; if (num3 == 5) { num += 3; } else if (num3 > 5) { num++; } } else { num2 = num4; num3 = 1; } } } for (int k = 0; k < size; k++) { int num5 = -1; int num6 = 0; for (int l = 0; l < size; l++) { int num7 = m[l, k]; if (num7 == num5) { num6++; if (num6 == 5) { num += 3; } else if (num6 > 5) { num++; } } else { num5 = num7; num6 = 1; } } } for (int n = 0; n < size - 1; n++) { for (int num8 = 0; num8 < size - 1; num8++) { int num9 = m[n, num8]; if (num9 == m[n, num8 + 1] && num9 == m[n + 1, num8] && num9 == m[n + 1, num8 + 1]) { num += 3; } } } int[] pat = new int[11] { 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0 }; int[] pat2 = new int[11] { 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1 }; for (int num10 = 0; num10 < size; num10++) { for (int num11 = 0; num11 <= size - 11; num11++) { if (MatchPattern(m, num10, num11, horizontal: true, pat) || MatchPattern(m, num10, num11, horizontal: true, pat2)) { num += 40; } } } for (int num12 = 0; num12 < size; num12++) { for (int num13 = 0; num13 <= size - 11; num13++) { if (MatchPattern(m, num13, num12, horizontal: false, pat) || MatchPattern(m, num13, num12, horizontal: false, pat2)) { num += 40; } } } int num14 = 0; for (int num15 = 0; num15 < size; num15++) { for (int num16 = 0; num16 < size; num16++) { if (m[num15, num16] == 1) { num14++; } } } int num17 = size * size; int num18 = Math.Abs(num14 * 100 / num17 - 50) / 5; return num + num18 * 10; } private static bool MatchPattern(int[,] m, int r, int c, bool horizontal, int[] pat) { for (int i = 0; i < 11; i++) { if ((horizontal ? m[r, c + i] : m[r + i, c]) != pat[i]) { return false; } } return true; } private static void Set(int[,] m, bool[,] fn, int r, int c, bool dark) { m[r, c] = (dark ? 1 : 0); fn[r, c] = true; } private static void SetVal(int[,] m, int r, int c, int v) { m[r, c] = v; } private static int Bit(int value, int i) { return (value >> i) & 1; } } internal static class QrImage { private const int Scale = 6; private const int Quiet = 4; private static int[] _cache; private static string _cacheUrl; internal static int[] Build() { if (!LanServer.Running) { _cache = null; _cacheUrl = null; return null; } string text = (WebAssets.HasBundledDashboard() ? ("&lan=" + LanServer.Ip + ":" + LanServer.Port) : ""); string text2; if (RelayHost.Running && !string.IsNullOrEmpty(RelayHost.Code)) { text2 = "https://snitch.doodesch.de/#join=" + RelayHost.Code + "&t=" + LanServer.Token + text; } else { if (text.Length <= 0) { _cache = null; _cacheUrl = null; return null; } text2 = "http://" + LanServer.Ip + ":" + LanServer.Port + "/#remote&t=" + LanServer.Token; } if (text2 == _cacheUrl && _cache != null) { return _cache; } bool[,] array = QrCode.Encode(text2); if (array == null) { return null; } _cache = Rasterize(array); _cacheUrl = text2; return _cache; } private static int[] Rasterize(bool[,] m) { int length = m.GetLength(0); int num = (length + 8) * 6; int[] array = new int[2 + num * num]; array[0] = num; array[1] = num; for (int i = 0; i < num * num; i++) { array[2 + i] = -1; } for (int j = 0; j < length; j++) { for (int k = 0; k < length; k++) { if (!m[j, k]) { continue; } int num2 = (k + 4) * 6; int num3 = (j + 4) * 6; for (int l = 0; l < 6; l++) { int num4 = 2 + (num3 + l) * num + num2; for (int n = 0; n < 6; n++) { array[num4 + n] = -16777216; } } } } return array; } } } namespace Snitch.Server { internal static class LanServer { private const int MaxRequestBytes = 65536; private const int SocketTimeoutMs = 5000; private static TcpListener _listener; private static Thread _accept; private static volatile bool _running; private static int _port; private static string _ip = "127.0.0.1"; private static string _token = ""; private const string EmptySnapshot = "{\"type\":\"snapshot\",\"v\":1,\"frame\":{},\"sections\":[],\"counters\":[],\"states\":[],\"panels\":[],\"logs\":{\"timeline\":[]}}"; internal static bool Running => _running; internal static int Port => _port; internal static string Ip => _ip; internal static string Token => _token; internal static void Start(int port) { if (_running) { return; } _port = port; _ip = DetectLanIp(); _token = (string.IsNullOrEmpty(Preferences.ServerToken) ? Guid.NewGuid().ToString("N").Substring(0, 8) : Preferences.ServerToken); try { _listener = new TcpListener(IPAddress.Any, port); _listener.Start(); _running = true; _accept = new Thread(AcceptLoop) { IsBackground = true, Name = "Snitch-LanServer" }; _accept.Start(); Instance log = Core.Log; if (log != null) { log.Msg($"[snitch] LAN remote on http://{_ip}:{port}/ (token {_token}). Scan the QR in the dashboard from your phone."); } Instance log2 = Core.Log; if (log2 != null) { log2.Msg($"[snitch] if the phone can't connect, allow Schedule I (or TCP port {port}) through Windows Firewall on your Private network."); } } catch (Exception ex) { _running = false; Instance log3 = Core.Log; if (log3 != null) { log3.Error($"[snitch] LAN remote failed to start on {port}: {ex.Message} (port in use? change LanPort)"); } } } internal static void Stop() { _running = false; try { _listener?.Stop(); } catch { } _listener = null; } internal static string LanInfoJson(bool includeToken) { if (!_running) { return "\"lan\":{\"enabled\":false}"; } StringBuilder stringBuilder = new StringBuilder(160); stringBuilder.Append("\"lan\":{\"enabled\":true,\"ip\":\"").Append(_ip).Append("\",\"port\":") .Append(_port) .Append(",\"url\":\"http://") .Append(_ip) .Append(':') .Append(_port) .Append("/\"") .Append(",\"bundled\":") .Append(WebAssets.HasBundledDashboard() ? "true" : "false"); if (includeToken) { stringBuilder.Append(",\"token\":\"").Append(_token).Append('"'); } stringBuilder.Append('}'); return stringBuilder.ToString(); } private static void AcceptLoop() { while (_running) { TcpClient client; try { client = _listener.AcceptTcpClient(); } catch { if (!_running) { break; } continue; } Task.Run(delegate { Handle(client); }); } } private static void Handle(TcpClient client) { try { using (client) { using NetworkStream networkStream = client.GetStream(); networkStream.ReadTimeout = 5000; networkStream.WriteTimeout = 5000; if (ReadRequest(networkStream, out var method, out var path, out var query, out var body)) { Route(networkStream, method, path, ParseQuery(query), body); } } } catch { } } private static void Route(NetworkStream stream, string method, string path, Dictionary q, string body) { path = path.ToLowerInvariant(); if (method == "OPTIONS") { WriteStatus(stream, 204, "No Content"); return; } switch (path) { case "/health": WriteJson(stream, WireProtocol.BuildHealth(SnitchCore.LastFrame, SnitchCore.LastScene, LanInfoJson(includeToken: false))); return; case "/snapshot": case "/caps": case "/control": if (!TokenOk(q)) { WriteJson(stream, "{\"ok\":false,\"error\":\"token\"}", 401, "Unauthorized"); return; } switch (path) { case "/snapshot": WriteJson(stream, SnitchCore.LatestJson ?? "{\"type\":\"snapshot\",\"v\":1,\"frame\":{},\"sections\":[],\"counters\":[],\"states\":[],\"panels\":[],\"logs\":{\"timeline\":[]}}"); return; case "/caps": WriteJson(stream, SnitchCore.CapsJson ?? WireProtocol.BuildCaps()); return; case "/control": { string value; string cmd = (q.TryGetValue("cmd", out value) ? value : ""); string value2; string id = (q.TryGetValue("id", out value2) ? value2 : ""); string value4; string value3 = (q.TryGetValue("value", out value4) ? value4 : ""); WriteJson(stream, SnitchServer.ApplyControl(cmd, id, value3)); return; } } break; } byte[] bytes; string contentType; byte[] bytes2; string contentType2; if (method != "GET") { WriteStatus(stream, 405, "Method Not Allowed"); } else if (WebAssets.TryResolve(path, out bytes, out contentType)) { WriteBytes(stream, bytes, contentType, path.StartsWith("/assets/")); } else if (WebAssets.TryResolve("/index.html", out bytes2, out contentType2)) { WriteBytes(stream, bytes2, contentType2, cache: false); } else { WriteStatus(stream, 404, "Not Found"); } } private static bool TokenOk(Dictionary q) { if (q.TryGetValue("token", out var value) && value == _token) { return true; } if (q.TryGetValue("x-snitch-token", out var value2) && value2 == _token) { return true; } return false; } private static bool ReadRequest(NetworkStream stream, out string method, out string path, out string query, out string body) { method = (path = (query = (body = ""))); MemoryStream memoryStream = new MemoryStream(); byte[] array = new byte[4096]; int num = -1; while (num < 0 && memoryStream.Length < 65536) { int num2; try { num2 = stream.Read(array, 0, array.Length); } catch { return false; } if (num2 <= 0) { break; } memoryStream.Write(array, 0, num2); num = IndexOfDoubleCrlf(memoryStream.GetBuffer(), (int)memoryStream.Length); } if (num < 0) { return false; } string[] array2 = Encoding.ASCII.GetString(memoryStream.GetBuffer(), 0, num).Split(new string[1] { "\r\n" }, StringSplitOptions.None); if (array2.Length == 0) { return false; } string[] array3 = array2[0].Split(' '); if (array3.Length < 2) { return false; } method = array3[0].ToUpperInvariant(); string text = array3[1]; int num3 = text.IndexOf('?'); if (num3 >= 0) { path = text.Substring(0, num3); query = text.Substring(num3 + 1); } else { path = text; } int result = 0; for (int i = 1; i < array2.Length; i++) { int num4 = array2[i].IndexOf(':'); if (num4 > 0) { string text2 = array2[i].Substring(0, num4).Trim().ToLowerInvariant(); string text3 = array2[i].Substring(num4 + 1).Trim(); if (text2 == "content-length") { int.TryParse(text3, out result); } else if (text2 == "x-snitch-token") { query = ((query.Length > 0) ? (query + "&") : "") + "x-snitch-token=" + Uri.EscapeDataString(text3); } } } if (result > 0) { int num5 = num + 4; int num6; for (int j = (int)memoryStream.Length - num5; j < result; j += num6) { if (memoryStream.Length >= 65536) { break; } try { num6 = stream.Read(array, 0, array.Length); } catch { break; } if (num6 <= 0) { break; } memoryStream.Write(array, 0, num6); } int num7 = Math.Min(result, (int)memoryStream.Length - num5); if (num7 > 0) { body = Encoding.UTF8.GetString(memoryStream.GetBuffer(), num5, num7); } } return true; } private static int IndexOfDoubleCrlf(byte[] b, int len) { for (int i = 0; i + 3 < len; i++) { if (b[i] == 13 && b[i + 1] == 10 && b[i + 2] == 13 && b[i + 3] == 10) { return i; } } return -1; } private static Dictionary ParseQuery(string query) { Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); if (string.IsNullOrEmpty(query)) { return dictionary; } string[] array = query.Split('&'); foreach (string text in array) { if (text.Length != 0) { int num = text.IndexOf('='); string text2 = ((num < 0) ? text : text.Substring(0, num)); string text3 = ((num < 0) ? "" : text.Substring(num + 1)); try { text2 = Uri.UnescapeDataString(text2); text3 = Uri.UnescapeDataString(text3); } catch { } dictionary[text2] = text3; } } return dictionary; } private static void WriteJson(NetworkStream stream, string json, int code = 200, string reason = "OK") { WriteResponse(stream, code, reason, "application/json", Encoding.UTF8.GetBytes(json ?? ""), cache: false); } private static void WriteBytes(NetworkStream stream, byte[] bytes, string contentType, bool cache) { WriteResponse(stream, 200, "OK", contentType, bytes, cache); } private static void WriteStatus(NetworkStream stream, int code, string reason) { WriteResponse(stream, code, reason, "text/plain; charset=utf-8", Encoding.UTF8.GetBytes(reason), cache: false); } private static void WriteResponse(NetworkStream stream, int code, string reason, string contentType, byte[] body, bool cache) { try { StringBuilder stringBuilder = new StringBuilder(256); stringBuilder.Append("HTTP/1.1 ").Append(code).Append(' ') .Append(reason) .Append("\r\n"); stringBuilder.Append("Content-Type: ").Append(contentType).Append("\r\n"); stringBuilder.Append("Content-Length: ").Append(body.Length).Append("\r\n"); stringBuilder.Append(cache ? "Cache-Control: public, max-age=2592000, immutable\r\n" : "Cache-Control: no-store\r\n"); stringBuilder.Append("Connection: close\r\n\r\n"); byte[] bytes = Encoding.ASCII.GetBytes(stringBuilder.ToString()); stream.Write(bytes, 0, bytes.Length); if (body.Length != 0) { stream.Write(body, 0, body.Length); } stream.Flush(); } catch { } } private static string DetectLanIp() { try { using Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); socket.Connect("8.8.8.8", 65530); if (socket.LocalEndPoint is IPEndPoint iPEndPoint && !IPAddress.IsLoopback(iPEndPoint.Address)) { return iPEndPoint.Address.ToString(); } } catch { } try { IPAddress[] hostAddresses = Dns.GetHostAddresses(Dns.GetHostName()); foreach (IPAddress iPAddress in hostAddresses) { if (iPAddress.AddressFamily == AddressFamily.InterNetwork && !IPAddress.IsLoopback(iPAddress)) { string text = iPAddress.ToString(); if (!text.StartsWith("169.254")) { return text; } } } } catch { } return "127.0.0.1"; } } internal static class RelayCrypto { private static byte[] DeriveKey(string token) { using SHA256 sHA = SHA256.Create(); return sHA.ComputeHash(Encoding.UTF8.GetBytes("snitch-relay:v1:" + token)); } internal static string Encrypt(string token, string json) { byte[] key = DeriveKey(token); byte[] array = new byte[12]; RandomNumberGenerator.Fill(array); byte[] bytes = Encoding.UTF8.GetBytes(json); byte[] array2 = new byte[bytes.Length]; byte[] array3 = new byte[16]; using (AesGcm aesGcm = new AesGcm(key)) { aesGcm.Encrypt(array, bytes, array2, array3); } byte[] array4 = new byte[12 + array2.Length + 16]; Buffer.BlockCopy(array, 0, array4, 0, 12); Buffer.BlockCopy(array2, 0, array4, 12, array2.Length); Buffer.BlockCopy(array3, 0, array4, 12 + array2.Length, 16); return Convert.ToBase64String(array4); } internal static string Decrypt(string token, string b64) { byte[] array = Convert.FromBase64String(b64); if (array.Length < 28) { return null; } byte[] key = DeriveKey(token); byte[] array2 = new byte[12]; byte[] array3 = new byte[16]; int num = array.Length - 12 - 16; byte[] array4 = new byte[num]; Buffer.BlockCopy(array, 0, array2, 0, 12); Buffer.BlockCopy(array, 12, array4, 0, num); Buffer.BlockCopy(array, 12 + num, array3, 0, 16); byte[] array5 = new byte[num]; using (AesGcm aesGcm = new AesGcm(key)) { aesGcm.Decrypt(array2, array4, array3, array5); } return Encoding.UTF8.GetString(array5); } } internal static class RelayHost { private const string RelayBase = "wss://relay.doodesch.de/?app=snitch&role=host&code="; private static volatile bool _running; private static CancellationTokenSource _cts; private static string _code = ""; private static volatile int _clients; internal static bool Running => _running; internal static string Code => _code; internal static void Start(string code) { if (!_running) { _code = code ?? ""; _clients = 0; _running = true; _cts = new CancellationTokenSource(); Task.Run(() => Loop(_cts.Token)); Instance log = Core.Log; if (log != null) { log.Msg("[snitch] relay host on (code " + _code + ") - phone can connect via relay.doodesch.de from any network."); } } } internal static void Stop() { _running = false; _clients = 0; try { _cts?.Cancel(); } catch { } _cts = null; } private static async Task Loop(CancellationToken ct) { while (_running && !ct.IsCancellationRequested) { try { await Session(ct); } catch (Exception ex) { if (_running) { Instance log = Core.Log; if (log != null) { log.Warning("[snitch] relay host dropped: " + ex.Message); } } } if (_running && !ct.IsCancellationRequested) { try { await Task.Delay(3000, ct); } catch { } } } } private static async Task Session(CancellationToken ct) { using ClientWebSocket ws = new ClientWebSocket(); await ws.ConnectAsync(new Uri("wss://relay.doodesch.de/?app=snitch&role=host&code=" + Uri.EscapeDataString(_code)), ct); _clients = 0; Task task = ReceiveLoop(ws, ct); Task task2 = SendLoop(ws, ct); await Task.WhenAny(task, task2); try { await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "bye", CancellationToken.None); } catch { } } private static async Task SendLoop(ClientWebSocket ws, CancellationToken ct) { while (!ct.IsCancellationRequested && ws.State == WebSocketState.Open) { if (_clients > 0) { string latestJson = SnitchCore.LatestJson; if (!string.IsNullOrEmpty(latestJson)) { string s = "{\"d\":\"" + RelayCrypto.Encrypt(LanServer.Token, latestJson) + "\"}"; byte[] bytes = Encoding.UTF8.GetBytes(s); await ws.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, endOfMessage: true, ct); } } await Task.Delay(300, ct); } } private static async Task ReceiveLoop(ClientWebSocket ws, CancellationToken ct) { byte[] buf = new byte[16384]; StringBuilder sb = new StringBuilder(); while (!ct.IsCancellationRequested && ws.State == WebSocketState.Open) { sb.Clear(); WebSocketReceiveResult webSocketReceiveResult; do { webSocketReceiveResult = await ws.ReceiveAsync(new ArraySegment(buf), ct); if (webSocketReceiveResult.MessageType == WebSocketMessageType.Close) { return; } sb.Append(Encoding.UTF8.GetString(buf, 0, webSocketReceiveResult.Count)); } while (!webSocketReceiveResult.EndOfMessage); HandleFrame(sb.ToString()); } } private static void HandleFrame(string text) { if (text.IndexOf("__relay", StringComparison.Ordinal) >= 0) { if (text.IndexOf("nohost", StringComparison.Ordinal) >= 0) { _clients = 0; } else if (text.IndexOf("join", StringComparison.Ordinal) >= 0 || text.IndexOf("leave", StringComparison.Ordinal) >= 0) { _clients = Math.Max(0, ExtractInt(text, "n")); } return; } string text2 = ExtractField(text, "d"); if (string.IsNullOrEmpty(text2)) { return; } try { string text3 = RelayCrypto.Decrypt(LanServer.Token, text2); if (!string.IsNullOrEmpty(text3)) { SnitchServer.ApplyControl(ExtractField(text3, "cmd"), ExtractField(text3, "id"), ExtractField(text3, "value")); } } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Warning("[snitch] relay control failed: " + ex.Message); } } } private static string ExtractField(string body, string key) { if (string.IsNullOrEmpty(body)) { return null; } int num = body.IndexOf("\"" + key + "\"", StringComparison.Ordinal); if (num < 0) { return null; } int num2 = body.IndexOf(':', num + key.Length + 2); if (num2 < 0) { return null; } int i; for (i = num2 + 1; i < body.Length && (body[i] == ' ' || body[i] == '\t'); i++) { } if (i >= body.Length) { return null; } if (body[i] == '"') { int num3 = body.IndexOf('"', i + 1); if (num3 < 0) { return null; } return body.Substring(i + 1, num3 - i - 1); } int j; for (j = i; j < body.Length && body[j] != ',' && body[j] != '}' && body[j] != ']'; j++) { } return body.Substring(i, j - i).Trim(); } private static int ExtractInt(string body, string key) { if (!int.TryParse(ExtractField(body, key), out var result)) { return 0; } return result; } } internal static class SnitchServer { private sealed class Session { internal readonly WebSocket Ws; internal readonly SemaphoreSlim Gate = new SemaphoreSlim(1, 1); internal Session(WebSocket ws) { Ws = ws; } } private const int MaxClients = 8; private const int SendTimeoutMs = 10000; private static HttpListener _listener; private static Thread _accept; private static volatile bool _running; private static CancellationTokenSource _cts; private static int _port; private static string _token = ""; private static string[] _origins = Array.Empty(); private static readonly List _sockets = new List(); private static readonly object _lock = new object(); private static readonly ConcurrentQueue _mainQueue = new ConcurrentQueue(); internal static bool Running => _running; internal static int Port => _port; internal static int SocketCount { get { lock (_lock) { return _sockets.Count; } } } internal static void Start(int port, string token, string allowedOrigins) { if (_running) { return; } _port = port; _token = token ?? ""; _origins = ParseOrigins(allowedOrigins); try { _listener = new HttpListener(); _listener.Prefixes.Add($"http://127.0.0.1:{port}/"); _listener.Start(); _running = true; _cts = new CancellationTokenSource(); _accept = new Thread(AcceptLoop) { IsBackground = true, Name = "Snitch-Server" }; _accept.Start(); Instance log = Core.Log; if (log != null) { log.Msg($"[snitch] data server on http://127.0.0.1:{port}/ (ws://127.0.0.1:{port}/stream). token {((_token.Length > 0) ? "on" : "off")}."); } } catch (Exception ex) { _running = false; Instance log2 = Core.Log; if (log2 != null) { log2.Error($"[snitch] data server failed to start on {port}: {ex.Message} (port in use? change ServerPort)"); } } } internal static void Stop() { _running = false; try { _cts?.Cancel(); } catch { } lock (_lock) { foreach (Session socket in _sockets) { try { socket.Ws.Abort(); } catch { } try { socket.Gate.Dispose(); } catch { } } _sockets.Clear(); } try { _listener?.Stop(); _listener?.Close(); } catch { } try { _cts?.Dispose(); } catch { } _cts = null; _listener = null; } internal static void Pump() { int num = 0; Action result; while (num++ < 8 && _mainQueue.TryDequeue(out result)) { try { result(); } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Warning("[snitch] control failed: " + ex.Message); } } } } internal static void Broadcast(string json) { if (_running && !string.IsNullOrEmpty(json)) { byte[] bytes = Encoding.UTF8.GetBytes(json); Session[] array; lock (_lock) { array = _sockets.ToArray(); } for (int i = 0; i < array.Length; i++) { SendFireAndForget(array[i], bytes); } } } private static void SendFireAndForget(Session s, byte[] bytes) { if (s.Ws.State != WebSocketState.Open) { Remove(s); } else if (s.Gate.Wait(0)) { SendAndRelease(s, bytes); } } private static async Task SendAndRelease(Session s, byte[] bytes) { try { await SendWithTimeout(s.Ws, bytes, _cts?.Token ?? CancellationToken.None).ConfigureAwait(continueOnCapturedContext: false); } catch { Remove(s); try { s.Ws.Abort(); } catch { } } finally { try { s.Gate.Release(); } catch { } } } private static void Remove(Session s) { lock (_lock) { _sockets.Remove(s); } } private static void AcceptLoop() { while (_running) { HttpListenerContext ctx; try { ctx = _listener.GetContext(); } catch { if (!_running) { break; } continue; } Task.Run(() => HandleAsync(ctx)); } } private static async Task HandleAsync(HttpListenerContext ctx) { try { HttpListenerRequest request = ctx.Request; HttpListenerResponse response = ctx.Response; string origin = request.Headers["Origin"]; ApplyCors(response, origin); if (request.HttpMethod == "OPTIONS") { response.StatusCode = 204; response.Close(); return; } string text = request.Url.AbsolutePath.ToLowerInvariant(); if (request.IsWebSocketRequest) { if (!OriginAllowed(origin) || !TokenOk(request)) { response.StatusCode = 403; response.Close(); } else { await HandleWsAsync(ctx).ConfigureAwait(continueOnCapturedContext: false); } return; } switch (text) { case "/health": WriteJson(response, WireProtocol.BuildHealth(SnitchCore.LastFrame, SnitchCore.LastScene, LanServer.LanInfoJson(includeToken: true))); break; case "/snapshot": if (!TokenOk(request)) { response.StatusCode = 401; response.Close(); } else { WriteJson(response, SnitchCore.LatestJson ?? "{\"type\":\"snapshot\",\"v\":1,\"frame\":{},\"sections\":[],\"counters\":[],\"states\":[],\"panels\":[],\"logs\":{\"timeline\":[]}}"); } break; case "/caps": if (!TokenOk(request)) { response.StatusCode = 401; response.Close(); } else { WriteJson(response, SnitchCore.CapsJson ?? WireProtocol.BuildCaps()); } break; case "/control": if (!TokenOk(request)) { response.StatusCode = 401; response.Close(); } else { HandleControl(request, response); } break; default: ServeStatic(text, response); break; } } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Warning("[snitch] request error: " + ex.Message); } try { ctx.Response.Abort(); } catch { } } } private static void HandleControl(HttpListenerRequest req, HttpListenerResponse res) { string body = ""; try { using StreamReader streamReader = new StreamReader(req.InputStream, Encoding.UTF8); body = streamReader.ReadToEnd(); } catch { } string cmd = req.QueryString["cmd"] ?? ExtractField(body, "cmd"); string id = req.QueryString["id"] ?? ExtractField(body, "id"); string value = req.QueryString["value"] ?? ExtractField(body, "value"); WriteJson(res, ApplyControl(cmd, id, value)); } internal static string ApplyControl(string cmd, string id, string value) { cmd = (cmd ?? "").Trim().ToLowerInvariant(); switch (cmd) { case "start": _mainQueue.Enqueue(SnitchCore.Start); break; case "stop": _mainQueue.Enqueue(SnitchCore.Stop); break; case "reset": _mainQueue.Enqueue(delegate { SnitchCore.Stop(); SnitchCore.Start(); }); break; case "report": _mainQueue.Enqueue(delegate { try { ReportWriter.Write("all"); } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Warning("[snitch] report failed: " + ex.Message); } } }); break; case "action": if (string.IsNullOrEmpty(id)) { return "{\"ok\":false,\"error\":\"missing id\"}"; } _mainQueue.Enqueue(delegate { PanelRegistry.Invoke(id); }); break; case "toggle": { if (string.IsNullOrEmpty(id)) { return "{\"ok\":false,\"error\":\"missing id\"}"; } bool val = value == "true" || value == "1" || value == "on"; _mainQueue.Enqueue(delegate { PanelRegistry.SetToggle(id, val); }); break; } case "slider": { if (string.IsNullOrEmpty(id)) { return "{\"ok\":false,\"error\":\"missing id\"}"; } if (!double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var sv)) { return "{\"ok\":false,\"error\":\"bad value\"}"; } _mainQueue.Enqueue(delegate { PanelRegistry.SetSlider(id, sv); }); break; } default: return "{\"ok\":false,\"error\":\"unknown cmd\"}"; } return "{\"ok\":true,\"cmd\":\"" + cmd + "\"}"; } private static async Task HandleWsAsync(HttpListenerContext ctx) { lock (_lock) { if (_sockets.Count >= 8) { try { ctx.Response.StatusCode = 503; ctx.Response.Close(); return; } catch { return; } } } WebSocket ws; try { ws = (await ctx.AcceptWebSocketAsync(null).ConfigureAwait(continueOnCapturedContext: false)).WebSocket; } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Warning("[snitch] ws upgrade failed: " + ex.Message); } return; } CancellationToken ct = _cts?.Token ?? CancellationToken.None; Session session = new Session(ws); try { string latestJson = SnitchCore.LatestJson; if (!string.IsNullOrEmpty(latestJson)) { await SendWithTimeout(ws, Encoding.UTF8.GetBytes(latestJson), ct).ConfigureAwait(continueOnCapturedContext: false); } } catch { } lock (_lock) { _sockets.Add(session); } byte[] buf = new byte[4096]; try { while (!ct.IsCancellationRequested && ws.State == WebSocketState.Open && (await ws.ReceiveAsync(new ArraySegment(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(bytes), WebSocketMessageType.Text, endOfMessage: true, cts.Token).ConfigureAwait(continueOnCapturedContext: false); } private static void ServeStatic(string path, HttpListenerResponse res) { if (WebAssets.TryResolve(path, out var bytes, out var contentType)) { try { res.StatusCode = 200; res.ContentType = contentType; res.ContentLength64 = bytes.Length; res.OutputStream.Write(bytes, 0, bytes.Length); res.OutputStream.Close(); res.Close(); return; } catch { try { res.StatusCode = 500; res.Close(); return; } catch { return; } } } if (path == "/" || string.IsNullOrEmpty(path) || path == "/index.html") { WriteHtml(res, PlaceholderHtml()); return; } res.StatusCode = 404; res.Close(); } private static void ApplyCors(HttpListenerResponse res, string origin) { try { res.Headers["Access-Control-Allow-Origin"] = ((!OriginAllowed(origin)) ? "null" : (string.IsNullOrEmpty(origin) ? "*" : origin)); res.Headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"; res.Headers["Access-Control-Allow-Headers"] = "content-type, x-snitch-token"; res.Headers["Access-Control-Allow-Private-Network"] = "true"; res.Headers["Vary"] = "Origin"; } catch { } } private static bool OriginAllowed(string origin) { if (string.IsNullOrEmpty(origin)) { return true; } if (origin.StartsWith("http://localhost", StringComparison.OrdinalIgnoreCase) || origin.StartsWith("http://127.0.0.1", StringComparison.OrdinalIgnoreCase) || origin.StartsWith("https://localhost", StringComparison.OrdinalIgnoreCase)) { return true; } for (int i = 0; i < _origins.Length; i++) { if (string.Equals(_origins[i], origin, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } private static bool TokenOk(HttpListenerRequest req) { if (_token.Length == 0) { return true; } string text = req.Headers["x-snitch-token"]; if (string.IsNullOrEmpty(text)) { text = req.QueryString["token"]; } return text == _token; } private static string[] ParseOrigins(string csv) { if (string.IsNullOrWhiteSpace(csv)) { return Array.Empty(); } string[] array = csv.Split(','); List list = new List(array.Length); string[] array2 = array; for (int i = 0; i < array2.Length; i++) { string text = array2[i].Trim().TrimEnd('/'); if (text.Length > 0) { list.Add(text); } } return list.ToArray(); } private static void WriteJson(HttpListenerResponse res, string json) { Write(res, "application/json", Encoding.UTF8.GetBytes(json)); } private static void WriteHtml(HttpListenerResponse res, string html) { Write(res, "text/html; charset=utf-8", Encoding.UTF8.GetBytes(html)); } private static void Write(HttpListenerResponse res, string type, byte[] bytes) { try { res.StatusCode = 200; res.ContentType = type; res.ContentLength64 = bytes.Length; res.OutputStream.Write(bytes, 0, bytes.Length); res.OutputStream.Close(); } catch { } finally { try { res.Close(); } catch { } } } private static string ExtractField(string body, string key) { if (string.IsNullOrEmpty(body)) { return null; } int num = body.IndexOf("\"" + key + "\"", StringComparison.OrdinalIgnoreCase); if (num < 0) { return null; } int num2 = body.IndexOf(':', num + key.Length + 2); if (num2 < 0) { return null; } int i; for (i = num2 + 1; i < body.Length && (body[i] == ' ' || body[i] == '\t'); i++) { } if (i >= body.Length) { return null; } if (body[i] == '"') { int num3 = body.IndexOf('"', i + 1); if (num3 < 0) { return null; } return body.Substring(i + 1, num3 - i - 1); } int j; for (j = i; j < body.Length && body[j] != ',' && body[j] != '}' && body[j] != ']'; j++) { } return body.Substring(i, j - i).Trim(); } private static string PlaceholderHtml() { return "Snitch

Snitch data server

This loopback endpoint is live. The offline dashboard isn't bundled in this build yet.

Open the hosted dashboard and it will auto-connect to ws://127.0.0.1:" + _port + "/stream, or fetch /snapshot / /health / /caps directly.

Support: support.doodesch.de

"; } } internal static class WebAssets { private static string _wwwroot; internal static string Wwwroot => _wwwroot ?? (_wwwroot = Path.Combine(Directory.GetCurrentDirectory(), "Mods", "Snitch", "wwwroot")); internal static bool HasBundledDashboard() { try { return File.Exists(Path.Combine(Wwwroot, "index.html")); } catch { return false; } } internal static bool TryResolve(string path, out byte[] bytes, out string contentType) { bytes = null; contentType = null; if (string.IsNullOrEmpty(path) || path == "/") { path = "/index.html"; } string wwwroot = Wwwroot; string path2 = path.TrimStart('/').Replace('/', Path.DirectorySeparatorChar); string fullPath; try { fullPath = Path.GetFullPath(Path.Combine(wwwroot, path2)); } catch { return false; } if (!fullPath.StartsWith(wwwroot, StringComparison.OrdinalIgnoreCase) || !File.Exists(fullPath)) { return false; } try { bytes = File.ReadAllBytes(fullPath); contentType = ContentType(fullPath); return true; } catch { return false; } } internal static string ContentType(string path) { return Path.GetExtension(path).ToLowerInvariant() switch { ".html" => "text/html; charset=utf-8", ".js" => "text/javascript", ".css" => "text/css", ".json" => "application/json", ".svg" => "image/svg+xml", ".png" => "image/png", ".ico" => "image/x-icon", ".woff2" => "font/woff2", _ => "application/octet-stream", }; } } internal static class WireProtocol { internal const int Version = 1; internal const string CapsArray = "[\"panels\",\"logs\",\"phone-remote\"]"; private static readonly CultureInfo Inv = CultureInfo.InvariantCulture; internal static string BuildSnapshot(int frame, string scene) { FrameStats latestFrame = SnitchCore.LatestFrame; StringBuilder stringBuilder = new StringBuilder(8192); stringBuilder.Append("{\"type\":\"snapshot\",\"v\":").Append(1).Append(",\"t\":") .Append(frame) .Append(','); stringBuilder.Append("\"meta\":{\"mod\":\"Snitch\",\"version\":\"1.5.1\",\"scene\":\"").Append(Esc(scene)).Append("\",\"active\":") .Append(SnitchCore.Active ? "true" : "false") .Append(",\"caps\":") .Append("[\"panels\",\"logs\",\"phone-remote\"]") .Append("},"); stringBuilder.Append("\"frame\":{"); Num(stringBuilder, "meanMs", latestFrame.MeanMs); Num(stringBuilder, "medianMs", latestFrame.MedianMs); Num(stringBuilder, "p95Ms", latestFrame.P95Ms); Num(stringBuilder, "p99Ms", latestFrame.P99Ms); Num(stringBuilder, "minMs", latestFrame.MinMs); Num(stringBuilder, "maxMs", latestFrame.MaxMs); Num(stringBuilder, "meanFps", latestFrame.MeanFps); Num(stringBuilder, "minFps", latestFrame.MinFps); Num(stringBuilder, "gc0", latestFrame.Gc0Per1000); Num(stringBuilder, "gc1", latestFrame.Gc1Per1000); stringBuilder.Append("\"samples\":").Append(latestFrame.Samples).Append("},"); stringBuilder.Append("\"sections\":["); List 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 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 latestStates = SnitchCore.LatestStates; if (latestStates != null) { for (int k = 0; k < latestStates.Count; k++) { if (k > 0) { stringBuilder.Append(','); } StateSnapshot stateSnapshot = latestStates[k]; stringBuilder.Append("{\"id\":\"").Append(Esc(stateSnapshot.Id)).Append("\",\"title\":\"") .Append(Esc(stateSnapshot.Title)) .Append("\",\"total\":") .Append(stateSnapshot.EffectiveTotal()) .Append(",\"buckets\":["); for (int l = 0; l < stateSnapshot.Buckets.Count; l++) { if (l > 0) { stringBuilder.Append(','); } stringBuilder.Append("{\"name\":\"").Append(Esc(stateSnapshot.Buckets[l].Name)).Append("\",\"count\":") .Append(stateSnapshot.Buckets[l].Count) .Append('}'); } stringBuilder.Append("]}"); } } stringBuilder.Append("],"); AppendPanels(stringBuilder); stringBuilder.Append(','); AppendLogs(stringBuilder); stringBuilder.Append('}'); return stringBuilder.ToString(); } private static void AppendPanels(StringBuilder sb) { sb.Append("\"panels\":["); IReadOnlyList all = PanelRegistry.All; bool flag = true; for (int i = 0; i < all.Count; i++) { PanelModel panelModel = all[i]; if (!flag) { sb.Append(','); } flag = false; sb.Append("{\"id\":\"").Append(Esc(panelModel.Id)).Append("\",\"title\":\"") .Append(Esc(panelModel.Title)) .Append("\",\"hasLog\":") .Append(panelModel.HasLog ? "true" : "false") .Append(",\"text\":\""); sb.Append(Esc(EvalText(panelModel))).Append("\",\"actions\":["); for (int j = 0; j < panelModel.Actions.Count; j++) { if (j > 0) { sb.Append(','); } sb.Append("{\"id\":\"").Append(Esc(panelModel.Actions[j].Id)).Append("\",\"label\":\"") .Append(Esc(panelModel.Actions[j].Label)) .Append("\"}"); } sb.Append("],\"toggles\":["); for (int k = 0; k < panelModel.Toggles.Count; k++) { if (k > 0) { sb.Append(','); } bool flag2 = false; try { flag2 = panelModel.Toggles[k].Get != null && panelModel.Toggles[k].Get(); } catch { } sb.Append("{\"id\":\"").Append(Esc(panelModel.Toggles[k].Id)).Append("\",\"label\":\"") .Append(Esc(panelModel.Toggles[k].Label)) .Append("\",\"value\":") .Append(flag2 ? "true" : "false") .Append('}'); } sb.Append("],\"sliders\":["); for (int l = 0; l < panelModel.Sliders.Count; l++) { if (l > 0) { sb.Append(','); } SliderItem sliderItem = panelModel.Sliders[l]; sb.Append("{\"id\":\"").Append(Esc(sliderItem.Id)).Append("\",\"label\":\"") .Append(Esc(sliderItem.Label)) .Append("\",\"unit\":\"") .Append(Esc(sliderItem.Unit)) .Append("\",\"min\":") .Append(F(sliderItem.Min)) .Append(",\"max\":") .Append(F(sliderItem.Max)) .Append(",\"step\":") .Append(F(sliderItem.Step)) .Append(",\"value\":") .Append(F(sliderItem.Read())) .Append('}'); } sb.Append("]}"); } sb.Append(']'); } private static string EvalText(PanelModel p) { if (p.Texts.Count == 0) { return ""; } StringBuilder stringBuilder = new StringBuilder(128); for (int i = 0; i < p.Texts.Count; i++) { string value = null; try { value = p.Texts[i]?.Invoke(); } catch { } if (!string.IsNullOrEmpty(value)) { if (stringBuilder.Length > 0) { stringBuilder.Append('\n'); } stringBuilder.Append(value); } } return stringBuilder.ToString(); } private static void AppendLogs(StringBuilder sb) { sb.Append("\"logs\":{\"timeline\":["); List list = LogHub.Timeline(200); for (int i = 0; i < list.Count; i++) { if (i > 0) { sb.Append(','); } LogEntry logEntry = list[i]; sb.Append("{\"seq\":").Append(logEntry.Seq).Append(",\"t\":\"") .Append(Esc(logEntry.Time)) .Append("\",\"ch\":\"") .Append(Esc(logEntry.Ch)) .Append("\",\"lvl\":") .Append(logEntry.Lvl) .Append(",\"msg\":\"") .Append(Esc(logEntry.Msg)) .Append("\"}"); } sb.Append("]}"); } internal static string BuildHealth(int frame, string scene, string lanJson = null) { string text = "{\"ok\":true,\"mod\":\"Snitch\",\"version\":\"1.5.1\",\"caps\":[\"panels\",\"logs\",\"phone-remote\"],\"active\":" + (SnitchCore.Active ? "true" : "false") + ",\"scene\":\"" + Esc(scene) + "\",\"frame\":" + frame; if (!string.IsNullOrEmpty(lanJson)) { text = text + "," + lanJson; } return text + "}"; } internal static string BuildCaps() { return "{\"type\":\"caps\",\"v\":" + 1 + ",\"frameTime\":\"load-bearing\",\"gc\":\"load-bearing\",\"engineCounters\":\"unavailable\",\"perEntityAttribution\":\"viable\",\"note\":\"ProfilerRecorder is inert in this IL2CPP build; frame-time + GC are the truth. Per-entity vanilla cost attribution is viable. Causal subsystem cost uses the ablation 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 _idByLabel = new Dictionary(64); private static readonly List _all = new List(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 Report(double frameMeanMs) { List list = new List(_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 list = new List(); 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 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 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 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 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 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 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 Buckets = new List(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 _poll; public string Id { get; } public DelegateStateProvider(string id, Func poll) { Id = id; _poll = poll; } public StateSnapshot Poll() { return _poll(); } } private static readonly List _providers = new List(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 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 PollAll() { List list = new List(_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("state provider '" + _providers[i].Id + "' threw: " + ex.Message); } } } return list; } } internal static class CounterRegistry { private sealed class DelegateCounter : ICounterSource { private readonly Func _read; public string Id { get; } public string Unit { get; } public DelegateCounter(string id, Func read, string unit) { Id = id; _read = read; Unit = unit; } public double Read() { return _read(); } } private static readonly List _sources = new List(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 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 ReadAll() { List list = new List(_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("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 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.Instance; if ((Object)(object)instance == (Object)null) { _snap.Title = "Trash (no manager)"; return _snap; } List 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 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.Panels { internal sealed class ActionItem { public string Id; public string Label; public Action Run; } internal sealed class ToggleItem { public string Id; public string Label; public Func Get; public Action Set; } internal sealed class SliderItem { public string Id; public string Label; public string Unit; public double Min; public double Max; public double Step; public Func Get; public Action Set; public double Quantize(double v) { if (Max > Min) { if (v < Min) { v = Min; } else if (v > Max) { v = Max; } } if (Step > 0.0) { v = Min + Math.Round((v - Min) / Step) * Step; } return v; } public double Read() { if (Get == null) { return Min; } try { return Get(); } catch { return Min; } } } internal sealed class PanelModel { public string Id; public string Title; public bool HasLog; public readonly List> Texts = new List>(2); public readonly List Actions = new List(4); public readonly List Toggles = new List(4); public readonly List Sliders = new List(4); } internal static class PanelRegistry { private static readonly List _panels = new List(8); private static readonly Dictionary _byId = new Dictionary(8); private static readonly Dictionary _actions = new Dictionary(16); private static readonly Dictionary _toggles = new Dictionary(16); private static readonly Dictionary _sliders = new Dictionary(16); internal static IReadOnlyList All => _panels; internal static int Count => _panels.Count; internal static PanelModel GetOrCreate(string id, string title) { if (string.IsNullOrEmpty(id)) { id = "misc"; } if (!_byId.TryGetValue(id, out var value)) { value = new PanelModel { Id = id, Title = (string.IsNullOrEmpty(title) ? id : title) }; _byId[id] = value; _panels.Add(value); } else if (!string.IsNullOrEmpty(title)) { value.Title = title; } return value; } internal static PanelModel Get(string id) { if (string.IsNullOrEmpty(id)) { return null; } _byId.TryGetValue(id, out var value); return value; } internal static void RegisterPanel(string id, string title) { GetOrCreate(id, title); } internal static void RegisterText(string panelId, Func provider) { if (provider != null) { GetOrCreate(panelId, null).Texts.Add(provider); } } internal static void RegisterAction(string panelId, string actionId, string label, Action run) { if (run != null && !string.IsNullOrEmpty(actionId)) { PanelModel orCreate = GetOrCreate(panelId, null); ActionItem actionItem = new ActionItem { Id = actionId, Label = (label ?? actionId), Run = run }; orCreate.Actions.RemoveAll((ActionItem a) => a.Id == actionId); orCreate.Actions.Add(actionItem); _actions[actionId] = actionItem; } } internal static void RegisterToggle(string panelId, string toggleId, string label, Func get, Action set) { if (get != null && set != null && !string.IsNullOrEmpty(toggleId)) { PanelModel orCreate = GetOrCreate(panelId, null); ToggleItem toggleItem = new ToggleItem { Id = toggleId, Label = (label ?? toggleId), Get = get, Set = set }; orCreate.Toggles.RemoveAll((ToggleItem t) => t.Id == toggleId); orCreate.Toggles.Add(toggleItem); _toggles[toggleId] = toggleItem; } } internal static void RegisterSlider(string panelId, string sliderId, string label, double min, double max, double step, string unit, Func get, Action set) { if (get != null && set != null && !string.IsNullOrEmpty(sliderId) && !(max <= min)) { PanelModel orCreate = GetOrCreate(panelId, null); SliderItem sliderItem = new SliderItem { Id = sliderId, Label = (label ?? sliderId), Unit = (unit ?? ""), Min = min, Max = max, Step = ((step > 0.0) ? step : 0.0), Get = get, Set = set }; orCreate.Sliders.RemoveAll((SliderItem s) => s.Id == sliderId); orCreate.Sliders.Add(sliderItem); _sliders[sliderId] = sliderItem; } } internal static void BindPanelLog(string panelId) { GetOrCreate(panelId, null).HasLog = true; } internal static bool Invoke(string actionId) { if (string.IsNullOrEmpty(actionId) || !_actions.TryGetValue(actionId, out var value)) { return false; } try { value.Run?.Invoke(); } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Warning("[snitch] action '" + actionId + "' threw: " + ex.Message); } } return true; } internal static bool SetToggle(string toggleId, bool value) { if (string.IsNullOrEmpty(toggleId) || !_toggles.TryGetValue(toggleId, out var value2)) { return false; } try { value2.Set?.Invoke(value); } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Warning("[snitch] toggle '" + toggleId + "' threw: " + ex.Message); } } return true; } internal static bool GetToggle(string toggleId) { if (!string.IsNullOrEmpty(toggleId) && _toggles.TryGetValue(toggleId, out var value)) { try { return value.Get != null && value.Get(); } catch { } } return false; } internal static SliderItem GetSlider(string sliderId) { if (string.IsNullOrEmpty(sliderId)) { return null; } _sliders.TryGetValue(sliderId, out var value); return value; } internal static bool SetSlider(string sliderId, double value) { SliderItem slider = GetSlider(sliderId); if (slider == null) { return false; } try { slider.Set?.Invoke(slider.Quantize(value)); } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Warning("[snitch] slider '" + sliderId + "' threw: " + ex.Message); } } return true; } } } namespace Snitch.Logging { internal struct LogEntry { public long Seq; public string Ch; public int Lvl; public string Msg; public string Time; } internal static class LogHub { private const int PerChannelMax = 400; private const int TimelineMax = 1200; private static readonly object _lock = new object(); private static readonly Dictionary> _channels = new Dictionary>(); private static readonly Queue _timeline = new Queue(1200); private static long _seq; internal static void Install() { } internal static void Uninstall() { } internal static void Write(string channel, int lvl, string msg) { if (string.IsNullOrEmpty(msg)) { return; } if (string.IsNullOrEmpty(channel)) { channel = "misc"; } if (lvl < 0) { lvl = 0; } else if (lvl > 2) { lvl = 2; } lock (_lock) { LogEntry item = new LogEntry { Seq = ++_seq, Ch = channel, Lvl = lvl, Msg = msg, Time = DateTime.Now.ToString("HH:mm:ss") }; if (!_channels.TryGetValue(channel, out var value)) { value = new Queue(64); _channels[channel] = value; } value.Enqueue(item); while (value.Count > 400) { value.Dequeue(); } _timeline.Enqueue(item); while (_timeline.Count > 1200) { _timeline.Dequeue(); } } } internal static List Timeline(int n) { lock (_lock) { return TakeLast(_timeline, n); } } internal static List Channel(string channel, int n) { lock (_lock) { Queue value; return _channels.TryGetValue(channel, out value) ? TakeLast(value, n) : new List(); } } internal static List Channels() { lock (_lock) { return new List(_channels.Keys); } } internal static void Clear() { lock (_lock) { _channels.Clear(); _timeline.Clear(); } } private static List TakeLast(Queue q, int n) { if (n <= 0 || n >= q.Count) { return new List(q); } List list = new List(q); return list.GetRange(list.Count - n, n); } } } 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("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 LatestSections = new List(); internal static List LatestStates = new List(); internal static List LatestCounters = new List(); 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 _enabled; private static MelonPreferences_Entry _enableInMp; private static MelonPreferences_Entry _autoStart; private static MelonPreferences_Entry _autoInstrument; private static MelonPreferences_Entry _pollHz; private static MelonPreferences_Entry _serverEnabled; private static MelonPreferences_Entry _serverPort; private static MelonPreferences_Entry _serverToken; private static MelonPreferences_Entry _allowedOrigins; private static MelonPreferences_Entry _lanAccess; private static MelonPreferences_Entry _lanPort; 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 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 bool LanAccess { get { return _lanAccess?.Value ?? false; } set { if (_lanAccess != null) { _lanAccess.Value = value; } } } internal static int LanPort => Mathf.Clamp(_lanPort?.Value ?? 6141, 1024, 65535); 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 '.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."); _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(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(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."); _lanAccess = Create("LanAccess", def: false, "Phone remote (LAN access)", "OFF (default): the data server stays loopback-only (127.0.0.1), nothing is exposed to your network. ON: also run a small LAN endpoint so a phone on the same Wi-Fi can open the dashboard and use it as a remote (scan the QR shown in the dashboard). It is token-gated; you may need to allow the port through your firewall on the Private network. Toggle live in-game with 'snitch lan on|off'."); _lanPort = Create("LanPort", 6141, "Phone remote port", "TCP port for the LAN phone endpoint (separate from the loopback port). Change only if it clashes. Clamped 1024-65535.", (ValueValidator)(object)new ValueRange(1024, 65535)); } } private static MelonPreferences_Entry Create(string id, T def, string name, string desc = null, ValueValidator validator = null) { if (validator != null) { return _category.CreateEntry(id, def, name, desc, false, false, validator); } return _category.CreateEntry(id, def, name, desc, false, false, (ValueValidator)null, (string)null); } } } namespace Snitch.Compat { internal static class Net { internal static bool IsMultiplayer() { try { Lobby instance = Singleton.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.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 _recentMarks = new List(32); private static readonly HashSet _hotlineMetrics = new HashSet(); 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 read, string unit) { CounterRegistry.RegisterDelegate(id, read, unit); }; SnitchBridge.UnregisterCounter = delegate(string id) { CounterRegistry.Unregister(id); }; SnitchBridge.RegisterStateProvider = delegate(string id, Func 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); }; SnitchBridge.RegisterPanel = delegate(string id, string title) { PanelRegistry.RegisterPanel(id, title); Hud.RegisterPanel(id, title); EnsureHotlineMetrics(id); }; SnitchBridge.RegisterAction = delegate(string panelId, string actionId, string label, Action run) { PanelRegistry.RegisterAction(panelId, actionId, label, run); Hud.RegisterAction(panelId, label, run); }; SnitchBridge.RegisterToggle = delegate(string panelId, string toggleId, string label, Func get, Action set) { PanelRegistry.RegisterToggle(panelId, toggleId, label, get, set); Hud.RegisterToggle(panelId, label, get, set); }; SnitchBridge.RegisterSlider = delegate(string panelId, string sliderId, string label, double min, double max, double step, string unit, Func get, Action set) { PanelRegistry.RegisterSlider(panelId, sliderId, label, min, max, step, unit, get, set); Hud.RegisterSlider(panelId, label, min, max, get, set, step, unit); }; SnitchBridge.RegisterText = delegate(string panelId, Func provider) { PanelRegistry.RegisterText(panelId, provider); Hud.RegisterText(panelId, provider); }; SnitchBridge.BindPanelLog = delegate(string panelId) { PanelRegistry.BindPanelLog(panelId); Hud.BindPanelLog(panelId); }; SnitchBridge.Log = delegate(string channel, int level, string message) { LogHub.Write(channel, level, message); Hud.Log(channel, message, (LogLevel)level); }; } private static void EnsureHotlineMetrics(string panelId) { if (!string.IsNullOrEmpty(panelId) && _hotlineMetrics.Add(panelId)) { Hud.RegisterText(panelId, () => ProfilerHud.BuildPanelMetrics(panelId)); } } } public static class SnitchBridge { public const int AbiVersion = 1; public static Func IsEnabled; public static Func BeginScope; public static Action EndScope; public static Action BeginLabel; public static Action EndLabel; public static Action, string> RegisterCounter; public static Action UnregisterCounter; public static Action> RegisterStateProvider; public static Action UnregisterStateProvider; public static Action Mark; public static Action RegisterAblationLever; public static Action RegisterPanel; public static Action RegisterAction; public static Action, Action> RegisterToggle; public static Action> RegisterText; public static Action BindPanelLog; public static Action Log; public static Action, Action> RegisterSlider; } } namespace Snitch.Ablation { internal sealed class AblationLever { public string Name; public Func CanApply; public Action Apply; public Action Restore; } internal static class LeverRegistry { private static readonly Dictionary _levers = new Dictionary(StringComparer.OrdinalIgnoreCase); private static bool _builtins; internal static IEnumerable 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 act) { try { List 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); } } }