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