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.7.0
Snitch.dll
Decompiled 3 weeks 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.Reflection.Emit; 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.7.0", "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.7.0.0")] [assembly: AssemblyInformationalVersion("1.7.0+dff04885b86c4ccbb98c65bf2f59955047d954a6")] [assembly: AssemblyProduct("Snitch")] [assembly: AssemblyTitle("Snitch")] [assembly: NeutralResourcesLanguage("en-US")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.7.0.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.7.0"; } } 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, unattributed, patches, states, report", "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 "unattr": case "unattributed": Unattributed(); break; case "patches": PatchesCmd(p); 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 | unattributed | patches [on|off|list|status] | 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"); AttributionStats latestAttribution = SnitchCore.LatestAttribution; if (latestAttribution.Samples > 0) { Log($"unattributed={latestAttribution.UnattributedMeanMs:F2}ms/f ({latestAttribution.UnattributedPct:F0}% of the frame) badFrames={latestAttribution.SpikeFrames} patchTiming={(PatchInstrument.Enabled ? "on" : "off")} " + "('snitch unattributed' explains)"); } 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}"); AttributionStats latestAttribution = SnitchCore.LatestAttribution; if (latestAttribution.Samples != 0) { Log($"attribution: sections={latestAttribution.AttributedMeanMs:F2}ms/f unattributed={latestAttribution.UnattributedMeanMs:F2}ms/f ({latestAttribution.UnattributedPct:F0}% of the frame, worst {latestAttribution.MaxUnattributedMs:F2})" + ((latestAttribution.SpikeFrames > 0) ? $" | {latestAttribution.SpikeFrames} bad frame(s), {latestAttribution.SpikeUnexplainedPct:F0}% of their excess unexplained" : " | no bad frames in the window")); } } 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]; if (sectionRow.Label == "(unattributed)") { Log($" {sectionRow.Label,-28} {sectionRow.MsPerFrame,7:F3} ms/f {sectionRow.PctFrame,5:F1}% inside no section (max {sectionRow.MaxMs:F3})" + " <- 'snitch unattributed'"); } else { 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; SavePreferences(); 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; SavePreferences(); 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 Unattributed() { AttributionStats latestAttribution = SnitchCore.LatestAttribution; if (latestAttribution.Samples == 0) { Log("unattributed: nothing measured yet" + (SnitchCore.Active ? " (give it a second)." : " - run 'snitch start' first.")); return; } Log($"unattributed: {latestAttribution.UnattributedMeanMs:F2} ms/frame of a {latestAttribution.FrameMeanMs:F2} ms frame ({latestAttribution.UnattributedPct:F0}%) - inside no section at all."); Log($" sections account for {latestAttribution.AttributedMeanMs:F2} ms/frame; the worst single frame left {latestAttribution.MaxUnattributedMs:F2} ms unexplained."); if (latestAttribution.SpikeFrames == 0) { Log($" bad frames: none in the last {latestAttribution.Samples} (nothing above {latestAttribution.SpikeFactor:F1}x the {latestAttribution.FrameMedianMs:F2} ms median)."); } else { Log($" bad frames: {latestAttribution.SpikeFrames} of {latestAttribution.Samples} above {latestAttribution.SpikeFactor:F1}x the {latestAttribution.FrameMedianMs:F2} ms median; on those, {latestAttribution.SpikeMeanUnexplainedMs:F2} ms of the {latestAttribution.SpikeMeanExcessMs:F2} ms excess ({latestAttribution.SpikeUnexplainedPct:F0}%) is unexplained (worst {latestAttribution.WorstUnexplainedMs:F2} ms)."); } Log(" most of an unattributed frame is the game itself and always will be; the number that points at a mod is the unexplained share of the bad frames."); Log(" " + NextStep(latestAttribution)); } private static string NextStep(AttributionStats a) { if (!a.PointsAtHiddenWork) { return "the bad frames are explained by sections that got worse, so 'snitch top' has the answer."; } if (!PatchInstrument.Enabled) { return "-> run 'snitch patches on'. A mod's per-frame work does not have to live in OnUpdate: a Harmony postfix on a vanilla method the engine calls every frame belongs to no section, so the mod reads as cheap."; } return $"-> patch timing is already on ({PatchInstrument.WrappedCount} patches wrapped) and still nothing " + "accounts for it. What is left: a coroutine, an event handler, a Unity message on a MonoBehaviour a mod added, or the game itself - 'snitch vanilla on' and 'snitch ablate' cover the last two."; } private static void PatchesCmd(string[] p) { switch ((p.Length > 2) ? p[2].ToLowerInvariant() : "status") { case "on": PatchInstrument.Enable(); Log(PatchInstrument.Status()); Log(" numbers include the wrapper's own overhead, the same as the vanilla probes. Run 'snitch patches on' again after a late-loading mod has patched."); ReportSkipsAndFailures(); return; case "off": PatchInstrument.Disable(); Log("patch timing OFF (the wrappers stay installed but dormant)."); return; case "list": PatchesList(); return; } Log("patch timing: " + PatchInstrument.Status()); if (!PatchInstrument.EverScanned) { Log(" nothing scanned yet - 'snitch patches on' wraps every loaded mod's Harmony prefixes, postfixes and finalizers."); } ReportSkipsAndFailures(); } private static void ReportSkipsAndFailures() { StringBuilder stringBuilder = new StringBuilder(); foreach (KeyValuePair<string, int> skippedOwner in PatchInstrument.SkippedOwners) { if (stringBuilder.Length > 0) { stringBuilder.Append(", "); } stringBuilder.Append(skippedOwner.Key).Append(' ').Append(skippedOwner.Value); } if (stringBuilder.Length > 0) { Log(" not a mod's patch, left alone: " + stringBuilder); } int failureCount = PatchInstrument.FailureCount; if (failureCount == 0) { return; } Log($" {failureCount} wrap(s) FAILED - those methods may now be unusable for later patchers:"); foreach (string failure in PatchInstrument.Failures) { Log(" " + failure); } } private static void PatchesList() { List<WrappedPatch> list = PatchInstrument.All(); if (list.Count == 0) { Log("no patches wrapped. 'snitch patches on' wraps every loaded mod's Harmony prefixes, postfixes and finalizers."); return; } Dictionary<string, SectionRow> costByLabel = new Dictionary<string, SectionRow>(StringComparer.Ordinal); List<SectionRow> latestSections = SnitchCore.LatestSections; if (latestSections != null) { for (int i = 0; i < latestSections.Count; i++) { costByLabel[latestSections[i].Label] = latestSections[i]; } } list.Sort((WrappedPatch x, WrappedPatch y) => Cost(costByLabel, y).CompareTo(Cost(costByLabel, x))); Log($"{list.Count} wrapped patch(es), worst first:"); for (int num = 0; num < list.Count; num++) { WrappedPatch wrappedPatch = list[num]; costByLabel.TryGetValue(wrappedPatch.Label, out var value); string text = string.Join(", ", wrappedPatch.Targets); if (wrappedPatch.TargetCount > wrappedPatch.Targets.Count) { text += $", +{wrappedPatch.TargetCount - wrappedPatch.Targets.Count} more"; } Log($" {wrappedPatch.Label,-38} {value.MsPerFrame,7:F3} ms/f {value.Calls,7:F0} calls/f (max {value.MaxMs:F3}) [{string.Join("+", wrappedPatch.Kinds)}] on {text}"); } if (!PatchInstrument.Enabled) { Log(" timing is OFF, so those numbers are stale - 'snitch patches on' arms them."); } Log(" a zero is not proof of free: 0.000 ms with calls is below the printed resolution, and 0 calls on a patch you know is hot means it was not measured (the JIT can inline a small patch method past the wrapper)."); } private static double Cost(Dictionary<string, SectionRow> byLabel, WrappedPatch w) { if (!byLabel.TryGetValue(w.Label, out var value)) { return 0.0; } return value.MsPerFrame; } 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; } } private static void SavePreferences() { try { MelonPreferences.Save(); } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Warning("[snitch] could not write MelonPreferences (the setting is live for this session only): " + ex.Message); } } } 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 (Exception ex) { Instance log = Core.Log; if (log != null) { log.Error("[snitch] console prefix (string) threw, handing the command to the game: " + ex); } 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 (Exception ex) { Instance log = Core.Log; if (log != null) { log.Error("[snitch] console prefix (list) threw, handing the command to the game: " + ex); } 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; Instance log = Core.Log; if (log != null) { log.Warning("[snitch] probe discovery: " + asm.GetName().Name + " loaded only part of its types - " + ex.Message); } } catch (Exception ex2) { Instance log2 = Core.Log; if (log2 != null) { log2.Warning("[snitch] probe discovery: " + asm.GetName().Name + " would not enumerate its types - " + ex2.Message); } return null; } if (types == null) { Instance log3 = Core.Log; if (log3 != null) { log3.Warning("[snitch] probe discovery: " + asm.GetName().Name + " returned no type list; skipping it."); } 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 (Exception ex) { Instance log = Core.Log; if (log != null) { log.Warning("[snitch] MelonMod.RegisteredMelons is unreadable, falling back to reflection - " + ex.Message); } } 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; } Instance log2 = Core.Log; if (log2 != null) { log2.Msg("[snitch] " + type.Name + ".RegisteredMelons is not a mod list here; trying the next candidate."); } } catch (Exception ex2) { Instance log3 = Core.Log; if (log3 != null) { log3.Warning("[snitch] reflecting " + type.Name + ".RegisteredMelons failed - " + ex2.Message); } } } Instance log4 = Core.Log; if (log4 != null) { log4.Error("[snitch] no route to the loaded mod list; auto-instrumentation and probe discovery are off."); } return null; } private static string ModName(MelonMod mod) { string text = null; try { MelonInfoAttribute info = ((MelonBase)mod).Info; text = ((info != null) ? info.Name : null); } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Warning("[snitch] auto-instrument: a mod would not give its name - " + ex.Message); } } if (string.IsNullOrEmpty(text)) { text = ((object)mod).GetType().Namespace ?? ((object)mod).GetType().Name; } return GroupName(text); } internal static string GroupName(string rawName) { if (string.IsNullOrEmpty(rawName)) { return "(unnamed)"; } StringBuilder stringBuilder = new StringBuilder(rawName.Length); for (int i = 0; i < rawName.Length; i++) { char c = rawName[i]; if (c == '\u001b') { for (; i < rawName.Length && rawName[i] != 'm'; i++) { } } else if (c >= ' ') { stringBuilder.Append((c == '.' || c == ' ') ? '_' : c); } } string text = stringBuilder.ToString(); if (text.Length <= 0) { return "(unnamed)"; } return text; } } internal sealed class WrappedPatch { internal MethodInfo Method; internal string Label; internal int SectionId; internal string Owner; internal readonly List<string> Kinds = new List<string>(2); internal readonly List<string> Targets = new List<string>(1); internal int TargetCount; } internal static class PatchInstrument { internal static volatile bool Enabled; private static volatile Dictionary<MethodBase, int> _ids = new Dictionary<MethodBase, int>(); private static readonly Dictionary<MethodBase, WrappedPatch> _wrapped = new Dictionary<MethodBase, WrappedPatch>(); private static readonly SortedDictionary<string, int> _skips = new SortedDictionary<string, int>(StringComparer.Ordinal); private static readonly SortedDictionary<string, int> _skippedOwners = new SortedDictionary<string, int>(StringComparer.Ordinal); private static readonly List<string> _failures = new List<string>(); private static int _scans; private const int MaxListedTargets = 8; private static readonly Assembly SelfAssembly = typeof(PatchInstrument).Assembly; internal static bool Requested { get; private set; } internal static int WrappedCount => _wrapped.Count; internal static int FailureCount => _failures.Count; internal static bool EverScanned => _scans > 0; internal static IEnumerable<string> Failures => _failures; internal static IEnumerable<KeyValuePair<string, int>> SkippedOwners => _skippedOwners; internal static void Enable() { int value = Scan(); Enabled = true; Requested = true; Instance log = Core.Log; if (log != null) { log.Msg($"[snitch] patch timing ON. {value} newly wrapped, {_wrapped.Count} wrapped in total. " + Status()); } } internal static void Disable() { Enabled = false; Requested = false; Instance log = Core.Log; if (log != null) { log.Msg("[snitch] patch timing OFF (the wrappers stay installed but dormant - one bool read)."); } } internal static void Suspend() { Enabled = false; } internal static string Status() { StringBuilder stringBuilder = new StringBuilder(160); stringBuilder.Append("enabled=").Append(Enabled).Append(" wrapped=") .Append(_wrapped.Count) .Append(" scans=") .Append(_scans) .Append(" failed=") .Append(_failures.Count); if (_skips.Count > 0) { stringBuilder.Append(" skipped=["); bool flag = true; foreach (KeyValuePair<string, int> skip in _skips) { if (!flag) { stringBuilder.Append(", "); } flag = false; stringBuilder.Append(skip.Key).Append(' ').Append(skip.Value); } stringBuilder.Append(']'); } return stringBuilder.ToString(); } internal static List<WrappedPatch> All() { List<WrappedPatch> list = new List<WrappedPatch>(_wrapped.Values); list.Sort((WrappedPatch x, WrappedPatch y) => string.CompareOrdinal(x.Label, y.Label)); return list; } internal static int Scan() { _scans++; _skips.Clear(); _skippedOwners.Clear(); List<MethodBase> list; try { list = new List<MethodBase>(Harmony.GetAllPatchedMethods()); } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Error("[snitch] patch scan aborted (guard: Harmony.GetAllPatchedMethods threw): " + ex); } return 0; } MethodInfo methodInfo = AccessTools.Method(typeof(PatchInstrument), "Pre", (Type[])null, (Type[])null); MethodInfo methodInfo2 = AccessTools.Method(typeof(PatchInstrument), "Fin", (Type[])null, (Type[])null); if (methodInfo == null || methodInfo2 == null) { Instance log2 = Core.Log; if (log2 != null) { log2.Error("[snitch] patch scan aborted (guard: Snitch's own Pre/Fin did not resolve through AccessTools)."); } return 0; } Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.Ordinal); Dictionary<string, string> dictionary2 = new Dictionary<string, string>(StringComparer.Ordinal); BuildOwnerIndex(dictionary, dictionary2); if (dictionary.Count == 0 && dictionary2.Count == 0) { Instance log3 = Core.Log; if (log3 != null) { log3.Error("[snitch] patch scan aborted (guard: no loaded Melon could be indexed, so no patch could be attributed to a mod)."); } return 0; } Dictionary<MethodBase, WrappedPatch> pending = new Dictionary<MethodBase, WrappedPatch>(); for (int i = 0; i < list.Count; i++) { MethodBase methodBase = list[i]; if (methodBase == null) { Skip("target-null", 1); continue; } Patches patchInfo; try { patchInfo = Harmony.GetPatchInfo(methodBase); } catch (Exception ex2) { Skip("patch-info-threw", 1); Instance log4 = Core.Log; if (log4 != null) { log4.Warning("[snitch] patch scan: GetPatchInfo(" + Describe(methodBase) + ") threw - " + ex2.Message); } continue; } if (patchInfo == null) { Skip("no-patch-info", 1); continue; } Collect(patchInfo.Prefixes, "prefix", methodBase, dictionary, dictionary2, pending); Collect(patchInfo.Postfixes, "postfix", methodBase, dictionary, dictionary2, pending); Collect(patchInfo.Finalizers, "finalizer", methodBase, dictionary, dictionary2, pending); if (patchInfo.Transpilers.Count > 0) { Skip("transpiler(runs once, not per call)", patchInfo.Transpilers.Count); } if (patchInfo.ILManipulators.Count > 0) { Skip("ilmanipulator(runs once, not per call)", patchInfo.ILManipulators.Count); } } return Install(pending, methodInfo, methodInfo2); } private static int Install(Dictionary<MethodBase, WrappedPatch> pending, MethodInfo pre, MethodInfo fin) { //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Expected O, but got Unknown //IL_00a1: Expected O, but got Unknown if (pending.Count == 0) { return 0; } Dictionary<MethodBase, int> dictionary = new Dictionary<MethodBase, int>(_ids); foreach (WrappedPatch value in pending.Values) { dictionary[value.Method] = value.SectionId; } _ids = dictionary; int num = 0; List<WrappedPatch> list = null; foreach (WrappedPatch value2 in pending.Values) { try { Core.HarmonyInst.Patch((MethodBase)value2.Method, new HarmonyMethod(pre), (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(fin), (HarmonyMethod)null); _wrapped[value2.Method] = value2; num++; } catch (Exception ex) { string text = value2.Label + " (" + Describe(value2.Method) + "): " + ex.Message; _failures.Add(text); if (list == null) { list = new List<WrappedPatch>(); } list.Add(value2); Instance log = Core.Log; if (log != null) { log.Error("[snitch] could not wrap patch " + text); } } } if (list != null) { Dictionary<MethodBase, int> dictionary2 = new Dictionary<MethodBase, int>(_ids); for (int i = 0; i < list.Count; i++) { dictionary2.Remove(list[i].Method); } _ids = dictionary2; } return num; } private static void Collect(IList<Patch> patches, string kind, MethodBase target, Dictionary<string, string> byHarmonyId, Dictionary<string, string> byAssembly, Dictionary<MethodBase, WrappedPatch> pending) { if (patches == null) { Skip("patch-list-null", 1); return; } for (int i = 0; i < patches.Count; i++) { Patch val = patches[i]; if (val == null) { Skip("patch-entry-null", 1); continue; } MethodInfo patchMethod; try { patchMethod = val.PatchMethod; } catch (Exception ex) { Skip("patch-method-unresolvable", 1); Instance log = Core.Log; if (log != null) { log.Warning($"[snitch] patch scan: a {kind} on {Describe(target)} owned by '{val.owner}' " + "has no resolvable method - " + ex.Message); } continue; } if (patchMethod == null) { Skip("patch-method-null", 1); continue; } if (pending.TryGetValue(patchMethod, out var value)) { Note(value, kind, target); continue; } if (_wrapped.TryGetValue(patchMethod, out var value2)) { Note(value2, kind, target); continue; } if (patchMethod is DynamicMethod) { Skip("dynamic-method", 1); continue; } if (patchMethod.DeclaringType == null) { Skip("no-declaring-type", 1); continue; } if ((object)patchMethod.DeclaringType.Assembly == SelfAssembly) { Skip("snitch-own", 1); continue; } if (patchMethod.IsAbstract) { Skip("abstract", 1); continue; } if (patchMethod.IsGenericMethodDefinition || patchMethod.ContainsGenericParameters) { Skip("open-generic", 1); continue; } try { if (patchMethod.GetMethodBody() == null) { Skip("no-il-body", 1); continue; } } catch (Exception ex2) { Skip("method-body-threw", 1); Instance log2 = Core.Log; if (log2 != null) { log2.Warning("[snitch] patch scan: " + Describe(patchMethod) + " would not hand over its IL body - " + ex2.Message); } continue; } string text = ResolveOwner(patchMethod, val.owner, byHarmonyId, byAssembly); if (text == null) { Skip("not-a-mod-patch", 1); NoteSkippedOwner(val.owner, patchMethod); continue; } string label = text + "." + Clean(patchMethod.DeclaringType.Name) + "." + Clean(patchMethod.Name); WrappedPatch wrappedPatch = new WrappedPatch { Method = patchMethod, Label = label, SectionId = SectionProfiler.GetId(label), Owner = text }; Note(wrappedPatch, kind, target); pending[patchMethod] = wrappedPatch; } } private static void Note(WrappedPatch w, string kind, MethodBase target) { if (!w.Kinds.Contains(kind)) { w.Kinds.Add(kind); } w.TargetCount++; if (w.Targets.Count < 8) { string item = Describe(target); if (!w.Targets.Contains(item)) { w.Targets.Add(item); } } } private static void Skip(string reason, int count) { _skips.TryGetValue(reason, out var value); _skips[reason] = value + count; } private static void NoteSkippedOwner(string harmonyOwnerId, MethodInfo pm) { string text = ShortOwnerId(harmonyOwnerId); if (string.IsNullOrEmpty(text)) { try { text = pm.DeclaringType.Assembly.GetName().Name; } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Warning("[snitch] patch scan: could not name the owner of " + Describe(pm) + " - " + ex.Message); } text = "(unnamed)"; } } _skippedOwners.TryGetValue(text, out var value); _skippedOwners[text] = value + 1; } private static string ShortOwnerId(string harmonyOwnerId) { if (string.IsNullOrEmpty(harmonyOwnerId)) { return null; } int num = harmonyOwnerId.LastIndexOf(':'); if (num < 0 || num >= harmonyOwnerId.Length - 1) { return harmonyOwnerId; } return harmonyOwnerId.Substring(num + 1); } private static string ResolveOwner(MethodInfo pm, string harmonyOwnerId, Dictionary<string, string> byHarmonyId, Dictionary<string, string> byAssembly) { if (!string.IsNullOrEmpty(harmonyOwnerId) && byHarmonyId.TryGetValue(harmonyOwnerId, out var value)) { return value; } string fullName; try { fullName = pm.DeclaringType.Assembly.FullName; } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Warning("[snitch] patch scan: could not read the assembly of " + Describe(pm) + " - " + ex.Message); } return null; } if (!string.IsNullOrEmpty(fullName) && byAssembly.TryGetValue(fullName, out var value2)) { return value2; } return null; } private static void BuildOwnerIndex(Dictionary<string, string> byHarmonyId, Dictionary<string, string> byAssembly) { IEnumerable<MelonBase> registeredMelons; try { registeredMelons = MelonBase.RegisteredMelons; } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Error("[snitch] patch scan: MelonBase.RegisteredMelons is unreadable, no patch can be attributed - " + ex); } return; } if (registeredMelons == null) { Instance log2 = Core.Log; if (log2 != null) { log2.Error("[snitch] patch scan: MelonBase.RegisteredMelons returned null, no patch can be attributed."); } return; } foreach (MelonBase item in registeredMelons) { if (item == null || (object)item == Core.Instance) { continue; } string text = MelonName(item); try { if (item.HarmonyInstance != null) { byHarmonyId[item.HarmonyInstance.Id] = text; } } catch (Exception ex2) { Instance log3 = Core.Log; if (log3 != null) { log3.Warning("[snitch] patch scan: " + text + " has no readable Harmony instance - " + ex2.Message); } } try { MelonAssembly melonAssembly = item.MelonAssembly; Assembly assembly = ((melonAssembly != null) ? melonAssembly.Assembly : null) ?? ((object)item).GetType().Assembly; if ((object)assembly != null && assembly.FullName != null) { byAssembly[assembly.FullName] = text; } } catch (Exception ex3) { Instance log4 = Core.Log; if (log4 != null) { log4.Warning("[snitch] patch scan: " + text + " has no readable assembly - " + ex3.Message); } } } } private static string MelonName(MelonBase melon) { string text = null; try { MelonInfoAttribute info = melon.Info; text = ((info != null) ? info.Name : null); } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Warning("[snitch] patch scan: a Melon would not give its name - " + ex.Message); } } if (string.IsNullOrEmpty(text)) { text = ((object)melon).GetType().Namespace ?? ((object)melon).GetType().Name; } return AutoInstrument.GroupName(text); } private static string Describe(MethodBase m) { if (m == null) { return "(null)"; } return ((m.DeclaringType != null) ? m.DeclaringType.Name : "(global)") + "." + m.Name; } private static string Clean(string s) { if (string.IsNullOrEmpty(s)) { return "(unnamed)"; } return s.Replace('`', '_').Replace('<', '_').Replace('>', '_') .Replace(' ', '_'); } 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); } } } 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); } AttributionStats latestAttribution = SnitchCore.LatestAttribution; if (latestAttribution.Samples > 0) { string value2 = (latestAttribution.PointsAtHiddenWork ? "#f55" : "#999"); stringBuilder.Append("\n<color=").Append(value2).Append(">unattributed</color> ") .Append(latestAttribution.UnattributedMeanMs.ToString("F2")) .Append(" ms ") .Append(latestAttribution.UnattributedPct.ToString("F0")) .Append('%'); if (latestAttribution.PointsAtHiddenWork) { stringBuilder.Append(" <color=#f55>").Append(latestAttribution.SpikeFrames).Append(" bad frame(s) unexplained</color>"); } } 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) {