using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using ExitGames.Client.Photon; using HarmonyLib; using Microsoft.CodeAnalysis; using Photon.Pun; using Photon.Realtime; using UnityEngine; using UnityEngine.Profiling; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("WhySoLaggy")] [assembly: AssemblyDescription("BepInEx performance monitor: FPS tracking, plugin Update profiling, Harmony patch profiling.")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("WhySoLaggy")] [assembly: AssemblyCopyright("")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("7a0d965a-ed09-40ae-9680-c8917710a150")] [assembly: AssemblyFileVersion("1.0.3.0")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyVersion("1.0.3.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.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 WhySoLaggy { internal static class AbuseLogger { private static StreamWriter _writer; private static readonly object _lock = new object(); public static void Initialize(string bepInExDir) { try { string text = Path.Combine(bepInExDir, "WhySoLaggy_Abuse.log"); _writer = new StreamWriter(text, append: false, Encoding.UTF8) { AutoFlush = true }; _writer.WriteLine("[" + Timestamp() + "] WhySoLaggy Abuse Detection Monitor started"); _writer.WriteLine("[" + Timestamp() + "] Log file: " + text); _writer.WriteLine(new string('=', 60)); } catch (Exception ex) { Logger.CreateLogSource("WhySoLaggy").LogError((object)("Failed to create abuse log file: " + ex.Message)); } } public static void Info(string message) { if (_writer == null) { return; } lock (_lock) { try { _writer.WriteLine("[" + Timestamp() + "] " + message); } catch { } } } public static void Write(string message) { if (_writer == null || !LogFilter.AllowAbuse()) { return; } lock (_lock) { try { _writer.WriteLine("[" + Timestamp() + "] " + message); } catch { } } } public static void Alert(string message) { string text = "⚠ ABUSE ALERT: " + message; if (_writer != null) { lock (_lock) { try { _writer.WriteLine("[" + Timestamp() + "] " + text); } catch { } } } ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogWarning((object)("[WHY_LAG] " + text)); } try { PerformanceDashboard.ReportAlert(message); } catch { } } public static void AlertDetail(string message) { if (_writer == null) { return; } lock (_lock) { try { _writer.WriteLine("[" + Timestamp() + "] " + message); } catch { } } } public static void AlertDetailRaw(string line) { if (_writer == null) { return; } lock (_lock) { try { _writer.WriteLine(line); } catch { } } } public static void WriteRaw(string line) { if (_writer == null || !LogFilter.AllowAbuse()) { return; } lock (_lock) { try { _writer.WriteLine(line); } catch { } } } public static void Shutdown() { if (_writer == null) { return; } lock (_lock) { try { _writer.WriteLine("[" + Timestamp() + "] WhySoLaggy Abuse Detection shutting down"); _writer.Flush(); _writer.Close(); _writer = null; } catch { } } } private static string Timestamp() { return DateTime.Now.ToString("HH:mm:ss.fff"); } } internal static class AbuseNotificationUI { private struct Notification { public string Message; public float ExpireTime; } private const float DisplayDuration = 12f; private const int MaxVisibleMessages = 8; private const float BoxWidth = 520f; private const float LineHeight = 22f; private const float Padding = 8f; private const float TopMargin = 10f; private const float LeftMargin = 10f; private static readonly List _notifications = new List(); private static GUIStyle _boxStyle; private static GUIStyle _textStyle; public static void Show(string message) { _notifications.Add(new Notification { Message = message, ExpireTime = Time.unscaledTime + 12f }); while (_notifications.Count > 8) { _notifications.RemoveAt(0); } } public static void DrawGUI() { //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Unknown result type (might be due to invalid IL or missing references) //IL_00ce: Unknown result type (might be due to invalid IL or missing references) //IL_00e3: Unknown result type (might be due to invalid IL or missing references) //IL_00fa: Unknown result type (might be due to invalid IL or missing references) float now = Time.unscaledTime; _notifications.RemoveAll((Notification n) => now >= n.ExpireTime); if (_notifications.Count == 0) { return; } EnsureStyles(); float num = (float)_notifications.Count * 22f + 16f; GUI.Box(new Rect(10f, 10f, 520f, num), GUIContent.none, _boxStyle); float num2 = 18f; foreach (Notification notification in _notifications) { float num3 = notification.ExpireTime - now; float a = ((num3 < 3f) ? (num3 / 3f) : 1f); Color textColor = _textStyle.normal.textColor; textColor.a = a; _textStyle.normal.textColor = textColor; GUI.Label(new Rect(18f, num2, 504f, 22f), notification.Message, _textStyle); num2 += 22f; } } private static void EnsureStyles() { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Expected O, but got Unknown //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Expected O, but got Unknown //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Expected O, but got Unknown //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Expected O, but got Unknown //IL_00ac: Unknown result type (might be due to invalid IL or missing references) if (_boxStyle == null) { Texture2D val = new Texture2D(1, 1); val.SetPixel(0, 0, new Color(0f, 0f, 0f, 0.75f)); val.Apply(); _boxStyle = new GUIStyle(GUI.skin.box); _boxStyle.normal.background = val; _boxStyle.border = new RectOffset(0, 0, 0, 0); _textStyle = new GUIStyle(GUI.skin.label); _textStyle.fontSize = 14; _textStyle.normal.textColor = new Color(1f, 0.35f, 0.3f, 1f); _textStyle.fontStyle = (FontStyle)1; _textStyle.wordWrap = true; } } } internal static class ExpressionEvaluator { public enum RootKind { Instance, Arg, Args, Result, Exception, StaticType } public struct Step { public string Name; public bool NullSafe; public bool IsIndex; public int Index; } public sealed class CompiledExpr { public string Source; public RootKind Root; public int ArgIndex; public Type StaticRootType; public List Steps; public string CompileError; } private sealed class MemberReader { public FieldInfo Field; public PropertyInfo Property; public bool IsNone; public object Read(object instance) { if (Field != null) { return Field.GetValue(instance); } if (Property != null) { return Property.GetValue(instance, null); } return null; } } private static readonly Dictionary _typeCache = new Dictionary(StringComparer.Ordinal); private static readonly Dictionary _memberCache = new Dictionary(StringComparer.Ordinal); private static readonly object _cacheLock = new object(); public static CompiledExpr Compile(string expr) { CompiledExpr compiledExpr = new CompiledExpr { Source = expr, Steps = new List() }; if (string.IsNullOrEmpty(expr)) { compiledExpr.CompileError = "empty"; return compiledExpr; } int pos = 0; string text = ReadIdent(expr, ref pos); if (string.IsNullOrEmpty(text)) { compiledExpr.CompileError = "no root token"; return compiledExpr; } switch (text) { case "__instance": compiledExpr.Root = RootKind.Instance; break; case "__args": compiledExpr.Root = RootKind.Args; break; case "__result": compiledExpr.Root = RootKind.Result; break; case "__exception": compiledExpr.Root = RootKind.Exception; break; default: { if (text.StartsWith("__arg", StringComparison.Ordinal) && int.TryParse(text.Substring(5), out var result) && result >= 0) { compiledExpr.Root = RootKind.Arg; compiledExpr.ArgIndex = result; break; } Type type = ResolveType(text); if (type == null) { compiledExpr.CompileError = "unknown root/type: " + text; return compiledExpr; } compiledExpr.Root = RootKind.StaticType; compiledExpr.StaticRootType = type; break; } } while (pos < expr.Length) { char c = expr[pos]; switch (c) { case '.': { pos++; string text4 = ReadIdent(expr, ref pos); if (string.IsNullOrEmpty(text4)) { compiledExpr.CompileError = "expected member after '.'"; return compiledExpr; } compiledExpr.Steps.Add(new Step { Name = text4, NullSafe = false }); break; } case '?': { if (pos + 1 >= expr.Length || expr[pos + 1] != '.') { compiledExpr.CompileError = "expected '?.' at " + pos; return compiledExpr; } pos += 2; string text3 = ReadIdent(expr, ref pos); if (string.IsNullOrEmpty(text3)) { compiledExpr.CompileError = "expected member after '?.'"; return compiledExpr; } compiledExpr.Steps.Add(new Step { Name = text3, NullSafe = true }); break; } case '[': { pos++; int num = pos; for (; pos < expr.Length && expr[pos] != ']'; pos++) { } if (pos >= expr.Length) { compiledExpr.CompileError = "unterminated [index]"; return compiledExpr; } string text2 = expr.Substring(num, pos - num).Trim(); pos++; if (!int.TryParse(text2, out var result2)) { compiledExpr.CompileError = "bad index '" + text2 + "'"; return compiledExpr; } compiledExpr.Steps.Add(new Step { IsIndex = true, Index = result2 }); break; } default: compiledExpr.CompileError = "unexpected '" + c + "' at " + pos; return compiledExpr; } } return compiledExpr; } public static string Evaluate(CompiledExpr ce, object instance, object[] args, object result, Exception ex, int maxLen) { if (ce == null) { return "err:null_expr"; } if (!string.IsNullOrEmpty(ce.CompileError)) { return "err:compile_" + ce.CompileError; } object obj; try { obj = GetRoot(ce, instance, args, result, ex); } catch (Exception ex2) { return "err:root_" + ex2.GetType().Name; } int num = ce.Steps?.Count ?? 0; for (int i = 0; i < num; i++) { Step step = ce.Steps[i]; if (obj == null || IsUnityNull(obj)) { if (!step.NullSafe) { return "null-deref:" + (step.IsIndex ? ("[" + step.Index + "]") : step.Name); } return "null"; } if (step.IsIndex) { if (!(obj is IList list)) { return "err:not_ilist"; } if (step.Index < 0 || step.Index >= list.Count) { return "err:idx_oor"; } try { obj = list[step.Index]; } catch (Exception ex3) { return "err:idx_" + ex3.GetType().Name; } continue; } if ((step.Name == "Count" || step.Name == "Length") && TryGetCollectionSize(obj, out var size)) { obj = size; continue; } bool flag = i == 0 && ce.Root == RootKind.StaticType; Type type; object instance2; if (flag) { type = ce.StaticRootType; instance2 = null; } else { type = obj.GetType(); instance2 = obj; } MemberReader member = GetMember(type, step.Name, flag); if (member.IsNone) { return "err:no_member_" + step.Name; } try { obj = member.Read(instance2); } catch (Exception ex4) { return "err:read_" + ex4.GetType().Name; } } if (num == 0 && ce.Root == RootKind.Args) { return FormatArgs(args, maxLen); } return ValueFormatter.Format(obj, maxLen); } private static string FormatArgs(object[] args, int maxLen) { if (args == null) { return "null"; } if (args.Length == 0) { return "[]"; } StringBuilder stringBuilder = new StringBuilder(64); stringBuilder.Append('['); for (int i = 0; i < args.Length; i++) { if (i > 0) { stringBuilder.Append(", "); } stringBuilder.Append(ValueFormatter.Format(args[i], 32)); } stringBuilder.Append(']'); string text = stringBuilder.ToString(); if (text.Length <= maxLen) { return text; } return text.Substring(0, maxLen) + "..."; } private static object GetRoot(CompiledExpr ce, object instance, object[] args, object result, Exception ex) { switch (ce.Root) { case RootKind.Instance: return instance; case RootKind.Args: return args; case RootKind.Result: return result; case RootKind.Exception: return ex; case RootKind.Arg: if (args == null || ce.ArgIndex >= args.Length) { return null; } return args[ce.ArgIndex]; case RootKind.StaticType: return null; default: return null; } } private static Type ResolveType(string name) { lock (_cacheLock) { if (_typeCache.TryGetValue(name, out var value)) { return value; } Type type = null; Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { Type[] types; try { types = assembly.GetTypes(); } catch { continue; } Type[] array = types; foreach (Type type2 in array) { if (type2.Name == name) { type = type2; break; } } if (type != null) { break; } } _typeCache[name] = type; return type; } } private static MemberReader GetMember(Type type, string name, bool isStatic) { string key = (isStatic ? "S|" : "I|") + type.FullName + "|" + name; lock (_cacheLock) { if (_memberCache.TryGetValue(key, out var value)) { return value; } BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | (isStatic ? BindingFlags.Static : BindingFlags.Instance); MemberReader memberReader = new MemberReader(); FieldInfo fieldInfo = null; PropertyInfo propertyInfo = null; Type type2 = type; while (type2 != null && fieldInfo == null && propertyInfo == null) { try { fieldInfo = type2.GetField(name, bindingFlags | BindingFlags.DeclaredOnly); } catch { } if (fieldInfo == null) { try { propertyInfo = type2.GetProperty(name, bindingFlags | BindingFlags.DeclaredOnly); } catch { } } type2 = type2.BaseType; } memberReader.Field = fieldInfo; memberReader.Property = propertyInfo; memberReader.IsNone = fieldInfo == null && propertyInfo == null; _memberCache[key] = memberReader; return memberReader; } } private static bool TryGetCollectionSize(object v, out int size) { size = 0; if (v is Array array) { size = array.Length; return true; } if (v is ICollection collection) { size = collection.Count; return true; } PropertyInfo property = v.GetType().GetProperty("Count"); if (property != null && property.PropertyType == typeof(int)) { try { size = (int)property.GetValue(v, null); return true; } catch { } } return false; } private static bool IsUnityNull(object v) { if (v == null) { return true; } Object val = (Object)((v is Object) ? v : null); if (val == null) { return false; } return val == (Object)null; } private static string ReadIdent(string s, ref int pos) { int num = pos; for (; pos < s.Length; pos++) { char c = s[pos]; switch (c) { default: if ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) { continue; } break; case '_': case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': case 'o': case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': continue; } break; } if (pos <= num) { return ""; } return s.Substring(num, pos - num); } public static void ClearCaches() { lock (_cacheLock) { _typeCache.Clear(); _memberCache.Clear(); } } } internal static class FieldProbe { private sealed class Rule { public int Id; public string TargetTypeName; public string TargetMethodName; public string TargetKey; public string[] RawFields; public ExpressionEvaluator.CompiledExpr[] Compiled; public int RateLimit; public int MaxValueLen; public bool IncludeStack; public int StackMaxDepth; public string Note; public bool Enabled; public bool NeedsPostfix; public bool NeedsFinalizer; public bool NeedsPrefix; } public static bool Enabled = false; public static string RulesFilePath = ""; public static int DefaultRateLimit = 60; public static int DefaultMaxValueLen = 128; public static bool DefaultIncludeStack = false; public static int DefaultStackMaxDepth = 5; private static readonly Dictionary> _methodToRules = new Dictionary>(); private static readonly object _rateLock = new object(); private static readonly Dictionary _counter = new Dictionary(); private static readonly Dictionary _windowStart = new Dictionary(); private static readonly HashSet _overflowWarned = new HashSet(); private static readonly long _ticksPerSec = 10000000L; private static int _hookedRules = 0; private static bool _inited; public static void Initialize(Harmony harmony) { //IL_04b7: Unknown result type (might be due to invalid IL or missing references) //IL_04bc: Unknown result type (might be due to invalid IL or missing references) //IL_04c9: Expected O, but got Unknown //IL_04d9: Unknown result type (might be due to invalid IL or missing references) //IL_04de: Unknown result type (might be due to invalid IL or missing references) //IL_04eb: Expected O, but got Unknown //IL_04fb: Unknown result type (might be due to invalid IL or missing references) //IL_0500: Unknown result type (might be due to invalid IL or missing references) //IL_050d: Expected O, but got Unknown //IL_051d: Unknown result type (might be due to invalid IL or missing references) //IL_0522: Unknown result type (might be due to invalid IL or missing references) //IL_052f: Expected O, but got Unknown if (_inited) { return; } _inited = true; if (!Enabled || string.IsNullOrWhiteSpace(RulesFilePath)) { ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogInfo((object)"[WHY_LAG] FieldProbe disabled or rules file empty, skip"); } return; } if (!File.Exists(RulesFilePath)) { ManualLogSource log2 = WhySoLaggyPlugin.Log; if (log2 != null) { log2.LogWarning((object)("[WHY_LAG] FieldProbe rules file not found: " + RulesFilePath)); } return; } string json; try { json = File.ReadAllText(RulesFilePath, Encoding.UTF8); } catch (Exception ex) { ManualLogSource log3 = WhySoLaggyPlugin.Log; if (log3 != null) { log3.LogWarning((object)("[WHY_LAG] FieldProbe read rules failed: " + ex.Message)); } return; } if (!MiniJson.TryParse(json, out var value, out var error)) { ManualLogSource log4 = WhySoLaggyPlugin.Log; if (log4 != null) { log4.LogWarning((object)("[WHY_LAG] FieldProbe JSON parse failed: " + error)); } return; } Dictionary dictionary = MiniJson.AsObject(value); if (dictionary == null) { ManualLogSource log5 = WhySoLaggyPlugin.Log; if (log5 != null) { log5.LogWarning((object)"[WHY_LAG] FieldProbe rules root is not object"); } return; } if (!MiniJson.GetBool(dictionary, "enabled", defVal: true)) { ManualLogSource log6 = WhySoLaggyPlugin.Log; if (log6 != null) { log6.LogInfo((object)"[WHY_LAG] FieldProbe rules file disabled by 'enabled:false'"); } return; } int @int = MiniJson.GetInt(dictionary, "rateLimitPerRule", DefaultRateLimit); int int2 = MiniJson.GetInt(dictionary, "maxValueLen", DefaultMaxValueLen); bool @bool = MiniJson.GetBool(dictionary, "includeStack", DefaultIncludeStack); int int3 = MiniJson.GetInt(dictionary, "stackMaxDepth", DefaultStackMaxDepth); object value2; List list = MiniJson.AsArray(dictionary.TryGetValue("rules", out value2) ? value2 : null); if (list == null || list.Count == 0) { ManualLogSource log7 = WhySoLaggyPlugin.Log; if (log7 != null) { log7.LogInfo((object)"[WHY_LAG] FieldProbe: no rules in file"); } return; } int num = 0; List list2 = new List(); foreach (object item in list) { Dictionary dictionary2 = MiniJson.AsObject(item); if (dictionary2 == null || !MiniJson.GetBool(dictionary2, "enabled", defVal: true)) { continue; } string @string = MiniJson.GetString(dictionary2, "target"); if (string.IsNullOrEmpty(@string) || @string.IndexOf('.') <= 0) { ManualLogSource log8 = WhySoLaggyPlugin.Log; if (log8 != null) { log8.LogWarning((object)("[WHY_LAG] FieldProbe rule skipped: bad target '" + @string + "'")); } continue; } object value3; List list3 = MiniJson.AsArray(dictionary2.TryGetValue("fields", out value3) ? value3 : null); if (list3 == null || list3.Count == 0) { ManualLogSource log9 = WhySoLaggyPlugin.Log; if (log9 != null) { log9.LogWarning((object)("[WHY_LAG] FieldProbe rule skipped (no fields): " + @string)); } continue; } int num2 = @string.IndexOf('.'); Rule rule = new Rule(); num = (rule.Id = num + 1); rule.TargetTypeName = @string.Substring(0, num2); rule.TargetMethodName = @string.Substring(num2 + 1); rule.TargetKey = @string; rule.RateLimit = MiniJson.GetInt(dictionary2, "rateLimit", @int); rule.MaxValueLen = MiniJson.GetInt(dictionary2, "maxValueLen", int2); rule.IncludeStack = MiniJson.GetBool(dictionary2, "includeStack", @bool); rule.StackMaxDepth = MiniJson.GetInt(dictionary2, "stackMaxDepth", int3); rule.Note = MiniJson.GetString(dictionary2, "note", ""); rule.Enabled = true; Rule rule2 = rule; List list4 = new List(); List list5 = new List(); bool flag = false; bool flag2 = false; foreach (object item2 in list3) { string text = item2 as string; if (string.IsNullOrEmpty(text)) { continue; } text = text.Trim(); ExpressionEvaluator.CompiledExpr compiledExpr = ExpressionEvaluator.Compile(text); if (!string.IsNullOrEmpty(compiledExpr.CompileError)) { ManualLogSource log10 = WhySoLaggyPlugin.Log; if (log10 != null) { log10.LogWarning((object)("[WHY_LAG] FieldProbe compile err (" + @string + "): '" + text + "' → " + compiledExpr.CompileError)); } } else { if (compiledExpr.Root == ExpressionEvaluator.RootKind.Result) { flag = true; } if (compiledExpr.Root == ExpressionEvaluator.RootKind.Exception) { flag2 = true; } list4.Add(text); list5.Add(compiledExpr); } } if (list5.Count != 0) { rule2.RawFields = list4.ToArray(); rule2.Compiled = list5.ToArray(); rule2.NeedsFinalizer = flag2; rule2.NeedsPostfix = flag && !flag2; rule2.NeedsPrefix = !rule2.NeedsPostfix && !rule2.NeedsFinalizer; list2.Add(rule2); } } if (list2.Count == 0) { ManualLogSource log11 = WhySoLaggyPlugin.Log; if (log11 != null) { log11.LogInfo((object)"[WHY_LAG] FieldProbe: no valid rules after parsing"); } return; } HarmonyMethod val = new HarmonyMethod(typeof(FieldProbe), "OnPrefix", (Type[])null) { priority = 800 }; HarmonyMethod val2 = new HarmonyMethod(typeof(FieldProbe), "OnArgsCapture", (Type[])null) { priority = 800 }; HarmonyMethod val3 = new HarmonyMethod(typeof(FieldProbe), "OnPostfix", (Type[])null) { priority = 800 }; HarmonyMethod val4 = new HarmonyMethod(typeof(FieldProbe), "OnFinalizer", (Type[])null) { priority = 800 }; Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { Type[] types; try { types = assembly.GetTypes(); } catch { continue; } Type[] array = types; foreach (Type type in array) { if (type == null) { continue; } string name = type.Name; foreach (Rule item3 in list2) { if (name != item3.TargetTypeName) { continue; } MethodInfo[] methods; try { methods = type.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); } catch { continue; } int num3 = 0; MethodInfo[] array2 = methods; foreach (MethodInfo methodInfo in array2) { if (methodInfo == null || methodInfo.Name != item3.TargetMethodName || methodInfo.IsAbstract || methodInfo.ContainsGenericParameters) { continue; } try { HarmonyMethod val5 = null; if (item3.NeedsPrefix) { val5 = val; } else if (item3.NeedsPostfix || item3.NeedsFinalizer) { val5 = val2; } harmony.Patch((MethodBase)methodInfo, val5, item3.NeedsPostfix ? val3 : null, (HarmonyMethod)null, item3.NeedsFinalizer ? val4 : null, (HarmonyMethod)null); if (!_methodToRules.TryGetValue(methodInfo, out var value4)) { value4 = new List(); _methodToRules[methodInfo] = value4; } value4.Add(item3); _hookedRules++; num3++; string text2 = FormatSig(methodInfo); AbuseLogger.Write("[FIELD_PROBE] Hooked " + item3.TargetKey + text2 + " (mode=" + (item3.NeedsPostfix ? "Postfix" : (item3.NeedsFinalizer ? "Finalizer" : "Prefix")) + ", " + $"fields={item3.Compiled.Length}, rate={item3.RateLimit}/s, stack={item3.IncludeStack})"); } catch (Exception ex2) { ManualLogSource log12 = WhySoLaggyPlugin.Log; if (log12 != null) { log12.LogWarning((object)("[WHY_LAG] FieldProbe Patch failed on " + item3.TargetKey + FormatSig(methodInfo) + ": " + ex2.Message)); } } } if (num3 == 0) { ManualLogSource log13 = WhySoLaggyPlugin.Log; if (log13 != null) { log13.LogInfo((object)("[WHY_LAG] FieldProbe: type " + name + " matched in asm " + assembly.GetName().Name + " but no method '" + item3.TargetMethodName + "' found")); } } } } } AbuseLogger.Write($"[FIELD_PROBE] Initialized (rules={list2.Count}, hooks={_hookedRules}, file={Path.GetFileName(RulesFilePath)})"); } public static void OnPrefix(MethodBase __originalMethod, object[] __args, object __instance, out object[] __state) { __state = ((__args != null) ? ((object[])__args.Clone()) : null); Dispatch(__originalMethod, __args, __instance, null, null, 0); } public static void OnArgsCapture(object[] __args, out object[] __state) { __state = ((__args != null) ? ((object[])__args.Clone()) : null); } public static void OnPostfix(MethodBase __originalMethod, object __instance, object __result, object[] __state) { Dispatch(__originalMethod, __state, __instance, __result, null, 1); } public static Exception OnFinalizer(MethodBase __originalMethod, object __instance, object __result, Exception __exception, object[] __state) { Dispatch(__originalMethod, __state, __instance, __result, __exception, 2); return __exception; } private static void Dispatch(MethodBase mb, object[] args, object instance, object result, Exception ex, int kind) { if (mb == null || !_methodToRules.TryGetValue(mb, out var value)) { return; } for (int i = 0; i < value.Count; i++) { Rule rule = value[i]; if (rule.Enabled && ((kind == 0 && rule.NeedsPrefix) || (kind == 1 && rule.NeedsPostfix) || (kind == 2 && rule.NeedsFinalizer)) && CheckRate(rule)) { WriteSnapshot(rule, instance, args, result, ex, mb); } } } private static void WriteSnapshot(Rule rule, object instance, object[] args, object result, Exception ex, MethodBase mb) { StringBuilder stringBuilder = new StringBuilder(128); for (int i = 0; i < rule.Compiled.Length; i++) { if (i > 0) { stringBuilder.Append(";"); } stringBuilder.Append(rule.RawFields[i]); stringBuilder.Append('='); string value; try { value = ExpressionEvaluator.Evaluate(rule.Compiled[i], instance, args, result, ex, rule.MaxValueLen); } catch (Exception ex2) { value = "err:" + ex2.GetType().Name; } stringBuilder.Append(value); } string text = null; string text2 = null; if (rule.IncludeStack) { string stack; try { stack = Environment.StackTrace; } catch { stack = null; } text = FilterStack(stack, rule.StackMaxDepth); text2 = ExtractFirstFrame(text); } try { StructuredEvent evt = default(StructuredEvent); evt.Timestamp = StructuredLogger.NowStamp(); evt.FrameNumber = Time.frameCount; evt.Type = EventType.MethodTrace; evt.Fields = new Dictionary { { "TargetMethod", rule.TargetKey }, { "Snapshot", Truncate(stringBuilder.ToString(), 2000) }, { "TraceStack", text ?? "" }, { "TraceCaller", text2 ?? "" }, { "PatchType", kindToStr(rule) } }; StructuredLogger.WriteEvent(evt); } catch { } } private static string kindToStr(Rule r) { if (!r.NeedsPostfix) { if (!r.NeedsFinalizer) { return "Prefix"; } return "Finalizer"; } return "Postfix"; } private static bool CheckRate(Rule rule) { lock (_rateLock) { long ticks = DateTime.UtcNow.Ticks; _windowStart.TryGetValue(rule.Id, out var value); if (value == 0L || ticks - value >= _ticksPerSec) { _windowStart[rule.Id] = ticks; _counter[rule.Id] = 1; _overflowWarned.Remove(rule.Id); return true; } _counter.TryGetValue(rule.Id, out var value2); value2++; _counter[rule.Id] = value2; if (value2 > rule.RateLimit) { if (_overflowWarned.Add(rule.Id)) { ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogWarning((object)$"[WHY_LAG] FieldProbe rate limit hit for {rule.TargetKey} (>{rule.RateLimit}/s)"); } } return false; } return true; } } private static string FilterStack(string stack, int maxDepth) { if (string.IsNullOrEmpty(stack) || maxDepth <= 0) { return ""; } string[] array = stack.Split(new char[2] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); StringBuilder stringBuilder = new StringBuilder(128); int num = 0; string[] array2 = array; for (int i = 0; i < array2.Length; i++) { string text = array2[i].TrimStart(Array.Empty()); if (text.StartsWith("at ", StringComparison.Ordinal) && text.IndexOf("UnityEngine.", StringComparison.Ordinal) < 0 && text.IndexOf("HarmonyLib.", StringComparison.Ordinal) < 0 && text.IndexOf("MonoMod.", StringComparison.Ordinal) < 0 && text.IndexOf("System.Environment.", StringComparison.Ordinal) < 0 && text.IndexOf("WhySoLaggy.FieldProbe", StringComparison.Ordinal) < 0 && text.IndexOf("WhySoLaggy.ExpressionEvaluator", StringComparison.Ordinal) < 0) { if (num > 0) { stringBuilder.Append(" | "); } stringBuilder.Append(text); num++; if (num >= maxDepth) { break; } } } return stringBuilder.ToString(); } private static string ExtractFirstFrame(string filtered) { if (string.IsNullOrEmpty(filtered)) { return null; } int num = filtered.IndexOf('|'); if (num <= 0) { return filtered; } return filtered.Substring(0, num).Trim(); } private static string FormatSig(MethodBase mb) { if (mb == null) { return ""; } ParameterInfo[] parameters = mb.GetParameters(); if (parameters == null || parameters.Length == 0) { return "()"; } StringBuilder stringBuilder = new StringBuilder(32); stringBuilder.Append('('); for (int i = 0; i < parameters.Length; i++) { if (i > 0) { stringBuilder.Append(','); } stringBuilder.Append(parameters[i].ParameterType.Name); } stringBuilder.Append(')'); return stringBuilder.ToString(); } private static string Truncate(string s, int max) { if (string.IsNullOrEmpty(s) || s.Length <= max) { return s; } return s.Substring(0, max) + "..."; } } internal static class FpsTracker { public static int SpikeThresholdMs = 50; public static int ReportIntervalSeconds = 10; public static bool EnableMemoryMonitor = true; private const int WindowSize = 600; private static readonly float[] _frameTimes = new float[600]; private static int _writeIndex; private static int _sampleCount; private static float _periodTimer; private static int _periodFrames; private static float _periodSumMs; private static float _periodMinFps = float.MaxValue; private static float _periodMaxFps; private static int _periodSpikeCount; private static long _memLastSampleBytes; private static float _memSampleTimer; private static float _memLastRateKBps; private const float MemSampleInterval = 1f; private static readonly StringBuilder _reportSb = new StringBuilder(256); public static bool IsSpikeFrame { get; private set; } public static float CurrentFrameMs { get; private set; } public static void Tick() { float unscaledDeltaTime = Time.unscaledDeltaTime; float num2 = (CurrentFrameMs = unscaledDeltaTime * 1000f); _frameTimes[_writeIndex] = num2; _writeIndex = (_writeIndex + 1) % 600; if (_sampleCount < 600) { _sampleCount++; } IsSpikeFrame = num2 > (float)SpikeThresholdMs; if (IsSpikeFrame) { _periodSpikeCount++; } _periodFrames++; _periodSumMs += num2; float num3 = ((unscaledDeltaTime > 0f) ? (1f / unscaledDeltaTime) : 0f); if (num3 < _periodMinFps) { _periodMinFps = num3; } if (num3 > _periodMaxFps) { _periodMaxFps = num3; } if (EnableMemoryMonitor) { _memSampleTimer += unscaledDeltaTime; if (_memSampleTimer >= 1f) { try { long totalAllocatedMemoryLong = Profiler.GetTotalAllocatedMemoryLong(); if (_memLastSampleBytes > 0 && totalAllocatedMemoryLong >= _memLastSampleBytes) { _memLastRateKBps = (float)((double)(totalAllocatedMemoryLong - _memLastSampleBytes) / 1024.0 / (double)_memSampleTimer); } _memLastSampleBytes = totalAllocatedMemoryLong; } catch (Exception ex) { ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogWarning((object)("[WHY_LAG] MemoryMonitor sample failed: " + ex.Message)); } EnableMemoryMonitor = false; } _memSampleTimer = 0f; } } _periodTimer += unscaledDeltaTime; if (_periodTimer >= (float)ReportIntervalSeconds) { WriteReport(); ResetPeriod(); } } public static float GetWindowAvgMs() { if (_sampleCount == 0) { return 0f; } float num = 0f; for (int i = 0; i < _sampleCount; i++) { num += _frameTimes[i]; } return num / (float)_sampleCount; } public static float GetAllocRateKBps() { return _memLastRateKBps; } private static void WriteReport() { if (_periodFrames == 0) { return; } float num = ((_periodSumMs > 0f) ? ((float)_periodFrames / (_periodSumMs / 1000f)) : 0f); float num2 = _periodSumMs / (float)_periodFrames; _reportSb.Clear(); _reportSb.AppendLine($"[WHY_LAG] === FPS Report ({_periodTimer:F1}s, {_periodFrames} frames) ==="); _reportSb.AppendLine($"[WHY_LAG] FPS: avg={num:F1} | min={_periodMinFps:F1} | max={_periodMaxFps:F1} | avgFrame={num2:F1}ms"); _reportSb.Append($"[WHY_LAG] Spikes(>{SpikeThresholdMs}ms): {_periodSpikeCount}"); if (EnableMemoryMonitor) { _reportSb.Append($" | AllocRate: {_memLastRateKBps:F1} KB/s"); } LagLogger.Write(_reportSb.ToString()); try { Dictionary dictionary = new Dictionary { { "AvgFps", Math.Round(num, 1) }, { "MinFps", Math.Round(_periodMinFps, 1) }, { "MaxFps", Math.Round(_periodMaxFps, 1) }, { "AvgFrameMs", Math.Round(num2, 2) }, { "SpikeThresholdMs", SpikeThresholdMs }, { "SpikeCount", _periodSpikeCount }, { "ReportDuration", Math.Round(_periodTimer, 2) } }; if (EnableMemoryMonitor) { dictionary["AllocRateKBps"] = Math.Round(_memLastRateKBps, 2); } StructuredEvent evt = default(StructuredEvent); evt.Timestamp = StructuredLogger.NowStamp(); evt.FrameNumber = Time.frameCount; evt.Type = EventType.FpsReport; evt.Fields = dictionary; StructuredLogger.WriteEvent(evt); } catch (Exception ex) { ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogWarning((object)("[WHY_LAG] FpsTracker WriteEvent failed: " + ex.Message)); } } } private static void ResetPeriod() { _periodTimer = 0f; _periodFrames = 0; _periodSumMs = 0f; _periodMinFps = float.MaxValue; _periodMaxFps = 0f; _periodSpikeCount = 0; } } internal static class HarmonyScanner { public static void Scan(string bepInExDir) { try { string text = Path.Combine(bepInExDir, "harmony_patches.csv"); using StreamWriter streamWriter = new StreamWriter(text, append: false, Encoding.UTF8); streamWriter.WriteLine("TargetMethod,PatchType,OwnerHarmonyId,Priority"); int num = 0; int num2 = 0; foreach (MethodBase allPatchedMethod in Harmony.GetAllPatchedMethods()) { if (allPatchedMethod == null) { continue; } Patches val = null; try { val = Harmony.GetPatchInfo(allPatchedMethod); } catch (Exception ex) { ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogWarning((object)("[WHY_LAG] HarmonyScanner GetPatchInfo failed on " + allPatchedMethod.Name + ": " + ex.Message)); } continue; } if (val != null) { string text2 = (allPatchedMethod.DeclaringType?.FullName ?? "?") + "." + allPatchedMethod.Name; HashSet hashSet = new HashSet(StringComparer.Ordinal); WriteGroup(streamWriter, val.Prefixes, "Prefix", text2, hashSet); WriteGroup(streamWriter, val.Postfixes, "Postfix", text2, hashSet); WriteGroup(streamWriter, val.Transpilers, "Transpiler", text2, hashSet); WriteGroup(streamWriter, val.Finalizers, "Finalizer", text2, hashSet); num++; if (hashSet.Count >= 2) { num2++; AbuseLogger.Write(string.Format("[HARMONY_SCAN] Potential conflict: {0} patched by {1} mods: {2}", text2, hashSet.Count, string.Join(", ", hashSet))); } } } streamWriter.Flush(); AbuseLogger.Write($"[HARMONY_SCAN] Scanned {num} patched methods, {num2} potential conflicts. CSV -> {text}"); } catch (Exception ex2) { ManualLogSource log2 = WhySoLaggyPlugin.Log; if (log2 != null) { log2.LogError((object)("[WHY_LAG] HarmonyScanner failed: " + ex2.Message)); } } } private static void WriteGroup(StreamWriter sw, IList list, string kind, string target, HashSet owners) { if (list == null) { return; } foreach (Patch item in list) { if (item != null) { string text = item.owner ?? "?"; owners.Add(text); string value = CsvEscape(target); string value2 = CsvEscape(text); sw.Write(value); sw.Write(','); sw.Write(kind); sw.Write(','); sw.Write(value2); sw.Write(','); int priority = item.priority; sw.Write(priority.ToString(CultureInfo.InvariantCulture)); sw.WriteLine(); try { StructuredEvent evt = default(StructuredEvent); evt.Timestamp = StructuredLogger.NowStamp(); evt.FrameNumber = Time.frameCount; evt.Type = EventType.HarmonyPatchMap; evt.Fields = new Dictionary { { "TargetMethod", target }, { "PatchType", kind }, { "OwnerHarmonyId", text }, { "Priority", item.priority } }; StructuredLogger.WriteEvent(evt); } catch { } } } } private static string CsvEscape(string s) { if (string.IsNullOrEmpty(s)) { return ""; } bool flag = false; foreach (char c in s) { if (c == ',' || c == '"' || c == '\n' || c == '\r') { flag = true; break; } } if (!flag) { return s; } StringBuilder stringBuilder = new StringBuilder(s.Length + 8); stringBuilder.Append('"'); foreach (char c2 in s) { if (c2 == '"') { stringBuilder.Append("\"\""); } else { stringBuilder.Append(c2); } } stringBuilder.Append('"'); return stringBuilder.ToString(); } } internal static class LagLogger { private static StreamWriter _writer; private static readonly object _lock = new object(); public static void Initialize(string bepInExDir) { try { string text = Path.Combine(bepInExDir, "WhySoLaggy.log"); _writer = new StreamWriter(text, append: false, Encoding.UTF8) { AutoFlush = true }; _writer.WriteLine("[" + Timestamp() + "] WhySoLaggy Performance Monitor started"); _writer.WriteLine("[" + Timestamp() + "] Log file: " + text); _writer.WriteLine(new string('=', 60)); } catch (Exception ex) { Logger.CreateLogSource("WhySoLaggy").LogError((object)("Failed to create log file: " + ex.Message)); } } public static void Write(string message) { if (_writer == null || !LogFilter.AllowLag()) { return; } lock (_lock) { try { _writer.WriteLine("[" + Timestamp() + "] " + message); } catch { } } } public static void Info(string message) { if (_writer == null) { return; } lock (_lock) { try { _writer.WriteLine("[" + Timestamp() + "] " + message); } catch { } } } public static void WriteRaw(string line) { if (_writer == null) { return; } lock (_lock) { try { _writer.WriteLine(line); } catch { } } } public static void Flush() { if (_writer == null) { return; } lock (_lock) { try { _writer.Flush(); } catch { } } } public static void Shutdown() { if (_writer == null) { return; } lock (_lock) { try { _writer.WriteLine("[" + Timestamp() + "] WhySoLaggy shutting down"); _writer.Flush(); _writer.Close(); _writer = null; } catch { } } } private static string Timestamp() { return DateTime.Now.ToString("HH:mm:ss.fff"); } } public enum LogVerbosity { Minimal, Normal } public static class LogFilter { public static LogVerbosity Level; public static bool AllowLag() { return Level >= LogVerbosity.Normal; } public static bool AllowAbuse() { return Level >= LogVerbosity.Normal; } } internal static class MethodTracer { public static string TraceMethodNames = ""; public static int TraceMaxDepth = 5; public static int TraceRateLimit = 100; private static readonly object _rateLock = new object(); private static readonly Dictionary _counter = new Dictionary(StringComparer.Ordinal); private static readonly Dictionary _windowStartTicks = new Dictionary(StringComparer.Ordinal); private static readonly HashSet _overflowWarnedInWindow = new HashSet(StringComparer.Ordinal); private static readonly long _ticksPerSecond = 10000000L; private static bool _inited; private static int _hooked; public static void Initialize(Harmony harmony) { //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Expected O, but got Unknown if (_inited) { return; } _inited = true; if (string.IsNullOrWhiteSpace(TraceMethodNames)) { return; } HashSet hashSet = new HashSet(StringComparer.Ordinal); string[] array = TraceMethodNames.Split(new char[1] { ',' }); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (!string.IsNullOrEmpty(text)) { hashSet.Add(text); } } if (hashSet.Count == 0) { return; } HarmonyMethod val = new HarmonyMethod(typeof(MethodTracer), "OnPrefix", (Type[])null) { priority = 800 }; Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { Type[] array2 = null; try { array2 = assembly.GetTypes(); } catch (Exception ex) { ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogWarning((object)("[WHY_LAG] MethodTracer GetTypes failed in asm=" + assembly.GetName().Name + ": " + ex.Message)); } continue; } Type[] array3 = array2; foreach (Type type in array3) { string name = type.Name; foreach (string item in hashSet) { int num = item.IndexOf('.'); if (num <= 0) { continue; } string text2 = item.Substring(0, num); string name2 = item.Substring(num + 1); if (name != text2) { continue; } MethodInfo methodInfo = null; try { methodInfo = type.GetMethod(name2, BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); } catch { } if (methodInfo == null) { continue; } try { harmony.Patch((MethodBase)methodInfo, val, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _hooked++; AbuseLogger.Write("[METHOD_TRACE] Hooked " + item); } catch (Exception ex2) { ManualLogSource log2 = WhySoLaggyPlugin.Log; if (log2 != null) { log2.LogWarning((object)("[WHY_LAG] MethodTracer Patch failed on " + item + ": " + ex2.Message)); } } } } } AbuseLogger.Write($"[METHOD_TRACE] Initialized (hooked: {_hooked}, maxDepth: {TraceMaxDepth}, rateLimit: {TraceRateLimit}/s)"); } public static void OnPrefix(MethodBase __originalMethod) { if (__originalMethod == null) { return; } string text = (__originalMethod.DeclaringType?.Name ?? "?") + "." + __originalMethod.Name; if (!CheckRateLimit(text)) { return; } string stackTrace; try { stackTrace = Environment.StackTrace; } catch { return; } string text2 = FilterStack(stackTrace, TraceMaxDepth); string text3 = ExtractFirstFrame(text2); try { StructuredEvent evt = default(StructuredEvent); evt.Timestamp = StructuredLogger.NowStamp(); evt.FrameNumber = Time.frameCount; evt.Type = EventType.MethodTrace; evt.Fields = new Dictionary { { "TargetMethod", text }, { "TraceStack", Truncate(text2, 1200) }, { "TraceCaller", text3 ?? "" } }; StructuredLogger.WriteEvent(evt); } catch { } } private static bool CheckRateLimit(string key) { lock (_rateLock) { long ticks = DateTime.UtcNow.Ticks; _windowStartTicks.TryGetValue(key, out var value); if (value == 0L || ticks - value >= _ticksPerSecond) { _windowStartTicks[key] = ticks; _counter[key] = 1; _overflowWarnedInWindow.Remove(key); return true; } _counter.TryGetValue(key, out var value2); value2++; _counter[key] = value2; if (value2 > TraceRateLimit) { if (_overflowWarnedInWindow.Add(key)) { ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogWarning((object)$"[WHY_LAG] MethodTracer rate limit exceeded for {key} (>{TraceRateLimit}/s); suppressing additional traces this second"); } } return false; } return true; } } private static string FilterStack(string stack, int maxDepth) { if (string.IsNullOrEmpty(stack)) { return ""; } string[] array = stack.Split(new char[2] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); StringBuilder stringBuilder = new StringBuilder(256); int num = 0; string[] array2 = array; for (int i = 0; i < array2.Length; i++) { string text = array2[i].TrimStart(Array.Empty()); if (text.StartsWith("at ", StringComparison.Ordinal) && text.IndexOf("UnityEngine.", StringComparison.Ordinal) < 0 && text.IndexOf("HarmonyLib.", StringComparison.Ordinal) < 0 && text.IndexOf("MonoMod.", StringComparison.Ordinal) < 0 && text.IndexOf("System.Environment.", StringComparison.Ordinal) < 0 && text.IndexOf("WhySoLaggy.MethodTracer", StringComparison.Ordinal) < 0) { if (num > 0) { stringBuilder.Append(" | "); } stringBuilder.Append(text); num++; if (num >= maxDepth) { break; } } } return stringBuilder.ToString(); } private static string ExtractFirstFrame(string filtered) { if (string.IsNullOrEmpty(filtered)) { return null; } int num = filtered.IndexOf('|'); if (num <= 0) { return filtered; } return filtered.Substring(0, num).Trim(); } private static string Truncate(string s, int max) { if (string.IsNullOrEmpty(s) || s.Length <= max) { return s; } return s.Substring(0, max) + "..."; } } internal static class MiniJson { public static object Parse(string json) { if (string.IsNullOrEmpty(json)) { return null; } int pos = 0; SkipWs(json, ref pos); object result = ParseValue(json, ref pos); SkipWs(json, ref pos); if (pos != json.Length) { throw new FormatException($"MiniJson: unexpected trailing chars at {pos}"); } return result; } public static bool TryParse(string json, out object value, out string error) { try { value = Parse(json); error = null; return true; } catch (Exception ex) { value = null; error = ex.Message; return false; } } public static Dictionary AsObject(object v) { return v as Dictionary; } public static List AsArray(object v) { return v as List; } public static string GetString(Dictionary obj, string key, string defVal = null) { if (obj == null || !obj.TryGetValue(key, out var value) || value == null) { return defVal; } return (value as string) ?? Convert.ToString(value, CultureInfo.InvariantCulture); } public static bool GetBool(Dictionary obj, string key, bool defVal) { if (obj == null || !obj.TryGetValue(key, out var value) || value == null) { return defVal; } if (value is bool) { return (bool)value; } if (value is string value2) { if (!bool.TryParse(value2, out var result)) { return defVal; } return result; } return defVal; } public static int GetInt(Dictionary obj, string key, int defVal) { if (obj == null || !obj.TryGetValue(key, out var value) || value == null) { return defVal; } if (value is long num) { return (int)num; } if (value is double num2) { return (int)num2; } if (value is string s && int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { return result; } return defVal; } private static object ParseValue(string s, ref int pos) { SkipWs(s, ref pos); if (pos >= s.Length) { throw new FormatException($"MiniJson: unexpected EOF at {pos}"); } char c = s[pos]; switch (c) { case '{': return ParseObject(s, ref pos); case '[': return ParseArray(s, ref pos); case '"': return ParseString(s, ref pos); case 'f': case 't': return ParseBool(s, ref pos); case 'n': return ParseNull(s, ref pos); case '-': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': return ParseNumber(s, ref pos); default: throw new FormatException($"MiniJson: unexpected '{c}' at {pos}"); } } private static Dictionary ParseObject(string s, ref int pos) { Dictionary dictionary = new Dictionary(StringComparer.Ordinal); pos++; SkipWs(s, ref pos); if (pos < s.Length && s[pos] == '}') { pos++; return dictionary; } while (true) { SkipWs(s, ref pos); if (pos >= s.Length || s[pos] != '"') { throw new FormatException($"MiniJson: expected string key at {pos}"); } string key = ParseString(s, ref pos); SkipWs(s, ref pos); if (pos >= s.Length || s[pos] != ':') { throw new FormatException($"MiniJson: expected ':' at {pos}"); } pos++; object value = ParseValue(s, ref pos); dictionary[key] = value; SkipWs(s, ref pos); if (pos >= s.Length) { throw new FormatException("MiniJson: unexpected EOF in object"); } if (s[pos] != ',') { break; } pos++; } if (s[pos] == '}') { pos++; return dictionary; } throw new FormatException($"MiniJson: expected ',' or '}}' at {pos}"); } private static List ParseArray(string s, ref int pos) { List list = new List(); pos++; SkipWs(s, ref pos); if (pos < s.Length && s[pos] == ']') { pos++; return list; } while (true) { object item = ParseValue(s, ref pos); list.Add(item); SkipWs(s, ref pos); if (pos >= s.Length) { throw new FormatException("MiniJson: unexpected EOF in array"); } if (s[pos] != ',') { break; } pos++; } if (s[pos] == ']') { pos++; return list; } throw new FormatException($"MiniJson: expected ',' or ']' at {pos}"); } private static string ParseString(string s, ref int pos) { if (s[pos] != '"') { throw new FormatException($"MiniJson: expected '\"' at {pos}"); } pos++; StringBuilder stringBuilder = new StringBuilder(); while (pos < s.Length) { char c = s[pos++]; switch (c) { case '"': return stringBuilder.ToString(); case '\\': { if (pos >= s.Length) { throw new FormatException("MiniJson: bad escape at EOF"); } char c2 = s[pos++]; switch (c2) { case '"': stringBuilder.Append('"'); break; case '\\': stringBuilder.Append('\\'); break; case '/': stringBuilder.Append('/'); break; case 'b': stringBuilder.Append('\b'); break; case 'f': stringBuilder.Append('\f'); break; case 'n': stringBuilder.Append('\n'); break; case 'r': stringBuilder.Append('\r'); break; case 't': stringBuilder.Append('\t'); break; case 'u': { if (pos + 4 > s.Length) { throw new FormatException("MiniJson: bad \\u at EOF"); } string text = s.Substring(pos, 4); pos += 4; if (!int.TryParse(text, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var result)) { throw new FormatException("MiniJson: bad \\u hex '" + text + "'"); } stringBuilder.Append((char)result); break; } default: throw new FormatException($"MiniJson: unknown escape '\\{c2}' at {pos - 1}"); } break; } default: stringBuilder.Append(c); break; } } throw new FormatException("MiniJson: unterminated string"); } private static object ParseNumber(string s, ref int pos) { int num = pos; if (s[pos] == '-') { pos++; } bool flag = false; while (pos < s.Length) { char c = s[pos]; if (c >= '0' && c <= '9') { pos++; continue; } if (c != '.' && c != 'e' && c != 'E' && c != '+' && c != '-') { break; } flag = true; pos++; } string text = s.Substring(num, pos - num); long result2; if (flag) { if (double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { return result; } } else if (long.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out result2)) { return result2; } throw new FormatException($"MiniJson: bad number '{text}' at {num}"); } private static bool ParseBool(string s, ref int pos) { if (pos + 4 <= s.Length && s.Substring(pos, 4) == "true") { pos += 4; return true; } if (pos + 5 <= s.Length && s.Substring(pos, 5) == "false") { pos += 5; return false; } throw new FormatException($"MiniJson: bad bool at {pos}"); } private static object ParseNull(string s, ref int pos) { if (pos + 4 <= s.Length && s.Substring(pos, 4) == "null") { pos += 4; return null; } throw new FormatException($"MiniJson: bad null at {pos}"); } private static void SkipWs(string s, ref int pos) { while (pos < s.Length) { switch (s[pos]) { case '\t': case '\n': case '\r': case ' ': pos++; break; case '/': if (pos + 1 < s.Length && s[pos + 1] == '/') { pos += 2; while (pos < s.Length && s[pos] != '\n') { pos++; } break; } return; default: return; } } } } internal static class NetworkAbuseDetector { private struct ClientRpcRecord { public float Time; public int Actor; public string Method; } public static int InstantiateRateThreshold = 15; public static int DestroyRateThreshold = 20; public static int RpcRateThreshold = 50; public static int ObjectSpikeThreshold = 30; public static float CheckIntervalSeconds = 1f; public static float ReportIntervalSeconds = 30f; private const byte EventInstantiate = 202; private const byte EventRpc = 200; private const byte EventDestroy = 204; private const byte EventDestroyPlayer = 207; private const byte EventOwnershipRequest = 210; private const byte EventOwnershipTransfer = 211; private const byte EventOwnershipUpdate = 215; private static readonly object _lock = new object(); private static int _localInstantiateCount; private static int _localDestroyCount; private static int _localRpcCount; private static int _remoteInstantiateCount; private static int _remoteDestroyCount; private static int _remoteRpcCount; private static readonly Dictionary _instantiateByActor = new Dictionary(); private static readonly Dictionary _rpcByActor = new Dictionary(); private static readonly Dictionary _destroyByActor = new Dictionary(); private static readonly Dictionary> _rpcByActorMethod = new Dictionary>(); private static readonly Dictionary> _rpcByActorMethodTotal = new Dictionary>(); public static int ActorMethodRateThreshold = 20; private static readonly Dictionary _ownershipGrabbedByActor = new Dictionary(); public static int OwnershipGrabRateThreshold = 10; private static readonly Dictionary _prefabCount = new Dictionary(); private static readonly Dictionary _prefabTraceTaken = new Dictionary(); private static float _lastTraceSampleTime; private const int TraceFirstNPerPrefab = 3; private const float TraceSampleWindowSeconds = 5f; private static bool _forceTraceNext; private static readonly LinkedList _recentClientRpcs = new LinkedList(); private const float SuspectWindowSeconds = 2.5f; private const int SuspectWindowMaxEntries = 256; private static int _lastPhotonViewCount; private static int _lastZombieCount; private static Type _zombieType; private static float _checkTimer; private static float _reportTimer; private static bool _initialized; private static int _totalInstantiates; private static int _totalDestroys; private static int _totalRpcs; private static int _alertCount; private static bool IsChinese { get { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Invalid comparison between Unknown and I4 //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Invalid comparison between Unknown and I4 //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Invalid comparison between Unknown and I4 if ((int)Application.systemLanguage != 6 && (int)Application.systemLanguage != 40) { return (int)Application.systemLanguage == 41; } return true; } } public static void Initialize(Harmony harmony) { if (_initialized) { return; } try { PatchPhotonMethods(harmony); FindGameTypes(); _lastPhotonViewCount = CountPhotonViews(); _lastZombieCount = CountZombies(); _initialized = true; AbuseLogger.Info("[ABUSE] NetworkAbuseDetector initialized (observation-only mode)"); AbuseLogger.Info($"[ABUSE] Thresholds: Instantiate={InstantiateRateThreshold}/s, Destroy={DestroyRateThreshold}/s, RPC={RpcRateThreshold}/s, ObjectSpike={ObjectSpikeThreshold}"); AbuseLogger.Info($"[ABUSE] Initial counts: PhotonViews={_lastPhotonViewCount}, Zombies={_lastZombieCount}"); } catch (Exception arg) { ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogError((object)$"[WHY_LAG] NetworkAbuseDetector init failed: {arg}"); } } } public static void OnNetworkEvent(byte eventCode, int senderActorNumber) { if (!_initialized || (PhotonNetwork.LocalPlayer != null && senderActorNumber == PhotonNetwork.LocalPlayer.ActorNumber)) { return; } lock (_lock) { switch (eventCode) { case 202: _remoteInstantiateCount++; _totalInstantiates++; IncrementActor(_instantiateByActor, senderActorNumber); break; case 200: _remoteRpcCount++; _totalRpcs++; IncrementActor(_rpcByActor, senderActorNumber); break; case 204: case 207: _remoteDestroyCount++; _totalDestroys++; IncrementActor(_destroyByActor, senderActorNumber); break; } } } public static void OnRemoteRpcEvent(EventData photonEvent) { if (!_initialized || photonEvent.Code != 200) { return; } int sender = photonEvent.Sender; try { if (PhotonNetwork.LocalPlayer != null && sender == PhotonNetwork.LocalPlayer.ActorNumber) { return; } } catch { } string text = null; int num = 0; int num2 = 0; try { object customData = photonEvent.CustomData; Hashtable val = (Hashtable)((customData is Hashtable) ? customData : null); if (val != null) { if (val.ContainsKey((byte)0) && val[(byte)0] is int num3) { num = num3; } if (val.ContainsKey((byte)3)) { text = val[(byte)3] as string; } if (string.IsNullOrEmpty(text) && val.ContainsKey((byte)5)) { object obj2 = val[(byte)5]; try { List list = PhotonNetwork.PhotonServerSettings?.RpcList; if (list != null && obj2 is byte) { byte b = (byte)obj2; if (b < list.Count) { text = list[b]; } } } catch { } } if (val.ContainsKey((byte)4) && val[(byte)4] is object[] array) { num2 = array.Length; } } } catch (Exception ex) { ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogWarning((object)("[WHY_LAG] OnRemoteRpcEvent unpack failed: " + ex.Message)); } } if (!string.IsNullOrEmpty(text)) { lock (_lock) { _recentClientRpcs.AddLast(new ClientRpcRecord { Time = Time.realtimeSinceStartup, Actor = sender, Method = text }); if (_recentClientRpcs.Count > 256) { _recentClientRpcs.RemoveFirst(); } if (!_rpcByActorMethod.TryGetValue(sender, out var value)) { value = new Dictionary(StringComparer.Ordinal); _rpcByActorMethod[sender] = value; } value.TryGetValue(text, out var value2); value[text] = value2 + 1; if (!_rpcByActorMethodTotal.TryGetValue(sender, out var value3)) { value3 = new Dictionary(StringComparer.Ordinal); _rpcByActorMethodTotal[sender] = value3; } value3.TryGetValue(text, out var value4); value3[text] = value4 + 1; } } if (string.IsNullOrEmpty(text) || !RpcMonitor.WatchedMethods.Contains(text)) { return; } try { string text2 = null; try { if (PhotonNetwork.CurrentRoom != null && PhotonNetwork.CurrentRoom.Players != null && PhotonNetwork.CurrentRoom.Players.TryGetValue(sender, out var value5)) { text2 = ((value5 != null) ? value5.NickName : null); } } catch { } Dictionary fields = new Dictionary { { "RpcMethod", text }, { "SenderActor", sender }, { "SenderName", text2 ?? "" }, { "TargetViewID", num }, { "IsMasterClient", PhotonNetwork.IsMasterClient ? 1 : 0 }, { "ArgsSummary", $"params={num2}" } }; StructuredEvent evt = default(StructuredEvent); evt.Timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); evt.FrameNumber = Time.frameCount; evt.Type = EventType.RemoteRpcTrace; evt.Fields = fields; StructuredLogger.WriteEvent(evt); } catch (Exception ex2) { ManualLogSource log2 = WhySoLaggyPlugin.Log; if (log2 != null) { log2.LogWarning((object)("[WHY_LAG] RemoteRpcTrace emit failed: " + ex2.Message)); } } } private static bool TryFindRecentClientRpc(float now, out int actor, out string method, out int ageMs) { actor = -1; method = null; ageMs = -1; lock (_lock) { while (_recentClientRpcs.Count > 0 && now - _recentClientRpcs.First.Value.Time > 2.5f) { _recentClientRpcs.RemoveFirst(); } if (_recentClientRpcs.Count == 0) { return false; } ClientRpcRecord value = _recentClientRpcs.Last.Value; actor = value.Actor; method = value.Method; ageMs = (int)((now - value.Time) * 1000f); return true; } } public static void OnOwnershipEvent(EventData photonEvent) { if (!_initialized) { return; } byte code = photonEvent.Code; if (code != 210 && code != 211 && code != 215) { return; } int num = 0; int num2 = 0; try { if (photonEvent.CustomData is int[] array && array.Length >= 2) { num = array[0]; num2 = array[1]; } } catch { } int sender = photonEvent.Sender; string text = code switch { 211 => "Transfer", 210 => "Request", _ => "Update", }; int num3 = ((code == 211 || code == 215) ? num2 : sender); if (num3 > 0) { lock (_lock) { _ownershipGrabbedByActor.TryGetValue(num3, out var value); _ownershipGrabbedByActor[num3] = value + 1; } } try { Dictionary dictionary = new Dictionary { { "RpcMethod", "Ownership" + text }, { "SenderActor", sender }, { "TargetViewID", num }, { "ArgsSummary", $"otherActor={num2}" } }; try { if (PhotonNetwork.CurrentRoom != null && PhotonNetwork.CurrentRoom.Players != null && PhotonNetwork.CurrentRoom.Players.TryGetValue(sender, out var value2)) { dictionary["SenderName"] = ((value2 != null) ? value2.NickName : null); } } catch { } StructuredEvent evt = default(StructuredEvent); evt.Timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); evt.FrameNumber = Time.frameCount; evt.Type = EventType.OwnershipChange; evt.Fields = dictionary; StructuredLogger.WriteEvent(evt); } catch (Exception ex) { ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogWarning((object)("[WHY_LAG] OwnershipChange emit failed: " + ex.Message)); } } } public static void Tick() { if (_initialized) { float unscaledDeltaTime = Time.unscaledDeltaTime; _checkTimer += unscaledDeltaTime; _reportTimer += unscaledDeltaTime; if (_checkTimer >= CheckIntervalSeconds) { CheckRates(); CheckObjectSpike(); CheckActorMethodHotspots(); CheckOwnershipGrab(); ResetCounters(); RpcMonitor.OnWindowEnd(); _checkTimer = 0f; } if (_reportTimer >= ReportIntervalSeconds) { WritePeriodicReport(); ResetReportStats(); _reportTimer = 0f; } } } private static void PatchPhotonMethods(Harmony harmony) { //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Expected O, but got Unknown //IL_00d0: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Expected O, but got Unknown //IL_0146: Unknown result type (might be due to invalid IL or missing references) //IL_0154: Expected O, but got Unknown MethodInfo[] methods = typeof(PhotonNetwork).GetMethods(BindingFlags.Static | BindingFlags.Public); foreach (MethodInfo methodInfo in methods) { if (methodInfo.Name == "Instantiate" || methodInfo.Name == "InstantiateRoomObject") { try { harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(NetworkAbuseDetector), "OnInstantiatePrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } catch (Exception ex) { AbuseLogger.Info("[ABUSE] Failed to patch " + methodInfo.Name + ": " + ex.Message); } } } methods = typeof(PhotonNetwork).GetMethods(BindingFlags.Static | BindingFlags.Public); foreach (MethodInfo methodInfo2 in methods) { if (methodInfo2.Name == "Destroy") { try { harmony.Patch((MethodBase)methodInfo2, new HarmonyMethod(typeof(NetworkAbuseDetector), "OnDestroyPrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } catch (Exception ex2) { AbuseLogger.Info("[ABUSE] Failed to patch Destroy: " + ex2.Message); } } } methods = typeof(PhotonView).GetMethods(BindingFlags.Instance | BindingFlags.Public); foreach (MethodInfo methodInfo3 in methods) { if (methodInfo3.Name == "RPC") { try { harmony.Patch((MethodBase)methodInfo3, new HarmonyMethod(typeof(NetworkAbuseDetector), "OnRpcPrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } catch (Exception ex3) { AbuseLogger.Info("[ABUSE] Failed to patch RPC: " + ex3.Message); } } } AbuseLogger.Info("[ABUSE] Photon method hooks registered (local API tracking)"); } private static void FindGameTypes() { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { if (_zombieType != null) { break; } try { Type[] types = assembly.GetTypes(); foreach (Type type in types) { if (type.Name == "MushroomZombie") { _zombieType = type; AbuseLogger.Info("[ABUSE] Found zombie type: " + type.FullName); break; } } } catch (Exception ex) { ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogWarning((object)("[WHY_LAG] FindGameTypes scan failed in asm=" + assembly.GetName().Name + ": " + ex.Message)); } } } if (_zombieType == null) { AbuseLogger.Info("[ABUSE] MushroomZombie type not found (zombie counting disabled)"); } } private static void OnInstantiatePrefix(string prefabName, Vector3 position) { //IL_0147: Unknown result type (might be due to invalid IL or missing references) //IL_0152: Unknown result type (might be due to invalid IL or missing references) //IL_015d: Unknown result type (might be due to invalid IL or missing references) bool flag = false; lock (_lock) { _localInstantiateCount++; _totalInstantiates++; _ = _totalInstantiates; if (!string.IsNullOrEmpty(prefabName)) { if (!_prefabCount.ContainsKey(prefabName)) { _prefabCount[prefabName] = 0; } _prefabCount[prefabName]++; _prefabTraceTaken.TryGetValue(prefabName, out var value); if (value < 3) { flag = true; _prefabTraceTaken[prefabName] = value + 1; } else if (_forceTraceNext) { flag = true; _forceTraceNext = false; } else if (Time.realtimeSinceStartup - _lastTraceSampleTime >= 5f) { flag = true; _lastTraceSampleTime = Time.realtimeSinceStartup; } } } int num; bool flag2; try { num = ((PhotonNetwork.LocalPlayer != null) ? PhotonNetwork.LocalPlayer.ActorNumber : (-1)); flag2 = PhotonNetwork.IsMasterClient; } catch { num = -1; flag2 = false; } string traceOut = null; string callerOut = null; if (flag) { ExtractCallerStack(out traceOut, out callerOut); } try { Dictionary dictionary = new Dictionary { { "PrefabName", prefabName ?? "" }, { "IsMasterClient", flag2 ? 1 : 0 }, { "Position", $"{position.x:F1},{position.y:F1},{position.z:F1}" }, { "LocalActor", num } }; if (!string.IsNullOrEmpty(traceOut)) { dictionary["TraceStack"] = traceOut; } if (!string.IsNullOrEmpty(callerOut)) { dictionary["TraceCaller"] = callerOut; } if (flag2 && TryFindRecentClientRpc(Time.realtimeSinceStartup, out var actor, out var method, out var ageMs)) { dictionary["SuspectedRequesterActor"] = actor; dictionary["SuspectedRequesterRpc"] = method ?? ""; dictionary["SuspectedAgeMs"] = ageMs; try { if (PhotonNetwork.CurrentRoom != null && PhotonNetwork.CurrentRoom.Players != null && PhotonNetwork.CurrentRoom.Players.TryGetValue(actor, out var value2)) { dictionary["SuspectedRequesterName"] = ((value2 != null) ? value2.NickName : null) ?? ""; } } catch { } } StructuredEvent evt = default(StructuredEvent); evt.Timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); evt.FrameNumber = Time.frameCount; evt.Type = EventType.InstantiateTrace; evt.Fields = dictionary; StructuredLogger.WriteEvent(evt); } catch (Exception ex) { ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogWarning((object)("[WHY_LAG] InstantiateTrace emit failed: " + ex.Message)); } } } private static void ExtractCallerStack(out string traceOut, out string callerOut) { traceOut = null; callerOut = null; try { StackTrace stackTrace = new StackTrace(2, fNeedFileInfo: false); int frameCount = stackTrace.FrameCount; StringBuilder stringBuilder = new StringBuilder(); int num = 0; for (int i = 0; i < frameCount; i++) { if (num >= 12) { break; } MethodBase method = stackTrace.GetFrame(i).GetMethod(); if (method == null) { continue; } Type declaringType = method.DeclaringType; string text = ((declaringType != null) ? declaringType.FullName : ""); if (stringBuilder.Length > 0) { stringBuilder.Append(" | "); } stringBuilder.Append(text).Append('.').Append(method.Name); num++; if (callerOut == null && declaringType != null) { string text2 = declaringType.Namespace ?? string.Empty; if (!text2.StartsWith("Photon") && !text2.StartsWith("HarmonyLib") && !text2.StartsWith("UnityEngine") && !text2.StartsWith("System") && text2 != "WhySoLaggy") { callerOut = text + "." + method.Name; } } } traceOut = stringBuilder.ToString(); } catch { } } public static void ForceTraceNextInstantiate() { _forceTraceNext = true; } private static void OnDestroyPrefix() { lock (_lock) { _localDestroyCount++; _totalDestroys++; } } private static void OnRpcPrefix(PhotonView __instance, string methodName) { lock (_lock) { _localRpcCount++; _totalRpcs++; } } private static void CheckRates() { float checkIntervalSeconds = CheckIntervalSeconds; int num; int num2; int num3; Dictionary dictionary; Dictionary dictionary2; Dictionary dictionary3; lock (_lock) { num = _localInstantiateCount + _remoteInstantiateCount; num2 = _localDestroyCount + _remoteDestroyCount; num3 = _localRpcCount + _remoteRpcCount; dictionary = new Dictionary(_instantiateByActor); dictionary2 = new Dictionary(_rpcByActor); dictionary3 = new Dictionary(_destroyByActor); } float num4 = (float)num / checkIntervalSeconds; float num5 = (float)num2 / checkIntervalSeconds; float num6 = (float)num3 / checkIntervalSeconds; if (num4 >= (float)InstantiateRateThreshold) { _alertCount++; AbuseLogger.Alert($"Instantiate flood! Rate: {num4:F1}/s (threshold: {InstantiateRateThreshold}/s)"); AbuseNotificationUI.Show(IsChinese ? $"⚠ 刷物体洪水!速率: {num4:F1}/秒(阈值: {InstantiateRateThreshold}/秒)" : $"⚠ Instantiate flood! Rate: {num4:F1}/s (threshold: {InstantiateRateThreshold}/s)"); LogTopActors(dictionary, "Instantiate"); LogTopPrefabs(); EmitAbuseAlertEvent("InstantiateFlood", num4, InstantiateRateThreshold, dictionary); ForceTraceNextInstantiate(); } if (num5 >= (float)DestroyRateThreshold) { _alertCount++; AbuseLogger.Alert($"Destroy flood! Rate: {num5:F1}/s (threshold: {DestroyRateThreshold}/s)"); AbuseNotificationUI.Show(IsChinese ? $"⚠ 大量销毁!速率: {num5:F1}/秒(阈值: {DestroyRateThreshold}/秒)" : $"⚠ Destroy flood! Rate: {num5:F1}/s (threshold: {DestroyRateThreshold}/s)"); LogTopActors(dictionary3, "Destroy"); EmitAbuseAlertEvent("DestroyFlood", num5, DestroyRateThreshold, dictionary3); } if (num6 >= (float)RpcRateThreshold) { _alertCount++; AbuseLogger.Alert($"RPC flood! Rate: {num6:F1}/s (threshold: {RpcRateThreshold}/s)"); AbuseNotificationUI.Show(IsChinese ? $"⚠ RPC洪水!速率: {num6:F1}/秒(阈值: {RpcRateThreshold}/秒)" : $"⚠ RPC flood! Rate: {num6:F1}/s (threshold: {RpcRateThreshold}/s)"); LogTopActors(dictionary2, "RPC"); RpcMonitor.LogCurrentWindowTopMethods(5); EmitAbuseAlertEvent("RpcFlood", num6, RpcRateThreshold, dictionary2); } } private static void EmitAbuseAlertEvent(string alertType, float rate, int threshold, Dictionary actorMap) { try { int num = -1; int num2 = 0; if (actorMap != null) { foreach (KeyValuePair item in actorMap) { if (item.Value > num2) { num = item.Key; num2 = item.Value; } } } Dictionary dictionary = new Dictionary { { "AlertType", alertType }, { "Rate", Math.Round(rate, 2) }, { "Threshold", threshold } }; if (num >= 0) { dictionary["TopActor"] = num; dictionary["TopActorName"] = ResolvePlayerName(num); } try { dictionary["Ping"] = PhotonNetwork.GetPing(); } catch { } StructuredEvent evt = default(StructuredEvent); evt.Timestamp = StructuredLogger.NowStamp(); evt.FrameNumber = Time.frameCount; evt.Type = EventType.AbuseAlert; evt.Fields = dictionary; StructuredLogger.WriteEvent(evt); } catch (Exception ex) { ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogWarning((object)("[WHY_LAG] NetworkAbuseDetector EmitAbuseAlert failed: " + ex.Message)); } } } private static void CheckActorMethodHotspots() { List<(int, string, int)> list = null; lock (_lock) { foreach (KeyValuePair> item in _rpcByActorMethod) { foreach (KeyValuePair item2 in item.Value) { if (item2.Value >= ActorMethodRateThreshold) { if (list == null) { list = new List<(int, string, int)>(4); } list.Add((item.Key, item2.Key, item2.Value)); } } } } if (list == null) { return; } foreach (var (num, text, num2) in list) { _alertCount++; string text2 = TryGetNickName(num); AbuseLogger.Alert($"Actor×Method hotspot! Actor #{num} ({text2}) sent '{text}' x{num2} in {CheckIntervalSeconds:F1}s (threshold: {ActorMethodRateThreshold})"); AbuseNotificationUI.Show(IsChinese ? $"⚠ 客户端 RPC 热点!#{num} {text2} {text} ×{num2}" : $"⚠ Actor×Method hotspot! #{num} {text2} {text} ×{num2}"); try { StructuredEvent evt = default(StructuredEvent); evt.Timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); evt.FrameNumber = Time.frameCount; evt.Type = EventType.AbuseAlert; evt.Fields = new Dictionary { { "AlertType", "ActorMethodHotspot" }, { "TopActor", num }, { "TopActorName", text2 }, { "RpcMethod", text }, { "CurrentCount", num2 }, { "Threshold", ActorMethodRateThreshold } }; StructuredLogger.WriteEvent(evt); } catch { } } } private static void CheckOwnershipGrab() { List<(int, int)> list = null; lock (_lock) { foreach (KeyValuePair item in _ownershipGrabbedByActor) { if (item.Value >= OwnershipGrabRateThreshold) { if (list == null) { list = new List<(int, int)>(2); } list.Add((item.Key, item.Value)); } } } if (list == null) { return; } foreach (var (num, num2) in list) { _alertCount++; string text = TryGetNickName(num); AbuseLogger.Alert($"Ownership grab! Actor #{num} ({text}) took {num2} PhotonView ownerships in {CheckIntervalSeconds:F1}s (threshold: {OwnershipGrabRateThreshold})"); AbuseNotificationUI.Show(IsChinese ? $"⚠ 所有权抢夺!#{num} {text} ■{num2}" : $"⚠ Ownership grab! #{num} {text} ×{num2}"); try { StructuredEvent evt = default(StructuredEvent); evt.Timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); evt.FrameNumber = Time.frameCount; evt.Type = EventType.AbuseAlert; evt.Fields = new Dictionary { { "AlertType", "OwnershipGrab" }, { "TopActor", num }, { "TopActorName", text }, { "CurrentCount", num2 }, { "Threshold", OwnershipGrabRateThreshold } }; StructuredLogger.WriteEvent(evt); } catch { } } } private static string TryGetNickName(int actor) { try { if (PhotonNetwork.CurrentRoom != null && PhotonNetwork.CurrentRoom.Players != null && PhotonNetwork.CurrentRoom.Players.TryGetValue(actor, out var value)) { return ((value != null) ? value.NickName : null) ?? "?"; } } catch { } return "?"; } private static void CheckObjectSpike() { int num = CountPhotonViews(); int num2 = CountZombies(); int num3 = num - _lastPhotonViewCount; int num4 = num2 - _lastZombieCount; if (num3 >= ObjectSpikeThreshold) { _alertCount++; AbuseLogger.Alert($"PhotonView spike! +{num3} in {CheckIntervalSeconds}s (now: {num})"); AbuseNotificationUI.Show(IsChinese ? $"⚠ 对象突增!+{num3} 个/{CheckIntervalSeconds}秒(当前: {num})" : $"⚠ PhotonView spike! +{num3} in {CheckIntervalSeconds}s (now: {num})"); string topOwners = InvestigatePhotonViewOwners(); EmitSpikeEvent("PhotonViewSpike", num3, num, topOwners); } if (num4 >= ObjectSpikeThreshold) { _alertCount++; AbuseLogger.Alert($"Zombie spike! +{num4} in {CheckIntervalSeconds}s (now: {num2})"); AbuseNotificationUI.Show(IsChinese ? $"⚠ 僵尸突增!+{num4} 个/{CheckIntervalSeconds}秒(当前: {num2})" : $"⚠ Zombie spike! +{num4} in {CheckIntervalSeconds}s (now: {num2})"); EmitSpikeEvent("ZombieSpike", num4, num2); } _lastPhotonViewCount = num; _lastZombieCount = num2; } private static int CountPhotonViews() { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) try { int num = 0; ValueIterator enumerator = PhotonNetwork.PhotonViewCollection.GetEnumerator(); try { while (enumerator.MoveNext()) { _ = enumerator.Current; num++; } } finally { ((IDisposable)enumerator).Dispose(); } return num; } catch { return 0; } } private static int CountZombies() { if (_zombieType == null) { return 0; } try { Object[] array = Object.FindObjectsByType(_zombieType, (FindObjectsSortMode)0); return (array != null) ? array.Length : 0; } catch { return 0; } } private static string InvestigatePhotonViewOwners() { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) try { Dictionary dictionary = new Dictionary(); ValueIterator enumerator = PhotonNetwork.PhotonViewCollection.GetEnumerator(); try { while (enumerator.MoveNext()) { PhotonView current = enumerator.Current; string key = "Scene"; if ((Object)(object)current != (Object)null && current.Owner != null) { key = string.Format("{0}#{1}", current.Owner.NickName ?? "?", current.Owner.ActorNumber); } if (!dictionary.ContainsKey(key)) { dictionary[key] = 0; } dictionary[key]++; } } finally { ((IDisposable)enumerator).Dispose(); } AbuseLogger.Write("[ABUSE] PhotonView owner distribution:"); List> list = new List>(dictionary); list.Sort((KeyValuePair a, KeyValuePair b) => b.Value.CompareTo(a.Value)); foreach (KeyValuePair item in list) { AbuseLogger.WriteRaw($" {item.Key}: {item.Value} objects"); } if (list.Count > 0 && list[0].Value > ObjectSpikeThreshold) { AbuseNotificationUI.Show(IsChinese ? $" → 最大持有: {list[0].Key}({list[0].Value} 个对象)" : $" → Top owner: {list[0].Key} ({list[0].Value} objs)"); } if (list.Count == 0) { return null; } StringBuilder stringBuilder = new StringBuilder(); int num = ((list.Count < 5) ? list.Count : 5); for (int i = 0; i < num; i++) { if (i > 0) { stringBuilder.Append(';'); } stringBuilder.Append(list[i].Key).Append(':').Append(list[i].Value); } return stringBuilder.ToString(); } catch (Exception ex) { AbuseLogger.Write("[ABUSE] Failed to investigate owners: " + ex.Message); return null; } } private static void WritePeriodicReport() { AbuseLogger.Write(new string('─', 50)); AbuseLogger.Write("[ABUSE] ═══ Periodic Abuse Report ═══"); if (PhotonNetwork.InRoom) { Room currentRoom = PhotonNetwork.CurrentRoom; AbuseLogger.Write(string.Format("[ABUSE] Room: {0}, Players: {1}/{2}", ((currentRoom != null) ? currentRoom.Name : null) ?? "?", (currentRoom != null) ? currentRoom.PlayerCount : 0, (currentRoom != null) ? currentRoom.MaxPlayers : 0)); } else { AbuseLogger.Write("[ABUSE] Not in room"); } AbuseLogger.Write($"[ABUSE] Current objects: PhotonViews={_lastPhotonViewCount}, Zombies={_lastZombieCount}"); int num = -1; try { num = PhotonNetwork.GetPing(); } catch { } if (num >= 0) { AbuseLogger.Write($"[ABUSE] Photon Ping: {num} ms"); } float reportIntervalSeconds = ReportIntervalSeconds; AbuseLogger.Write($"[ABUSE] Period totals ({reportIntervalSeconds:F0}s): Instantiates={_totalInstantiates}, Destroys={_totalDestroys}, RPCs={_totalRpcs}"); AbuseLogger.Write($"[ABUSE] Alerts triggered: {_alertCount}"); try { string text = null; int num2 = 0; int num3 = 0; if (PhotonNetwork.InRoom) { Room currentRoom2 = PhotonNetwork.CurrentRoom; text = ((currentRoom2 != null) ? currentRoom2.Name : null); num2 = ((currentRoom2 != null) ? currentRoom2.PlayerCount : 0); num3 = ((currentRoom2 != null) ? currentRoom2.MaxPlayers : 0); } StructuredEvent evt = default(StructuredEvent); evt.Timestamp = StructuredLogger.NowStamp(); evt.FrameNumber = Time.frameCount; evt.Type = EventType.PeriodicReport; evt.Fields = new Dictionary { { "AlertType", "PeriodicReport" }, { "CurrentCount", _lastPhotonViewCount }, { "Delta", _totalInstantiates - _totalDestroys }, { "TotalInstantiates", _totalInstantiates }, { "TotalDestroys", _totalDestroys }, { "TotalRpcs", _totalRpcs }, { "AlertCount", _alertCount }, { "ZombieCount", _lastZombieCount }, { "RoomName", text ?? "" }, { "PlayerCount", num2 }, { "MaxPlayers", num3 }, { "Ping", num } }; StructuredLogger.WriteEvent(evt); } catch (Exception ex) { ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogWarning((object)("[WHY_LAG] NetworkAbuseDetector PeriodicReport event failed: " + ex.Message)); } } lock (_lock) { if (_prefabCount.Count > 0) { AbuseLogger.Write("[ABUSE] Top spawned prefabs:"); List> list = new List>(_prefabCount); list.Sort((KeyValuePair a, KeyValuePair b) => b.Value.CompareTo(a.Value)); int num4 = 0; foreach (KeyValuePair item4 in list) { AbuseLogger.WriteRaw($" {item4.Key}: {item4.Value}x"); if (++num4 >= 5) { break; } } } } if (PhotonNetwork.InRoom) { AbuseLogger.Write("[ABUSE] Players in room:"); Player[] playerList = PhotonNetwork.PlayerList; foreach (Player val in playerList) { string arg = (val.IsMasterClient ? " [Master]" : ""); AbuseLogger.WriteRaw($" #{val.ActorNumber} {val.NickName}{arg}"); } } lock (_lock) { if (_rpcByActorMethodTotal.Count > 0) { AbuseLogger.Write("[ABUSE] Top client RPCs (Actor×Method):"); List<(int, string, int)> list2 = new List<(int, string, int)>(); foreach (KeyValuePair> item5 in _rpcByActorMethodTotal) { foreach (KeyValuePair item6 in item5.Value) { list2.Add((item5.Key, item6.Key, item6.Value)); } } list2.Sort(((int actor, string method, int count) a, (int actor, string method, int count) b) => b.count.CompareTo(a.count)); int num5 = 0; foreach (var item7 in list2) { int item = item7.Item1; string item2 = item7.Item2; int item3 = item7.Item3; string text2 = TryGetNickName(item); AbuseLogger.WriteRaw($" #{item} {text2}: {item2} ×{item3}"); if (++num5 >= 10) { break; } } } } AbuseLogger.Write(new string('─', 50)); RpcMonitor.WritePeriodicReport(); } private static void EmitSpikeEvent(string alertType, int delta, int currentCount, string topOwners = null) { try { Dictionary dictionary = new Dictionary { { "AlertType", alertType }, { "Delta", delta }, { "CurrentCount", currentCount }, { "Threshold", ObjectSpikeThreshold } }; if (!string.IsNullOrEmpty(topOwners)) { dictionary["TopOwners"] = topOwners; } try { dictionary["Ping"] = PhotonNetwork.GetPing(); } catch { } StructuredEvent evt = default(StructuredEvent); evt.Timestamp = StructuredLogger.NowStamp(); evt.FrameNumber = Time.frameCount; evt.Type = EventType.AbuseAlert; evt.Fields = dictionary; StructuredLogger.WriteEvent(evt); } catch (Exception ex) { ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogWarning((object)("[WHY_LAG] NetworkAbuseDetector EmitSpike failed: " + ex.Message)); } } } private static void ResetCounters() { lock (_lock) { _localInstantiateCount = 0; _localDestroyCount = 0; _localRpcCount = 0; _remoteInstantiateCount = 0; _remoteDestroyCount = 0; _remoteRpcCount = 0; _instantiateByActor.Clear(); _rpcByActor.Clear(); _destroyByActor.Clear(); _rpcByActorMethod.Clear(); _ownershipGrabbedByActor.Clear(); } } private static void ResetReportStats() { lock (_lock) { _totalInstantiates = 0; _totalDestroys = 0; _totalRpcs = 0; _alertCount = 0; _prefabCount.Clear(); _rpcByActorMethodTotal.Clear(); } } private static void IncrementActor(Dictionary dict, int actorNumber) { if (!dict.ContainsKey(actorNumber)) { dict[actorNumber] = 0; } dict[actorNumber]++; } private static void LogTopActors(Dictionary dict, string action) { if (dict.Count == 0) { return; } List> list = new List>(dict); list.Sort((KeyValuePair a, KeyValuePair b) => b.Value.CompareTo(a.Value)); AbuseLogger.AlertDetail("[ABUSE] Top " + action + " sources (by ActorNumber):"); int num = 0; foreach (KeyValuePair item in list) { string arg = ResolvePlayerName(item.Key); AbuseLogger.AlertDetailRaw($" {arg}#{item.Key}: {item.Value}x"); if (++num >= 5) { break; } } if (list.Count > 0) { string text = ResolvePlayerName(list[0].Key); AbuseNotificationUI.Show(IsChinese ? $" → 嫌疑人: {text}#{list[0].Key}({list[0].Value}次 {action})" : $" → Suspect: {text}#{list[0].Key} ({list[0].Value}x {action})"); } } private static void LogTopPrefabs() { lock (_lock) { if (_prefabCount.Count == 0) { return; } List> list = new List>(_prefabCount); list.Sort((KeyValuePair a, KeyValuePair b) => b.Value.CompareTo(a.Value)); AbuseLogger.AlertDetail("[ABUSE] Top prefabs this interval:"); int num = 0; foreach (KeyValuePair item in list) { AbuseLogger.AlertDetailRaw($" {item.Key}: {item.Value}x"); if (++num >= 5) { break; } } } } private static string ResolvePlayerName(int actorNumber) { try { if (!PhotonNetwork.InRoom) { return "?"; } Player[] playerList = PhotonNetwork.PlayerList; foreach (Player val in playerList) { if (val.ActorNumber == actorNumber) { return val.NickName ?? "?"; } } } catch { } return "?"; } } internal static class PatchProfiler { private class MethodTimingData { public long TotalTicks; public int CallCount; } private class FrameMethodData { public long Ticks; public int Calls; } public static bool Enabled = true; public static int TopMethodCount = 10; public static float MinReportMs = 0.1f; public static readonly HashSet IgnoreMethods = new HashSet(StringComparer.Ordinal); public static string OwnHarmonyId; private static readonly Dictionary _timings = new Dictionary(); private static readonly Dictionary _ownerMap = new Dictionary(); private static readonly Dictionary _frameTimers = new Dictionary(); private static readonly Dictionary _skipCounter = new Dictionary(); private static bool _initialized; private static int _patchedCount; private static readonly StringBuilder _reportSb = new StringBuilder(1024); private static readonly List> _reportSorted = new List>(64); private static readonly List> _spikeSorted = new List>(32); public static void Initialize(Harmony harmony) { //IL_0150: Unknown result type (might be due to invalid IL or missing references) //IL_0155: Unknown result type (might be due to invalid IL or missing references) //IL_0170: Unknown result type (might be due to invalid IL or missing references) //IL_0175: Unknown result type (might be due to invalid IL or missing references) //IL_0184: Expected O, but got Unknown //IL_0184: Expected O, but got Unknown if (_initialized || !Enabled) { return; } _initialized = true; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("[WHY_LAG] -- PatchProfiler: scanning patched methods --"); List list = Harmony.GetAllPatchedMethods().ToList(); stringBuilder.AppendLine($"[WHY_LAG] Found {list.Count} patched methods in total"); int num = 0; int num2 = 0; int num3 = 0; foreach (MethodBase item in list) { if (item == null) { continue; } string methodKey = GetMethodKey(item); if (IgnoreMethods.Count > 0 && IgnoreMethods.Contains(methodKey)) { num3++; continue; } try { Patches patchInfo = Harmony.GetPatchInfo(item); if (patchInfo == null) { continue; } List list2 = patchInfo.Owners?.ToList() ?? new List(); if (list2.Count != 0) { List list3 = list2.Where((string o) => o != OwnHarmonyId).ToList(); if (list3.Count == 0) { num++; continue; } _ownerMap[methodKey] = string.Join(", ", list3); harmony.Patch(item, new HarmonyMethod(typeof(PatchProfiler), "TimingPrefix", (Type[])null) { priority = 800 }, new HarmonyMethod(typeof(PatchProfiler), "TimingPostfix", (Type[])null) { priority = 0 }, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _patchedCount++; } } catch (Exception ex) { num2++; string text = item.DeclaringType?.Name + "." + item.Name; ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogWarning((object)("[WHY_LAG] PatchProfiler hook FAILED " + text + ": " + ex.Message)); } stringBuilder.AppendLine("[WHY_LAG] FAILED to hook " + text + ": " + ex.Message); } } stringBuilder.AppendLine($"[WHY_LAG] Hooked: {_patchedCount} | Skipped(self-only): {num} | IgnoredByConfig: {num3} | Failed: {num2}"); stringBuilder.Append($"[WHY_LAG] PatchProfiler initialized (MinReportMs={MinReportMs:F2})"); LagLogger.Info(stringBuilder.ToString()); } public static void TimingPrefix(MethodBase __originalMethod, out long __state) { __state = Stopwatch.GetTimestamp(); } public static void TimingPostfix(MethodBase __originalMethod, long __state) { long elapsedTicks = Stopwatch.GetTimestamp() - __state; Record((__originalMethod != null) ? GetMethodKey(__originalMethod) : "(unknown)", elapsedTicks); } private static void Record(string methodKey, long elapsedTicks) { if (!_timings.TryGetValue(methodKey, out var value)) { value = new MethodTimingData(); _timings[methodKey] = value; } if (MinReportMs > 0f && value.CallCount > 20) { double num = (double)Stopwatch.Frequency / 1000.0; if ((double)value.TotalTicks / (double)value.CallCount / num < (double)MinReportMs) { _skipCounter.TryGetValue(methodKey, out var value2); value2++; if (value2 % 10 != 0) { _skipCounter[methodKey] = value2; return; } _skipCounter[methodKey] = value2; } } value.TotalTicks += elapsedTicks; value.CallCount++; if (!_frameTimers.TryGetValue(methodKey, out var value3)) { value3 = new FrameMethodData(); _frameTimers[methodKey] = value3; } value3.Ticks += elapsedTicks; value3.Calls++; } public static void WriteReport(int totalFrames) { if (!_initialized || _timings.Count == 0) { return; } double num = (double)Stopwatch.Frequency / 1000.0; _reportSorted.Clear(); foreach (KeyValuePair timing in _timings) { if (timing.Value.CallCount > 0) { _reportSorted.Add(timing); } } _reportSorted.Sort((KeyValuePair a, KeyValuePair b) => b.Value.TotalTicks.CompareTo(a.Value.TotalTicks)); _reportSb.Clear(); _reportSb.AppendLine("[WHY_LAG] -- Top Slow Patched Methods (per-frame avg) --"); int num2 = 0; foreach (KeyValuePair item in _reportSorted) { if (num2 >= TopMethodCount) { break; } double num3 = (double)item.Value.TotalTicks / num; double num4 = ((totalFrames > 0) ? (num3 / (double)totalFrames) : num3); int num5 = ((totalFrames > 0) ? (item.Value.CallCount / totalFrames) : item.Value.CallCount); string value; string text = (_ownerMap.TryGetValue(item.Key, out value) ? value : "?"); string text2 = ((num4 > 2.0) ? " !!!" : ((num4 > 0.5) ? " !" : "")); _reportSb.AppendLine($"[WHY_LAG] {item.Key}: {num4:F2}ms x{num5} [{text}]{text2}"); try { StructuredEvent evt = default(StructuredEvent); evt.Timestamp = StructuredLogger.NowStamp(); evt.FrameNumber = Time.frameCount; evt.Type = EventType.PatchTiming; evt.Fields = new Dictionary { { "Name", item.Key }, { "AvgMs", Math.Round(num4, 3) }, { "TotalMs", Math.Round(num3, 3) }, { "CallCount", item.Value.CallCount }, { "Owner", text } }; StructuredLogger.WriteEvent(evt); } catch (Exception ex) { ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogWarning((object)("[WHY_LAG] PatchProfiler WriteEvent failed: " + ex.Message)); } } num2++; } LagLogger.Write(_reportSb.ToString().TrimEnd(Array.Empty())); foreach (MethodTimingData value2 in _timings.Values) { value2.TotalTicks = 0L; value2.CallCount = 0; } _skipCounter.Clear(); } public static void WriteSpikeDetail() { if (!_initialized || _frameTimers.Count == 0) { return; } double num = (double)Stopwatch.Frequency / 1000.0; _spikeSorted.Clear(); foreach (KeyValuePair frameTimer in _frameTimers) { if (frameTimer.Value.Ticks > 0) { _spikeSorted.Add(frameTimer); } } _spikeSorted.Sort((KeyValuePair a, KeyValuePair b) => b.Value.Ticks.CompareTo(a.Value.Ticks)); _reportSb.Clear(); _reportSb.Append("[WHY_LAG] Methods: "); int num2 = 0; bool flag = false; foreach (KeyValuePair item in _spikeSorted) { if (num2 >= 5) { break; } double num3 = (double)item.Value.Ticks / num; if (!(num3 < 0.5)) { flag = true; string value; string text = (_ownerMap.TryGetValue(item.Key, out value) ? value : "?"); _reportSb.Append($"{item.Key}={num3:F1}ms x{item.Value.Calls} [{text}] | "); num2++; } } if (flag) { LagLogger.Write(_reportSb.ToString()); } } public static void ResetFrameTimers() { foreach (FrameMethodData value in _frameTimers.Values) { value.Ticks = 0L; value.Calls = 0; } } private static string GetMethodKey(MethodBase method) { if (method == null) { return "(null)"; } return (method.DeclaringType?.Name ?? "?") + "." + method.Name; } } internal static class PerformanceDashboard { [CompilerGenerated] private static class <>O { public static WindowFunction <0>__DrawWindow; } public static bool ShowDashboard = false; private static Rect _windowRect = new Rect(16f, 16f, 380f, 220f); private const int WindowId = 109037; private static string _lastAlert = ""; private static float _lastAlertTime = -1f; public static void ReportAlert(string msg) { _lastAlert = msg ?? ""; _lastAlertTime = Time.realtimeSinceStartup; } public static void DrawGUI() { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Expected O, but got Unknown if (!ShowDashboard) { return; } try { Rect windowRect = _windowRect; object obj = <>O.<0>__DrawWindow; if (obj == null) { WindowFunction val = DrawWindow; <>O.<0>__DrawWindow = val; obj = (object)val; } _windowRect = GUILayout.Window(109037, windowRect, (WindowFunction)obj, "WhySoLaggy", Array.Empty()); } catch (Exception ex) { ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogWarning((object)("[WHY_LAG] Dashboard draw failed: " + ex.Message)); } } } private static void DrawWindow(int id) { float currentFrameMs = FpsTracker.CurrentFrameMs; float num = ((currentFrameMs > 0.01f) ? (1000f / currentFrameMs) : 0f); float windowAvgMs = FpsTracker.GetWindowAvgMs(); float allocRateKBps = FpsTracker.GetAllocRateKBps(); GUILayout.BeginVertical(Array.Empty()); GUILayout.Label($"FPS: {num:F1} | Frame: {currentFrameMs:F1}ms | AvgMs(win): {windowAvgMs:F1}", Array.Empty()); if (FpsTracker.EnableMemoryMonitor) { GUILayout.Label($"AllocRate: {allocRateKBps:F1} KB/s", Array.Empty()); } GUILayout.Space(4f); GUILayout.Label("Watched RPC (current period):", Array.Empty()); GUILayout.Label(" " + RpcMonitor.GetRecentWatchedSummary(8), Array.Empty()); GUILayout.Space(4f); if (!string.IsNullOrEmpty(_lastAlert)) { float num2 = ((_lastAlertTime < 0f) ? 0f : (Time.realtimeSinceStartup - _lastAlertTime)); GUILayout.Label($"Last alert ({num2:F0}s ago):", Array.Empty()); GUILayout.Label(" " + _lastAlert, Array.Empty()); } else { GUILayout.Label("No alerts yet", Array.Empty()); } GUILayout.EndVertical(); GUI.DragWindow(); } } internal static class PluginProfiler { private class PluginTimingData { public long TotalTicks; public int CallCount; } public static bool Enabled = true; public static readonly HashSet IgnoreGuids = new HashSet(StringComparer.Ordinal); private static readonly Dictionary _timings = new Dictionary(); private static readonly Dictionary _displayNames = new Dictionary(); private static readonly Dictionary _frameTimers = new Dictionary(); private static bool _initialized; private static int _patchedCount; private static readonly StringBuilder _reportSb = new StringBuilder(1024); private static readonly List> _reportSorted = new List>(32); private static readonly List> _spikeSorted = new List>(32); public static void Initialize(Harmony harmony) { //IL_0156: Unknown result type (might be due to invalid IL or missing references) //IL_015b: Unknown result type (might be due to invalid IL or missing references) //IL_0176: Unknown result type (might be due to invalid IL or missing references) //IL_017b: Unknown result type (might be due to invalid IL or missing references) //IL_018a: Expected O, but got Unknown //IL_018a: Expected O, but got Unknown if (_initialized || !Enabled) { return; } _initialized = true; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("[WHY_LAG] -- PluginProfiler: scanning plugins --"); foreach (KeyValuePair pluginInfo in Chainloader.PluginInfos) { PluginInfo value = pluginInfo.Value; if ((Object)(object)value.Instance == (Object)null) { continue; } if (IgnoreGuids.Count > 0 && IgnoreGuids.Contains(pluginInfo.Key)) { stringBuilder.AppendLine("[WHY_LAG] Skipped (by IgnorePluginGuids): " + pluginInfo.Key); continue; } Type type = ((object)value.Instance).GetType(); string key = type.FullName ?? type.Name; BepInPlugin metadata = value.Metadata; string text = ((metadata != null) ? metadata.Name : null) ?? pluginInfo.Key; if (type == typeof(WhySoLaggyPlugin)) { continue; } _displayNames[key] = text; string[] array = new string[3] { "Update", "LateUpdate", "FixedUpdate" }; foreach (string text2 in array) { MethodInfo method = type.GetMethod(text2, BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (method == null) { continue; } try { harmony.Patch((MethodBase)method, new HarmonyMethod(typeof(PluginProfiler), "TimingPrefix", (Type[])null) { priority = 800 }, new HarmonyMethod(typeof(PluginProfiler), "TimingPostfix", (Type[])null) { priority = 0 }, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _patchedCount++; stringBuilder.AppendLine("[WHY_LAG] Hooked " + text + "." + text2); } catch (Exception ex) { ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogWarning((object)("[WHY_LAG] PluginProfiler hook FAILED " + text + "." + text2 + ": " + ex.Message)); } stringBuilder.AppendLine("[WHY_LAG] FAILED " + text + "." + text2 + ": " + ex.Message); } } } stringBuilder.Append($"[WHY_LAG] PluginProfiler: {_patchedCount} methods hooked across {_displayNames.Count} plugins"); LagLogger.Info(stringBuilder.ToString()); } public static void TimingPrefix(MonoBehaviour __instance, out long __state) { __state = Stopwatch.GetTimestamp(); } public static void TimingPostfix(MonoBehaviour __instance, long __state) { long elapsedTicks = Stopwatch.GetTimestamp() - __state; if (!((Object)(object)__instance == (Object)null)) { Record(((object)__instance).GetType().FullName ?? ((object)__instance).GetType().Name, elapsedTicks); } } private static void Record(string typeName, long elapsedTicks) { if (!_timings.TryGetValue(typeName, out var value)) { value = new PluginTimingData(); _timings[typeName] = value; } value.TotalTicks += elapsedTicks; value.CallCount++; if (!_frameTimers.ContainsKey(typeName)) { _frameTimers[typeName] = 0L; } _frameTimers[typeName] += elapsedTicks; } public static void WriteReport(int totalFrames) { if (!_initialized || _timings.Count == 0) { return; } double num = (double)Stopwatch.Frequency / 1000.0; _reportSorted.Clear(); foreach (KeyValuePair timing in _timings) { _reportSorted.Add(timing); } _reportSorted.Sort((KeyValuePair a, KeyValuePair b) => b.Value.TotalTicks.CompareTo(a.Value.TotalTicks)); _reportSb.Clear(); _reportSb.AppendLine("[WHY_LAG] -- Plugin Update Time (per-frame avg) --"); double num2 = 0.0; foreach (KeyValuePair item in _reportSorted) { string value; string text = (_displayNames.TryGetValue(item.Key, out value) ? value : item.Key); double num3 = (double)item.Value.TotalTicks / num; double num4 = ((totalFrames > 0) ? (num3 / (double)totalFrames) : num3); num2 += num3; string text2 = ((num4 > 1.0) ? " !!!" : ((num4 > 0.5) ? " !" : "")); _reportSb.AppendLine($"[WHY_LAG] {text}: {num4:F2}ms/frame (total={num3:F1}ms, calls={item.Value.CallCount}){text2}"); try { StructuredEvent evt = default(StructuredEvent); evt.Timestamp = StructuredLogger.NowStamp(); evt.FrameNumber = Time.frameCount; evt.Type = EventType.PluginTiming; evt.Fields = new Dictionary { { "Name", text }, { "AvgMs", Math.Round(num4, 3) }, { "TotalMs", Math.Round(num3, 3) }, { "CallCount", item.Value.CallCount } }; StructuredLogger.WriteEvent(evt); } catch (Exception ex) { ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogWarning((object)("[WHY_LAG] PluginProfiler WriteEvent failed: " + ex.Message)); } } } double num5 = ((totalFrames > 0) ? (num2 / (double)totalFrames) : num2); _reportSb.Append($"[WHY_LAG] Total MOD overhead: {num5:F2}ms/frame"); LagLogger.Write(_reportSb.ToString()); foreach (PluginTimingData value2 in _timings.Values) { value2.TotalTicks = 0L; value2.CallCount = 0; } } public static void WriteSpikeDetail(float frameMs) { if (!_initialized || _frameTimers.Count == 0) { return; } double num = (double)Stopwatch.Frequency / 1000.0; _spikeSorted.Clear(); foreach (KeyValuePair frameTimer in _frameTimers) { if (frameTimer.Value > 0) { _spikeSorted.Add(frameTimer); } } _spikeSorted.Sort((KeyValuePair a, KeyValuePair b) => b.Value.CompareTo(a.Value)); _reportSb.Clear(); _reportSb.Append($"[WHY_LAG] !!! SPIKE {frameMs:F0}ms ({((frameMs > 0f) ? (1000f / frameMs) : 0f):F1} FPS) !!! Plugins: "); foreach (KeyValuePair item in _spikeSorted) { string value; string arg = (_displayNames.TryGetValue(item.Key, out value) ? value : item.Key); double num2 = (double)item.Value / num; if (!(num2 < 0.1)) { _reportSb.Append($"{arg}={num2:F1}ms | "); } } LagLogger.Write(_reportSb.ToString()); string value2 = null; double value3 = 0.0; if (_spikeSorted.Count > 0) { KeyValuePair keyValuePair = _spikeSorted[0]; value2 = (_displayNames.TryGetValue(keyValuePair.Key, out var value4) ? value4 : keyValuePair.Key); value3 = (double)keyValuePair.Value / num; } try { Dictionary dictionary = new Dictionary { { "AvgFrameMs", Math.Round(frameMs, 2) }, { "SpikeThresholdMs", FpsTracker.SpikeThresholdMs } }; if (!string.IsNullOrEmpty(value2)) { dictionary["TopPluginName"] = value2; dictionary["TopPluginMs"] = Math.Round(value3, 2); } StructuredEvent evt = default(StructuredEvent); evt.Timestamp = StructuredLogger.NowStamp(); evt.FrameNumber = Time.frameCount; evt.Type = EventType.SpikeFrame; evt.Fields = dictionary; StructuredLogger.WriteEvent(evt); } catch (Exception ex) { ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogWarning((object)("[WHY_LAG] PluginProfiler SpikeEvent failed: " + ex.Message)); } } } public static void ResetFrameTimers() { _frameTimers.Clear(); } } internal static class RpcMonitor { private struct WatchedRecord { public float time; public string method; public int senderActor; public string senderName; public int targetViewID; public string targetName; public string targetPath; public string argsSummary; public string specificDesc; public int payloadBytes; } private sealed class MethodBuffer { public readonly WatchedRecord[] Records; public int Idx; public int Count; public int Total; public MethodBuffer(int cap) { Records = new WatchedRecord[cap]; } public void Reset() { Idx = 0; Count = 0; Total = 0; } } private struct RpcQueueItem { public string methodName; public int senderActor; public string senderName; public int targetVID; public object[] args; public int payloadBytes; public bool watched; } public static bool Enabled = true; public static int TopMethodCount = 10; public static int WatchedRecordPerMethodCapacity = 32; public static int WatchedShowPerMethod = 6; public static int PumpBatchSize = 32; public static readonly HashSet WatchedMethods = new HashSet(StringComparer.Ordinal) { "SendFeedDataRPC", "RemoveFeedDataRPC", "GetFedItemRPC", "Consume", "RPCA_ConsumeItem", "IncrementFriendHealingRpc", "IncrementPoisonHealedStat", "SyncStatusesRPC", "SyncAfflictionsRPC", "RPC_ApplyStatusesFromFloatArray", "RPCA_AddStatusBingBing", "RPCA_Stick", "RPCA_Unstick", "RPC_StickToCharacterRemote", "TryAddAfflictionToLocalCharacter", "RPCA_Die", "RPCA_Zombify", "RPCA_SetDead", "RPCA_PassOut", "RPCA_UnPassOut", "RPCA_Fall", "RPCA_FallWithScreenShake", "RPCA_UnFall", "RPCA_Revive", "RPCA_ReviveAtPosition", "WarpPlayerRPC", "RPCA_AddForceAtPosition", "RPCA_AddForceToBodyPart", "RPCA_GrabAttach", "RPCA_GrabUnattach", "RPCA_Kick", "RPCA_StartCarry", "RPCA_Drop", "LightLanternRPC", "PutInBackpackRPC", "SetHeldItemID", "DropItemRpc", "DropItemFromSlotRPC", "DestroyHeldItemRpc", "EquipSlotRpc", "RequestPickup", "RPC_SetThrownData", "RPCAddItemToBackpack", "RPCAddItemToCharacterBackpack", "OpenLuggageRPC", "SyncInventoryRPC", "RPC_SetInventory", "RPCRemoveItemFromSlot", "CreatePrefabRPC", "InstantiateAndGrabRPC", "RPC_SpawnResourceAtPosition", "RPCEndGame", "RPCEndGame_ForceWin", "BeginIslandLoadRPC", "LoadIslandMaster", "BeginAirportLoadRPC", "LoadAirportMaster", "RPC_GetKicked", "RPCA_CaptureCharacter", "RPCA_ThrowPlayer", "RPCA_InitTornado", "RPCA_SyncCanBeCannibalized", "RPC_SyncSkeleton", "Light_Rpc", "Extinguish_Rpc" }; private const byte K_VIEWID = 0; private const byte K_PREFIX = 1; private const byte K_METHOD_NAME = 3; private const byte K_ARGS = 4; private const byte K_METHOD_IDX = 5; private static readonly Dictionary _totalByMethod = new Dictionary(128); private static readonly Dictionary> _totalByMethodActor = new Dictionary>(64); private static readonly Dictionary _windowByMethod = new Dictionary(64); private static readonly Dictionary _totalBytesByMethod = new Dictionary(128); private static readonly Dictionary _maxBytesByMethod = new Dictionary(128); private static readonly Dictionary _watchedByMethod = new Dictionary(32, StringComparer.Ordinal); private static readonly ConcurrentQueue _queue = new ConcurrentQueue(); private static readonly StringBuilder _argsSb = new StringBuilder(48); private static readonly StringBuilder _pathSb = new StringBuilder(64); private static readonly List _pathStack = new List(8); private static readonly List> _totalSorted = new List>(64); private static readonly List> _watchedSorted = new List>(32); private static readonly List> _topActorsSorted = new List>(8); private static bool _inited; public static void AddExtraWatchMethods(string csv) { if (string.IsNullOrWhiteSpace(csv)) { return; } string[] array = csv.Split(new char[1] { ',' }); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (!string.IsNullOrEmpty(text)) { WatchedMethods.Add(text); } } } public static void Initialize(Harmony harmony) { if (_inited) { return; } try { PatchExecuteRpc(harmony); _watchedByMethod.Clear(); foreach (string watchedMethod in WatchedMethods) { _watchedByMethod[watchedMethod] = new MethodBuffer(WatchedRecordPerMethodCapacity); } _inited = true; AbuseLogger.Info($"[RPC_MON] RpcMonitor initialized (watched: {WatchedMethods.Count}, per-method buffer: {WatchedRecordPerMethodCapacity}, pump batch: {PumpBatchSize})"); } catch (Exception ex) { AbuseLogger.Info("[RPC_MON] Init failed: " + ex.Message); ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogError((object)$"[WHY_LAG] RpcMonitor init failed: {ex}"); } } } private static void PatchExecuteRpc(Harmony harmony) { //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Expected O, but got Unknown MethodInfo method = typeof(PhotonNetwork).GetMethod("ExecuteRpc", BindingFlags.Static | BindingFlags.NonPublic, null, new Type[2] { typeof(Hashtable), typeof(Player) }, null); if (method == null) { AbuseLogger.Info("[RPC_MON] WARNING: PhotonNetwork.ExecuteRpc not found. Remote RPC method names unavailable."); return; } try { harmony.Patch((MethodBase)method, new HarmonyMethod(typeof(RpcMonitor), "OnExecuteRpcPrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); AbuseLogger.Info("[RPC_MON] Hooked PhotonNetwork.ExecuteRpc successfully"); } catch (Exception arg) { ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogError((object)$"[WHY_LAG] RpcMonitor patch ExecuteRpc failed: {arg}"); } } } private static void OnExecuteRpcPrefix(Hashtable rpcData, Player sender) { if (!Enabled || rpcData == null) { return; } try { string text = null; object obj = rpcData[(byte)5]; if (obj != null) { try { int num = (byte)obj; List list = PhotonNetwork.PhotonServerSettings?.RpcList; if (list != null && num < list.Count) { text = list[num]; } } catch { } } if (text == null) { text = rpcData[(byte)3] as string; } if (string.IsNullOrEmpty(text)) { return; } bool flag = _watchedByMethod.ContainsKey(text); int targetVID = -1; try { object obj3 = rpcData[(byte)0]; if (obj3 is int) { int num2 = (int)obj3; targetVID = num2; } } catch { } int payloadBytes = EstimatePayloadSize(rpcData); object[] args = (flag ? (rpcData[(byte)4] as object[]) : null); _queue.Enqueue(new RpcQueueItem { methodName = text, senderActor = ((sender != null) ? sender.ActorNumber : (-1)), senderName = ((sender != null) ? sender.NickName : null), targetVID = targetVID, args = args, payloadBytes = payloadBytes, watched = flag }); } catch (Exception ex) { ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogWarning((object)("[WHY_LAG] RpcMonitor prefix failed: " + ex.Message)); } } } public static void PumpQueue() { if (!_inited) { return; } int pumpBatchSize = PumpBatchSize; RpcQueueItem result; while (pumpBatchSize-- > 0 && _queue.TryDequeue(out result)) { try { ProcessDequeued(ref result); } catch (Exception ex) { ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogWarning((object)("[WHY_LAG] RpcMonitor pump failed: " + ex.Message)); } } } } private static void ProcessDequeued(ref RpcQueueItem item) { string methodName = item.methodName; _windowByMethod.TryGetValue(methodName, out var value); _windowByMethod[methodName] = value + 1; _totalByMethod.TryGetValue(methodName, out var value2); _totalByMethod[methodName] = value2 + 1; if (!_totalByMethodActor.TryGetValue(methodName, out var value3)) { value3 = new Dictionary(4); _totalByMethodActor[methodName] = value3; } value3.TryGetValue(item.senderActor, out var value4); value3[item.senderActor] = value4 + 1; if (item.payloadBytes > 0) { _totalBytesByMethod.TryGetValue(methodName, out var value5); _totalBytesByMethod[methodName] = value5 + item.payloadBytes; _maxBytesByMethod.TryGetValue(methodName, out var value6); if (item.payloadBytes > value6) { _maxBytesByMethod[methodName] = item.payloadBytes; } } if (item.watched && _watchedByMethod.TryGetValue(methodName, out var value7)) { RecordWatched(ref item, value7); int num = (value7.Idx - 1 + value7.Records.Length) % value7.Records.Length; WatchedRecord watchedRecord = value7.Records[num]; try { StructuredEvent evt = default(StructuredEvent); evt.Timestamp = StructuredLogger.NowStamp(); evt.FrameNumber = Time.frameCount; evt.Type = EventType.RpcCall; evt.Fields = new Dictionary { { "RpcMethod", methodName }, { "SenderActor", item.senderActor }, { "SenderName", item.senderName ?? "" }, { "TargetViewID", item.targetVID }, { "TargetName", watchedRecord.targetName ?? "" }, { "PayloadBytes", item.payloadBytes }, { "ArgsSummary", watchedRecord.argsSummary ?? "" }, { "SpecificDesc", watchedRecord.specificDesc ?? "" } }; StructuredLogger.WriteEvent(evt); } catch { } } } private static int EstimatePayloadSize(Hashtable rpcData) { int num = 16; if (!(rpcData[(byte)4] is object[] array)) { return num; } foreach (object obj in array) { num = ((obj != null) ? ((!(obj is bool) && !(obj is byte) && !(obj is sbyte)) ? ((!(obj is short) && !(obj is ushort)) ? ((!(obj is int) && !(obj is uint) && !(obj is float)) ? ((obj is long || obj is ulong || obj is double) ? (num + 8) : ((obj is Vector2) ? (num + 8) : ((obj is Vector3) ? (num + 12) : ((obj is Quaternion) ? (num + 16) : ((obj is string text) ? (num + (2 + text.Length * 2)) : ((obj is byte[] array2) ? (num + (4 + array2.Length)) : ((obj is int[] array3) ? (num + (4 + array3.Length * 4)) : ((obj is float[] array4) ? (num + (4 + array4.Length * 4)) : ((!(obj is Array array5)) ? (num + 16) : (num + (4 + array5.Length * 4))))))))))) : (num + 4)) : (num + 2)) : (num + 1)) : (num + 1)); } return num; } private static void RecordWatched(ref RpcQueueItem item, MethodBuffer buf) { string text = null; string targetPath = null; try { if (item.targetVID > 0) { PhotonView photonView = PhotonNetwork.GetPhotonView(item.targetVID); if ((Object)(object)photonView != (Object)null) { Player owner = photonView.Owner; text = ((owner != null) ? owner.NickName : null); if (string.IsNullOrEmpty(text)) { text = (((Object)(object)((Component)photonView).gameObject != (Object)null) ? ((Object)((Component)photonView).gameObject).name : null); } if ((Object)(object)((Component)photonView).gameObject != (Object)null) { targetPath = BuildTransformPath(((Component)photonView).gameObject.transform); } } } } catch { } string argsSummary = BuildArgsSummary(item.args); string specificDesc = null; try { specificDesc = ParseSpecific(item.methodName, item.args, item.targetVID); } catch { } WatchedRecord watchedRecord = default(WatchedRecord); watchedRecord.time = Time.realtimeSinceStartup; watchedRecord.method = item.methodName; watchedRecord.senderActor = item.senderActor; watchedRecord.senderName = item.senderName; watchedRecord.targetViewID = item.targetVID; watchedRecord.targetName = text; watchedRecord.targetPath = targetPath; watchedRecord.argsSummary = argsSummary; watchedRecord.specificDesc = specificDesc; watchedRecord.payloadBytes = item.payloadBytes; WatchedRecord watchedRecord2 = watchedRecord; buf.Records[buf.Idx] = watchedRecord2; buf.Idx = (buf.Idx + 1) % buf.Records.Length; if (buf.Count < buf.Records.Length) { buf.Count++; } buf.Total++; } private static string BuildArgsSummary(object[] args) { if (args == null || args.Length == 0) { return null; } _argsSb.Clear(); int num = ((args.Length > 3) ? 3 : args.Length); for (int i = 0; i < num; i++) { if (i > 0) { _argsSb.Append(','); } object obj = args[i]; if (obj == null) { _argsSb.Append("null"); continue; } _argsSb.Append(obj.GetType().Name); if (obj is int || obj is bool || obj is float || obj is string || obj is byte || obj is short) { _argsSb.Append('=').Append(obj); } } if (args.Length > 3) { _argsSb.Append(",..."); } return _argsSb.ToString(); } private static string ParseSpecific(string m, object[] args, int targetVID) { //IL_066e: Unknown result type (might be due to invalid IL or missing references) //IL_0673: Unknown result type (might be due to invalid IL or missing references) //IL_067a: Unknown result type (might be due to invalid IL or missing references) //IL_0686: Unknown result type (might be due to invalid IL or missing references) //IL_0692: Unknown result type (might be due to invalid IL or missing references) //IL_0a77: Unknown result type (might be due to invalid IL or missing references) //IL_0a7c: Unknown result type (might be due to invalid IL or missing references) //IL_0ad6: Unknown result type (might be due to invalid IL or missing references) //IL_0adb: Unknown result type (might be due to invalid IL or missing references) //IL_06c0: Unknown result type (might be due to invalid IL or missing references) //IL_06c5: Unknown result type (might be due to invalid IL or missing references) //IL_0703: Unknown result type (might be due to invalid IL or missing references) //IL_0712: Unknown result type (might be due to invalid IL or missing references) //IL_0721: Unknown result type (might be due to invalid IL or missing references) if (args == null) { return null; } switch (m) { case "SendFeedDataRPC": { if (args.Length < 4) { break; } object obj = args[0]; if (!(obj is int)) { break; } int num12 = (int)obj; obj = args[1]; if (obj is int) { int num13 = (int)obj; obj = args[2]; if (obj is int) { int num14 = (int)obj; string text7 = ResolvePhotonViewOwner(num12); string text8 = ResolvePhotonViewOwner(num13); string text9 = ResolvePhotonViewGameObject(num14); float num16 = ((args[3] is float num15) ? num15 : 0f); return $"喂食者={text7}#{num12} → 被喂者={text8}#{num13}, 物品={text9}#{num14}, 时长={num16:F1}s"; } } break; } case "RemoveFeedDataRPC": if (args.Length >= 1) { object obj = args[0]; if (obj is int) { int num7 = (int)obj; string arg2 = ResolvePhotonViewOwner(num7); return $"喂食结束: 喂食者={arg2}#{num7}"; } } break; case "GetFedItemRPC": if (args.Length >= 1) { object obj = args[0]; if (obj is int) { int num3 = (int)obj; string text2 = ResolvePhotonViewGameObject(num3); string text3 = ResolvePhotonViewOwner(targetVID); return $"接收物品={text2}#{num3}, 目标角色={text3}#{targetVID}"; } } break; case "RPCA_ConsumeItem": case "Consume": if (args.Length >= 1) { object obj = args[0]; if (obj is int) { int num6 = (int)obj; string text4 = ResolvePhotonViewOwner(num6); string text5 = ResolvePhotonViewGameObject(targetVID); return $"消耗者={text4}#{num6}, 物品={text5}#{targetVID}"; } } break; case "IncrementFriendHealingRpc": if (args.Length >= 1) { object obj = args[0]; if (obj is int) { int num = (int)obj; return $"治疗量={num}"; } } break; case "IncrementPoisonHealedStat": if (args.Length >= 1) { object obj = args[0]; if (obj is int) { int num4 = (int)obj; return $"解毒量={num4}"; } } break; case "DropItemRpc": return $"丢弃物品, target={ResolvePhotonViewGameObject(targetVID)}#{targetVID}"; case "RequestPickup": if (args.Length >= 1) { string arg = "?"; if (args[0] is int vid) { arg = ResolvePhotonViewOwner(vid); } return $"拾取请求: 角色={arg}, 物品={ResolvePhotonViewGameObject(targetVID)}#{targetVID}"; } break; case "RPC_SetThrownData": { if (args.Length < 2) { break; } object obj = args[0]; if (obj is int) { int num21 = (int)obj; obj = args[1]; if (obj is float) { float num22 = (float)obj; string arg4 = ResolvePhotonViewOwner(num21); return $"投掷者={arg4}#{num21}, 力度={num22:F2}"; } } break; } case "RPCA_Die": case "RPCA_Zombify": if (args.Length >= 1) { object obj = args[0]; if (obj is Vector3) { Vector3 val4 = (Vector3)obj; return $"spawnPos=({val4.x:F1},{val4.y:F1},{val4.z:F1})"; } } break; case "WarpPlayerRPC": if (args.Length >= 1) { object obj = args[0]; if (obj is Vector3) { Vector3 val3 = (Vector3)obj; string text = ((args.Length >= 2 && args[1] is bool flag) ? flag.ToString() : "?"); return $"to=({val3.x:F1},{val3.y:F1},{val3.z:F1}), poof={text}"; } } break; case "RPCA_Fall": if (args.Length >= 1) { object obj = args[0]; if (obj is float) { float num20 = (float)obj; return $"sec={num20:F2}"; } } break; case "RPCA_FallWithScreenShake": { if (args.Length < 2) { break; } object obj = args[0]; if (obj is float) { float num8 = (float)obj; obj = args[1]; if (obj is float) { float num9 = (float)obj; return $"sec={num8:F2}, shake={num9:F2}"; } } break; } case "LightLanternRPC": if (args.Length >= 1) { object obj = args[0]; if (obj is bool) { bool flag2 = (bool)obj; return $"lit={flag2}"; } } break; case "PutInBackpackRPC": if (args.Length >= 1) { byte b3 = (byte)((args[0] is byte b2) ? b2 : 0); return $"slot={b3}"; } break; case "RPCA_AddStatusBingBing": { if (args.Length < 3) { break; } object obj = args[0]; if (!(obj is int)) { break; } int num10 = (int)obj; obj = args[1]; if (obj is int) { int i2 = (int)obj; obj = args[2]; if (obj is int) { int num11 = (int)obj; string text6 = ResolvePhotonViewOwner(num10); return $"target={text6}#{num10}, status={StatusName(i2)}, mult={num11}"; } } break; } case "RPCA_Stick": if (args.Length >= 5) { string arg3 = args[0]?.ToString() ?? "?"; int i3 = ((args[3] is int num17) ? num17 : ((args[3] is byte b) ? b : (-1))); float num19 = ((args[4] is float num18) ? num18 : 0f); return $"body={arg3}, status={StatusName(i3)}, amount={num19:F2}"; } break; case "SyncStatusesRPC": case "SyncAfflictionsRPC": if (args.Length >= 1 && args[0] is byte[] array2) { return $"bytes={array2.Length}"; } break; case "RPC_ApplyStatusesFromFloatArray": { if (args.Length < 1 || !(args[0] is float[] array)) { break; } StringBuilder stringBuilder = new StringBuilder(64); stringBuilder.Append($"floats[{array.Length}]"); int num5 = Math.Min(array.Length, 12); stringBuilder.Append(" 值="); for (int i = 0; i < num5; i++) { if (i > 0) { stringBuilder.Append(','); } if (array[i] != 0f) { stringBuilder.Append(StatusName(i)).Append(':').Append(array[i].ToString("F2")); } else { stringBuilder.Append('_'); } } return stringBuilder.ToString(); } case "RPCA_AddForceAtPosition": if (args.Length >= 3) { object obj = args[0]; if (obj is Vector3) { Vector3 val2 = (Vector3)obj; float magnitude = ((Vector3)(ref val2)).magnitude; return $"force=|{magnitude:F1}|, radius={((args[2] is float num2) ? num2 : 0f):F1}"; } } break; case "RPCA_AddForceToBodyPart": if (args.Length >= 2) { object obj = args[1]; if (obj is Vector3) { Vector3 val = (Vector3)obj; return $"body={args[0]}, forceMag={((Vector3)(ref val)).magnitude:F1}"; } } break; } return null; } private static string StatusName(int i) { return i switch { 0 => "Injury", 1 => "Hunger", 2 => "Cold", 3 => "Poison", 4 => "Crab", 5 => "Curse", 6 => "Drowsy", 7 => "Weight", 8 => "Hot", 9 => "Thorns", 10 => "Spores", 11 => "Web", _ => $"STATUS_{i}", }; } private static string ResolvePhotonViewOwner(int vid) { if (vid <= 0) { return "?"; } try { PhotonView photonView = PhotonNetwork.GetPhotonView(vid); if ((Object)(object)photonView == (Object)null) { return "?"; } Player owner = photonView.Owner; if (!string.IsNullOrEmpty((owner != null) ? owner.NickName : null)) { return photonView.Owner.NickName; } return ((Object)(object)((Component)photonView).gameObject != (Object)null) ? ((Object)((Component)photonView).gameObject).name : "?"; } catch { return "?"; } } private static string ResolvePhotonViewGameObject(int vid) { if (vid <= 0) { return "?"; } try { PhotonView photonView = PhotonNetwork.GetPhotonView(vid); if ((Object)(object)photonView == (Object)null) { return "?"; } return ((Object)(object)((Component)photonView).gameObject != (Object)null) ? ((Object)((Component)photonView).gameObject).name : "?"; } catch { return "?"; } } private static string BuildTransformPath(Transform t) { if ((Object)(object)t == (Object)null) { return null; } try { _pathStack.Clear(); int num = 0; while ((Object)(object)t != (Object)null && num < 6) { _pathStack.Add(((Object)t).name); t = t.parent; num++; } _pathSb.Clear(); for (int num2 = _pathStack.Count - 1; num2 >= 0; num2--) { _pathSb.Append(_pathStack[num2]); if (num2 > 0) { _pathSb.Append('/'); } } return _pathSb.ToString(); } catch { return null; } } public static void OnWindowEnd() { _windowByMethod.Clear(); } public static void WritePeriodicReport() { PumpQueue(); bool flag = false; foreach (MethodBuffer value4 in _watchedByMethod.Values) { if (value4.Total > 0) { flag = true; break; } } if (_totalByMethod.Count == 0 && !flag) { return; } AbuseLogger.Write($"[RPC_MON] Top {TopMethodCount} RPC methods this period:"); _totalSorted.Clear(); foreach (KeyValuePair item in _totalByMethod) { _totalSorted.Add(item); } _totalSorted.Sort((KeyValuePair a, KeyValuePair b) => b.Value.CompareTo(a.Value)); int num = 0; foreach (KeyValuePair item2 in _totalSorted) { string text = BuildTopActorsString(item2.Key); _totalBytesByMethod.TryGetValue(item2.Key, out var value); _maxBytesByMethod.TryGetValue(item2.Key, out var value2); int num2 = (int)((item2.Value > 0) ? (value / item2.Value) : 0); AbuseLogger.WriteRaw($" {item2.Key}: {item2.Value}x avg={num2}B max={value2}B total={value}B [{text}]"); if (++num >= TopMethodCount) { break; } } if (flag) { _watchedSorted.Clear(); foreach (KeyValuePair item3 in _watchedByMethod) { _watchedSorted.Add(item3); } _watchedSorted.Sort((KeyValuePair a, KeyValuePair b) => b.Value.Total.CompareTo(a.Value.Total)); int num3 = 0; foreach (KeyValuePair item4 in _watchedSorted) { num3 += item4.Value.Total; } AbuseLogger.Write($"[RPC_MON] Watched RPC records: {num3} total across {WatchedMethods.Count} methods (per-method last {WatchedShowPerMethod})"); foreach (KeyValuePair item5 in _watchedSorted) { MethodBuffer value3 = item5.Value; if (value3.Total != 0) { int num4 = Math.Min(WatchedShowPerMethod, value3.Count); string text2 = ((value3.Total > value3.Count) ? $", dropped={value3.Total - value3.Count}" : ""); AbuseLogger.WriteRaw($" ── {item5.Key} total={value3.Total}, kept={value3.Count}{text2}, showing last {num4}"); int num5 = (value3.Idx - num4 + value3.Records.Length) % value3.Records.Length; for (int i = 0; i < num4; i++) { WatchedRecord watchedRecord = value3.Records[(num5 + i) % value3.Records.Length]; AbuseLogger.WriteRaw(string.Format(" [{0:F1}s] {1}#{2} → target=({3}#{4}) payload={5}B", watchedRecord.time, watchedRecord.senderName ?? "?", watchedRecord.senderActor, watchedRecord.targetName ?? "?", watchedRecord.targetViewID, watchedRecord.payloadBytes)); AbuseLogger.WriteRaw(" path=" + (watchedRecord.targetPath ?? "-")); AbuseLogger.WriteRaw(" " + ((watchedRecord.specificDesc != null) ? ("detail=" + watchedRecord.specificDesc) : ("args=[" + (watchedRecord.argsSummary ?? "-") + "]"))); } } } } _totalByMethod.Clear(); _totalByMethodActor.Clear(); _totalBytesByMethod.Clear(); _maxBytesByMethod.Clear(); foreach (MethodBuffer value5 in _watchedByMethod.Values) { value5.Reset(); } } private static string BuildTopActorsString(string methodName) { if (!_totalByMethodActor.TryGetValue(methodName, out var value) || value.Count == 0) { return ""; } _topActorsSorted.Clear(); foreach (KeyValuePair item in value) { _topActorsSorted.Add(item); } _topActorsSorted.Sort((KeyValuePair a, KeyValuePair b) => b.Value.CompareTo(a.Value)); _argsSb.Clear(); int num = ((_topActorsSorted.Count > 3) ? 3 : _topActorsSorted.Count); for (int i = 0; i < num; i++) { if (i > 0) { _argsSb.Append(", "); } int key = _topActorsSorted[i].Key; string value2 = ResolvePlayerName(key); _argsSb.Append(value2).Append('#').Append(key) .Append(':') .Append(_topActorsSorted[i].Value); } return _argsSb.ToString(); } private static string ResolvePlayerName(int actor) { if (actor < 0) { return "Scene"; } try { if (!PhotonNetwork.InRoom) { return "?"; } Player[] playerList = PhotonNetwork.PlayerList; foreach (Player val in playerList) { if (val.ActorNumber == actor) { return val.NickName ?? "?"; } } } catch { } return "?"; } public static void LogCurrentWindowTopMethods(int topN) { if (_windowByMethod.Count == 0) { return; } _totalSorted.Clear(); foreach (KeyValuePair item in _windowByMethod) { _totalSorted.Add(item); } _totalSorted.Sort((KeyValuePair a, KeyValuePair b) => b.Value.CompareTo(a.Value)); AbuseLogger.AlertDetail("[RPC_MON] Current-window top RPC methods:"); int num = 0; foreach (KeyValuePair item2 in _totalSorted) { AbuseLogger.AlertDetailRaw($" {item2.Key}: {item2.Value}x"); if (++num >= topN) { break; } } } public static string GetRecentWatchedSummary(int maxLines) { _argsSb.Clear(); int num = 0; foreach (KeyValuePair item in _watchedByMethod) { if (num >= maxLines) { break; } MethodBuffer value = item.Value; if (value.Total != 0) { _argsSb.Append(item.Key).Append('=').Append(value.Total) .Append(' '); num++; } } if (_argsSb.Length != 0) { return _argsSb.ToString(); } return "(none)"; } } internal enum EventType { FpsReport, PluginTiming, PatchTiming, SpikeFrame, AbuseAlert, RpcCall, PeriodicReport, HarmonyPatchMap, MethodTrace, InstantiateTrace, RemoteRpcTrace, OwnershipChange } internal struct StructuredEvent { public string Timestamp; public long FrameNumber; public EventType Type; public Dictionary Fields; } internal static class StructuredLogger { public static int MaxLogFileSizeMB = 10; private static readonly string[] CsvColumns = new string[58] { "Timestamp", "FrameNumber", "Type", "AvgFps", "MinFps", "MaxFps", "AvgFrameMs", "SpikeThresholdMs", "SpikeCount", "ReportDuration", "AllocRateKBps", "Ping", "Name", "AvgMs", "TotalMs", "CallCount", "Owner", "TopPluginName", "TopPluginMs", "AlertType", "Rate", "Threshold", "Delta", "CurrentCount", "TopActor", "TopActorName", "TopOwners", "TotalInstantiates", "TotalDestroys", "TotalRpcs", "AlertCount", "RoomName", "PlayerCount", "MaxPlayers", "ZombieCount", "RpcMethod", "SenderActor", "SenderName", "TargetViewID", "TargetName", "PayloadBytes", "ArgsSummary", "SpecificDesc", "TargetMethod", "PatchType", "OwnerHarmonyId", "Priority", "TraceStack", "TraceCaller", "Snapshot", "PrefabName", "IsMasterClient", "Position", "LocalActor", "SuspectedRequesterActor", "SuspectedRequesterName", "SuspectedRequesterRpc", "SuspectedAgeMs" }; private static readonly object _lock = new object(); private static StreamWriter _csvWriter; private static StreamWriter _jsonlWriter; private static string _dir; private static string _csvPath; private static string _jsonlPath; private static int _pendingFlush; private static bool _inited; private static readonly StringBuilder _csvSb = new StringBuilder(512); private static readonly StringBuilder _jsonSb = new StringBuilder(512); public static void Initialize(string dir) { lock (_lock) { if (_inited) { return; } try { _dir = dir; _csvPath = Path.Combine(dir, "whysolaggy_data.csv"); _jsonlPath = Path.Combine(dir, "whysolaggy_events.jsonl"); bool num = !File.Exists(_csvPath) || new FileInfo(_csvPath).Length == 0; _csvWriter = new StreamWriter(_csvPath, append: true, Encoding.UTF8) { AutoFlush = false }; _jsonlWriter = new StreamWriter(_jsonlPath, append: true, Encoding.UTF8) { AutoFlush = false }; if (num) { _csvWriter.WriteLine(string.Join(",", CsvColumns)); _csvWriter.Flush(); } _inited = true; } catch (Exception ex) { ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogError((object)("[WHY_LAG] StructuredLogger init failed: " + ex.Message)); } _csvWriter = null; _jsonlWriter = null; } } } public static void WriteEvent(StructuredEvent evt) { if (!_inited) { return; } lock (_lock) { if (_csvWriter == null || _jsonlWriter == null) { return; } try { RotateIfNeeded(); WriteCsvRow(evt); WriteJsonlRow(evt); if (++_pendingFlush >= 10) { _csvWriter.Flush(); _jsonlWriter.Flush(); _pendingFlush = 0; } } catch (Exception ex) { ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogWarning((object)("[WHY_LAG] StructuredLogger write failed: " + ex.Message)); } } } } public static void Flush() { if (!_inited) { return; } lock (_lock) { try { _csvWriter?.Flush(); _jsonlWriter?.Flush(); _pendingFlush = 0; } catch { } } } public static void Shutdown() { lock (_lock) { try { _csvWriter?.Flush(); _csvWriter?.Close(); _jsonlWriter?.Flush(); _jsonlWriter?.Close(); } catch { } _csvWriter = null; _jsonlWriter = null; _inited = false; } } private static void WriteCsvRow(StructuredEvent evt) { _csvSb.Clear(); Dictionary fields = evt.Fields; for (int i = 0; i < CsvColumns.Length; i++) { if (i > 0) { _csvSb.Append(','); } string text = CsvColumns[i]; AppendCsvField(v: text switch { "Timestamp" => evt.Timestamp, "FrameNumber" => evt.FrameNumber, "Type" => evt.Type.ToString(), _ => (fields == null || !fields.TryGetValue(text, out var value)) ? null : value, }, sb: _csvSb); } _csvWriter.WriteLine(_csvSb.ToString()); } private static void AppendCsvField(StringBuilder sb, object v) { if (v == null) { return; } string text = Convert.ToString(v, CultureInfo.InvariantCulture) ?? ""; bool flag = false; foreach (char c in text) { if (c == ',' || c == '"' || c == '\n' || c == '\r') { flag = true; break; } } if (!flag) { sb.Append(text); return; } sb.Append('"'); foreach (char c2 in text) { if (c2 == '"') { sb.Append("\"\""); } else { sb.Append(c2); } } sb.Append('"'); } private static void WriteJsonlRow(StructuredEvent evt) { _jsonSb.Clear(); _jsonSb.Append('{'); AppendJsonKV(_jsonSb, "timestamp", evt.Timestamp, first: true); AppendJsonKV(_jsonSb, "frameNumber", evt.FrameNumber); AppendJsonKV(_jsonSb, "type", evt.Type.ToString()); if (evt.Fields != null) { foreach (KeyValuePair field in evt.Fields) { AppendJsonKV(_jsonSb, ToCamel(field.Key), field.Value); } } _jsonSb.Append('}'); _jsonlWriter.WriteLine(_jsonSb.ToString()); } private static void AppendJsonKV(StringBuilder sb, string key, object value, bool first = false) { if (!first) { sb.Append(','); } sb.Append('"'); AppendJsonString(sb, key); sb.Append('"').Append(':'); AppendJsonValue(sb, value); } private static void AppendJsonValue(StringBuilder sb, object v) { if (v == null) { sb.Append("null"); } else if (!(v is string s)) { if (!(v is bool flag)) { if (!(v is float f)) { if (!(v is double d)) { if (v is byte || v is sbyte || v is short || v is ushort || v is int || v is uint || v is long || v is ulong) { sb.Append(Convert.ToString(v, CultureInfo.InvariantCulture)); return; } sb.Append('"'); AppendJsonString(sb, Convert.ToString(v, CultureInfo.InvariantCulture) ?? ""); sb.Append('"'); } else { sb.Append((double.IsNaN(d) || double.IsInfinity(d)) ? "null" : d.ToString("R", CultureInfo.InvariantCulture)); } } else { sb.Append((float.IsNaN(f) || float.IsInfinity(f)) ? "null" : f.ToString("R", CultureInfo.InvariantCulture)); } } else { sb.Append(flag ? "true" : "false"); } } else { sb.Append('"'); AppendJsonString(sb, s); sb.Append('"'); } } private static void AppendJsonString(StringBuilder sb, string s) { foreach (char c in s) { switch (c) { case '"': sb.Append("\\\""); continue; case '\\': sb.Append("\\\\"); continue; case '\b': sb.Append("\\b"); continue; case '\f': sb.Append("\\f"); continue; case '\n': sb.Append("\\n"); continue; case '\r': sb.Append("\\r"); continue; case '\t': sb.Append("\\t"); continue; } if (c < ' ') { sb.AppendFormat(CultureInfo.InvariantCulture, "\\u{0:X4}", (int)c); } else { sb.Append(c); } } } private static string ToCamel(string s) { if (string.IsNullOrEmpty(s)) { return s; } char c = s[0]; if (c >= 'A' && c <= 'Z') { return char.ToLowerInvariant(c) + ((s.Length > 1) ? s.Substring(1) : ""); } return s; } private static void RotateIfNeeded() { try { long num = (long)MaxLogFileSizeMB * 1024L * 1024; if (num > 0) { RotateOne(ref _csvWriter, _csvPath, csv: true, num); RotateOne(ref _jsonlWriter, _jsonlPath, csv: false, num); } } catch { } } private static void RotateOne(ref StreamWriter writer, string path, bool csv, long maxBytes) { if (writer == null || string.IsNullOrEmpty(path)) { return; } try { FileInfo fileInfo = new FileInfo(path); if (fileInfo.Exists && fileInfo.Length >= maxBytes) { writer.Flush(); writer.Close(); string text = DateTime.Now.ToString("yyyyMMdd_HHmm", CultureInfo.InvariantCulture); string directoryName = Path.GetDirectoryName(path); string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path); string extension = Path.GetExtension(path); string text2 = Path.Combine(directoryName ?? _dir ?? "", fileNameWithoutExtension + "_" + text + extension); int num = 0; while (File.Exists(text2) && num < 100) { string path2 = directoryName ?? _dir ?? ""; string[] obj = new string[6] { fileNameWithoutExtension, "_", text, "_", null, null }; int num2 = ++num; obj[4] = num2.ToString(); obj[5] = extension; text2 = Path.Combine(path2, string.Concat(obj)); } File.Move(path, text2); writer = new StreamWriter(path, append: false, Encoding.UTF8) { AutoFlush = false }; if (csv) { writer.WriteLine(string.Join(",", CsvColumns)); writer.Flush(); } } } catch (Exception ex) { ManualLogSource log = WhySoLaggyPlugin.Log; if (log != null) { log.LogWarning((object)("[WHY_LAG] StructuredLogger rotate failed: " + ex.Message)); } } } public static string NowStamp() { return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture); } } internal static class ValueFormatter { public const int DefaultMaxLen = 128; public static string Format(object v, int maxLen = 128) { string s; try { s = FormatCore(v); } catch (Exception ex) { s = "err:" + ex.GetType().Name; } return Truncate(s, maxLen); } private static string FormatCore(object v) { if (v == null) { return "null"; } Object val = (Object)((v is Object) ? v : null); if (val != null) { if (val == (Object)null) { return "null(UE)"; } string text; try { text = val.name; } catch { text = ""; } int num; try { num = val.GetInstanceID(); } catch { num = 0; } return text + "#" + num.ToString(CultureInfo.InvariantCulture); } if (!(v is string result)) { if (!(v is bool)) { if (!(v is char c)) { if (!(v is float num2)) { if (!(v is double num3)) { if (!(v is decimal num4)) { if (v is byte || v is sbyte || v is short || v is ushort || v is int || v is uint || v is long || v is ulong) { return Convert.ToString(v, CultureInfo.InvariantCulture); } Type type = v.GetType(); if (type.IsEnum) { return v.ToString(); } if (v is IEnumerable en && !(v is string)) { return FormatEnumerable(en); } return type.Name + "@" + RuntimeHashCode(v); } return num4.ToString(CultureInfo.InvariantCulture); } return num3.ToString("R", CultureInfo.InvariantCulture); } return num2.ToString("R", CultureInfo.InvariantCulture); } return c.ToString(); } if (!(bool)v) { return "false"; } return "true"; } return result; } private static string FormatEnumerable(IEnumerable en) { StringBuilder stringBuilder = new StringBuilder(64); stringBuilder.Append('['); int num = 0; int num2 = 0; IEnumerator enumerator = null; try { enumerator = en.GetEnumerator(); } catch { return "[iter_err]"; } try { while (enumerator.MoveNext()) { num++; if (num2 < 3) { if (num2 > 0) { stringBuilder.Append(", "); } string s; try { s = FormatCore(enumerator.Current); } catch (Exception ex) { s = "err:" + ex.GetType().Name; } stringBuilder.Append(Truncate(s, 32)); num2++; } } } finally { (enumerator as IDisposable)?.Dispose(); } if (num > num2) { stringBuilder.Append(", ..."); } stringBuilder.Append("] n=").Append(num.ToString(CultureInfo.InvariantCulture)); return stringBuilder.ToString(); } private static string RuntimeHashCode(object v) { try { return RuntimeHelpers.GetHashCode(v).ToString(CultureInfo.InvariantCulture); } catch { return "?"; } } private static string Truncate(string s, int maxLen) { if (string.IsNullOrEmpty(s)) { return s ?? "null"; } if (maxLen <= 0 || s.Length <= maxLen) { return s; } return s.Substring(0, maxLen) + "..."; } } [BepInPlugin("com.wuyachiyu.WhySoLaggy", "WhySoLaggy", "1.0.3")] public class WhySoLaggyPlugin : BaseUnityPlugin, IOnEventCallback { [CompilerGenerated] private sealed class d__46 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public WhySoLaggyPlugin <>4__this; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__46(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Expected O, but got Unknown int num = <>1__state; WhySoLaggyPlugin whySoLaggyPlugin = <>4__this; switch (num) { default: return false; case 0: <>1__state = -1; Log.LogInfo((object)"[WHY_LAG] Waiting 5s for all plugins to finish loading..."); LagLogger.Info("[WHY_LAG] Waiting 5s for all plugins to finish loading..."); <>2__current = (object)new WaitForSeconds(5f); <>1__state = 1; return true; case 1: <>1__state = -1; if (EnablePluginProfiling.Value) { PluginProfiler.Initialize(whySoLaggyPlugin._harmony); } if (EnablePatchProfiling.Value) { PatchProfiler.Initialize(whySoLaggyPlugin._harmony); } if (EnableAbuseDetection.Value) { NetworkAbuseDetector.InstantiateRateThreshold = InstantiateRateThreshold.Value; NetworkAbuseDetector.DestroyRateThreshold = DestroyRateThreshold.Value; NetworkAbuseDetector.RpcRateThreshold = RpcRateThreshold.Value; NetworkAbuseDetector.ObjectSpikeThreshold = ObjectSpikeThreshold.Value; NetworkAbuseDetector.CheckIntervalSeconds = AbuseCheckInterval.Value; NetworkAbuseDetector.ReportIntervalSeconds = AbuseReportInterval.Value; NetworkAbuseDetector.Initialize(whySoLaggyPlugin._harmony); } if (EnableRpcMonitor.Value) { RpcMonitor.Enabled = true; RpcMonitor.TopMethodCount = RpcMonitorTopCount.Value; RpcMonitor.WatchedRecordPerMethodCapacity = RpcMonitorWatchPerMethodCapacity.Value; RpcMonitor.WatchedShowPerMethod = RpcMonitorWatchShowPerMethod.Value; RpcMonitor.PumpBatchSize = PumpBatchSize.Value; RpcMonitor.AddExtraWatchMethods(ExtraWatchMethods.Value); RpcMonitor.Initialize(whySoLaggyPlugin._harmony); } else { RpcMonitor.Enabled = false; } try { HarmonyScanner.Scan(_bepInExDir); } catch (Exception ex) { Log.LogWarning((object)("[WHY_LAG] HarmonyScanner.Scan failed: " + ex.Message)); } MethodTracer.TraceMethodNames = TraceMethodNames.Value; MethodTracer.TraceMaxDepth = TraceMaxDepth.Value; MethodTracer.TraceRateLimit = TraceRateLimit.Value; try { MethodTracer.Initialize(whySoLaggyPlugin._harmony); } catch (Exception ex2) { Log.LogWarning((object)("[WHY_LAG] MethodTracer.Initialize failed: " + ex2.Message)); } FieldProbe.Enabled = EnableFieldProbe.Value; FieldProbe.RulesFilePath = ResolveFieldProbeRulesPath(FieldProbeRulesFile.Value); FieldProbe.DefaultRateLimit = FieldProbeDefaultRate.Value; FieldProbe.DefaultMaxValueLen = FieldProbeDefaultMaxLen.Value; FieldProbe.DefaultIncludeStack = FieldProbeDefaultStack.Value; FieldProbe.DefaultStackMaxDepth = FieldProbeDefaultStackDepth.Value; try { FieldProbe.Initialize(whySoLaggyPlugin._harmony); } catch (Exception ex3) { Log.LogWarning((object)("[WHY_LAG] FieldProbe.Initialize failed: " + ex3.Message)); } whySoLaggyPlugin._profilingActive = true; Log.LogInfo((object)"[WHY_LAG] Profiling active!"); LagLogger.Info("[WHY_LAG] === Profiling active ==="); return false; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } public const string PluginGuid = "com.wuyachiyu.WhySoLaggy"; public const string PluginName = "WhySoLaggy"; public const string PluginVersion = "1.0.3"; public static ConfigEntry SpikeThresholdMs; public static ConfigEntry ReportIntervalSeconds; public static ConfigEntry EnablePluginProfiling; public static ConfigEntry EnablePatchProfiling; public static ConfigEntry TopMethodCount; public static ConfigEntry EnableAbuseDetection; public static ConfigEntry AbuseCheckInterval; public static ConfigEntry AbuseReportInterval; public static ConfigEntry InstantiateRateThreshold; public static ConfigEntry DestroyRateThreshold; public static ConfigEntry RpcRateThreshold; public static ConfigEntry ObjectSpikeThreshold; public static ConfigEntry EnableRpcMonitor; public static ConfigEntry RpcMonitorTopCount; public static ConfigEntry RpcMonitorWatchPerMethodCapacity; public static ConfigEntry RpcMonitorWatchShowPerMethod; public static ConfigEntry ExtraWatchMethods; public static ConfigEntry PumpBatchSize; public static ConfigEntry MinReportMs; public static ConfigEntry IgnorePluginGuids; public static ConfigEntry IgnorePatchMethods; public static ConfigEntry VerbosityCfg; public static ConfigEntry MaxLogFileSizeMB; public static ConfigEntry EnableMemoryMonitor; public static ConfigEntry ShowDashboard; public static ConfigEntry TraceMethodNames; public static ConfigEntry TraceMaxDepth; public static ConfigEntry TraceRateLimit; public static ConfigEntry EnableFieldProbe; public static ConfigEntry FieldProbeRulesFile; public static ConfigEntry FieldProbeDefaultRate; public static ConfigEntry FieldProbeDefaultMaxLen; public static ConfigEntry FieldProbeDefaultStack; public static ConfigEntry FieldProbeDefaultStackDepth; private static string _bepInExDir; internal static ManualLogSource Log; private Harmony _harmony; private int _reportFrameCount; private float _reportTimer; private bool _profilingActive; private void Awake() { //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Expected O, but got Unknown //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Expected O, but got Unknown //IL_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Expected O, but got Unknown //IL_0145: Unknown result type (might be due to invalid IL or missing references) //IL_014f: Expected O, but got Unknown //IL_0182: Unknown result type (might be due to invalid IL or missing references) //IL_018c: Expected O, but got Unknown //IL_01b5: Unknown result type (might be due to invalid IL or missing references) //IL_01bf: Expected O, but got Unknown //IL_01e8: Unknown result type (might be due to invalid IL or missing references) //IL_01f2: Expected O, but got Unknown //IL_021f: Unknown result type (might be due to invalid IL or missing references) //IL_0229: Expected O, but got Unknown //IL_0252: Unknown result type (might be due to invalid IL or missing references) //IL_025c: Expected O, but got Unknown //IL_02a5: Unknown result type (might be due to invalid IL or missing references) //IL_02af: Expected O, but got Unknown //IL_02db: Unknown result type (might be due to invalid IL or missing references) //IL_02e5: Expected O, but got Unknown //IL_030d: Unknown result type (might be due to invalid IL or missing references) //IL_0317: Expected O, but got Unknown //IL_0367: Unknown result type (might be due to invalid IL or missing references) //IL_0371: Expected O, but got Unknown //IL_03a4: Unknown result type (might be due to invalid IL or missing references) //IL_03ae: Expected O, but got Unknown //IL_043f: Unknown result type (might be due to invalid IL or missing references) //IL_0449: Expected O, but got Unknown //IL_04d5: Unknown result type (might be due to invalid IL or missing references) //IL_04df: Expected O, but got Unknown //IL_050c: Unknown result type (might be due to invalid IL or missing references) //IL_0516: Expected O, but got Unknown //IL_0586: Unknown result type (might be due to invalid IL or missing references) //IL_0590: Expected O, but got Unknown //IL_05c0: Unknown result type (might be due to invalid IL or missing references) //IL_05ca: Expected O, but got Unknown //IL_0612: Unknown result type (might be due to invalid IL or missing references) //IL_061c: Expected O, but got Unknown //IL_0759: Unknown result type (might be due to invalid IL or missing references) //IL_0763: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; Log.LogInfo((object)"[WHY_LAG] WhySoLaggy Awake"); SpikeThresholdMs = ((BaseUnityPlugin)this).Config.Bind("General", "SpikeThresholdMs", 50, new ConfigDescription("Frame time threshold for spike detection (ms). Frames exceeding this are logged.", (AcceptableValueBase)(object)new AcceptableValueRange(16, 200), Array.Empty())); ReportIntervalSeconds = ((BaseUnityPlugin)this).Config.Bind("General", "ReportIntervalSeconds", 10, new ConfigDescription("Seconds between periodic performance reports.", (AcceptableValueBase)(object)new AcceptableValueRange(5, 60), Array.Empty())); EnablePluginProfiling = ((BaseUnityPlugin)this).Config.Bind("General", "EnablePluginProfiling", false, "Profile each BepInEx plugin's Update/LateUpdate/FixedUpdate callbacks. Disabled by default; turn on only when diagnosing."); EnablePatchProfiling = ((BaseUnityPlugin)this).Config.Bind("General", "EnablePatchProfiling", false, "Profile all Harmony-patched game methods. Disabled by default; turn on only when diagnosing."); TopMethodCount = ((BaseUnityPlugin)this).Config.Bind("General", "TopMethodCount", 10, new ConfigDescription("Number of top slow methods to show in reports.", (AcceptableValueBase)(object)new AcceptableValueRange(3, 30), Array.Empty())); EnableAbuseDetection = ((BaseUnityPlugin)this).Config.Bind("AbuseDetection", "EnableAbuseDetection", true, "Enable network abuse / room bombing detection."); AbuseCheckInterval = ((BaseUnityPlugin)this).Config.Bind("AbuseDetection", "CheckIntervalSeconds", 1f, new ConfigDescription("Seconds between each abuse rate check.", (AcceptableValueBase)(object)new AcceptableValueRange(0.5f, 5f), Array.Empty())); AbuseReportInterval = ((BaseUnityPlugin)this).Config.Bind("AbuseDetection", "ReportIntervalSeconds", 30f, new ConfigDescription("Seconds between periodic abuse summary reports.", (AcceptableValueBase)(object)new AcceptableValueRange(10f, 120f), Array.Empty())); InstantiateRateThreshold = ((BaseUnityPlugin)this).Config.Bind("AbuseDetection", "InstantiateRateThreshold", 15, new ConfigDescription("Max Instantiate calls per second before triggering alert.", (AcceptableValueBase)(object)new AcceptableValueRange(5, 100), Array.Empty())); DestroyRateThreshold = ((BaseUnityPlugin)this).Config.Bind("AbuseDetection", "DestroyRateThreshold", 20, new ConfigDescription("Max Destroy calls per second before triggering alert.", (AcceptableValueBase)(object)new AcceptableValueRange(5, 100), Array.Empty())); RpcRateThreshold = ((BaseUnityPlugin)this).Config.Bind("AbuseDetection", "RpcRateThreshold", 50, new ConfigDescription("Max RPC calls per second before triggering alert.", (AcceptableValueBase)(object)new AcceptableValueRange(10, 200), Array.Empty())); ObjectSpikeThreshold = ((BaseUnityPlugin)this).Config.Bind("AbuseDetection", "ObjectSpikeThreshold", 30, new ConfigDescription("Object count increase per check interval to trigger spike alert.", (AcceptableValueBase)(object)new AcceptableValueRange(5, 100), Array.Empty())); EnableRpcMonitor = ((BaseUnityPlugin)this).Config.Bind("RpcMonitor", "EnableRpcMonitor", true, "Track all network RPC method names and their sources. Low performance overhead (<0.5ms/s for 1000 RPCs/s)."); RpcMonitorTopCount = ((BaseUnityPlugin)this).Config.Bind("RpcMonitor", "TopMethodCount", 10, new ConfigDescription("Number of top RPC methods to show in periodic reports.", (AcceptableValueBase)(object)new AcceptableValueRange(3, 30), Array.Empty())); RpcMonitorWatchPerMethodCapacity = ((BaseUnityPlugin)this).Config.Bind("RpcMonitor", "WatchedRecordPerMethodCapacity", 32, new ConfigDescription("Per-method ring buffer size for watched high-risk RPCs. Each watched method has its own independent buffer so high-frequency methods (e.g. SyncAfflictionsRPC) cannot flood out low-frequency ones (e.g. SendFeedDataRPC).", (AcceptableValueBase)(object)new AcceptableValueRange(8, 256), Array.Empty())); RpcMonitorWatchShowPerMethod = ((BaseUnityPlugin)this).Config.Bind("RpcMonitor", "WatchedShowPerMethod", 6, new ConfigDescription("How many most-recent detailed records to print per watched method in each periodic report.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 50), Array.Empty())); ExtraWatchMethods = ((BaseUnityPlugin)this).Config.Bind("RpcMonitor", "ExtraWatchMethods", "", "Comma-separated extra RPC method names merged into the watch list (e.g. 'MyRPC1,MyRPC2')."); PumpBatchSize = ((BaseUnityPlugin)this).Config.Bind("RpcMonitor", "PumpBatchSize", 32, new ConfigDescription("Max RPC queue items consumed per frame on main thread.", (AcceptableValueBase)(object)new AcceptableValueRange(8, 256), Array.Empty())); MinReportMs = ((BaseUnityPlugin)this).Config.Bind("General", "MinReportMs", 0.1f, new ConfigDescription("Ignore patched methods whose average cost is below this threshold (ms), to reduce profiler overhead.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 5f), Array.Empty())); IgnorePluginGuids = ((BaseUnityPlugin)this).Config.Bind("General", "IgnorePluginGuids", "", "Comma-separated plugin GUIDs to skip in PluginProfiler."); IgnorePatchMethods = ((BaseUnityPlugin)this).Config.Bind("General", "IgnorePatchMethods", "", "Comma-separated full method names (Type.Method) to skip in PatchProfiler."); VerbosityCfg = ((BaseUnityPlugin)this).Config.Bind("Logging", "LogVerbosity", LogVerbosity.Minimal, "Minimal (default) = abuse alerts + their detail lines + monitor init/error/milestones. Normal = Minimal + periodic reports (RPC top/watched, FPS, patch/plugin profiler, frame spike detail)."); MaxLogFileSizeMB = ((BaseUnityPlugin)this).Config.Bind("Logging", "MaxLogFileSizeMB", 10, new ConfigDescription("Rotate structured CSV/JSONL files when they exceed this size.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 100), Array.Empty())); EnableMemoryMonitor = ((BaseUnityPlugin)this).Config.Bind("General", "EnableMemoryMonitor", true, "Sample GC allocation rate and include AllocRateKBps in FpsReport events."); ShowDashboard = ((BaseUnityPlugin)this).Config.Bind("UI", "ShowDashboard", false, "Show draggable on-screen performance dashboard. Zero cost when disabled."); TraceMethodNames = ((BaseUnityPlugin)this).Config.Bind("MethodTracer", "TraceMethodNames", "", "Comma-separated method full names to attach a stack-tracing prefix (e.g. 'Player.Update,ColdComponent.Apply'). Empty disables tracing."); TraceMaxDepth = ((BaseUnityPlugin)this).Config.Bind("MethodTracer", "TraceMaxDepth", 5, new ConfigDescription("Maximum stack frames recorded per trace sample.", (AcceptableValueBase)(object)new AcceptableValueRange(3, 20), Array.Empty())); TraceRateLimit = ((BaseUnityPlugin)this).Config.Bind("MethodTracer", "TraceRateLimit", 100, new ConfigDescription("Max trace records per method per second. Excess samples are dropped.", (AcceptableValueBase)(object)new AcceptableValueRange(10, 1000), Array.Empty())); EnableFieldProbe = ((BaseUnityPlugin)this).Config.Bind("FieldProbe", "EnableFieldProbe", false, "Enable FieldProbe: reflectively snapshot arbitrary fields / parameters / return values at any method via a JSON rules file. Zero overhead when disabled."); FieldProbeRulesFile = ((BaseUnityPlugin)this).Config.Bind("FieldProbe", "RulesFile", "WhySoLaggy.fieldprobe.json", "Path to FieldProbe JSON rules file. Relative path resolves under BepInEx/config/. See sample file for schema."); FieldProbeDefaultRate = ((BaseUnityPlugin)this).Config.Bind("FieldProbe", "DefaultRateLimit", 60, new ConfigDescription("Default max snapshots per rule per second when a rule doesn't specify its own rateLimit.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 10000), Array.Empty())); FieldProbeDefaultMaxLen = ((BaseUnityPlugin)this).Config.Bind("FieldProbe", "DefaultMaxValueLen", 128, new ConfigDescription("Default max string length per snapshot value. Longer values are truncated with '...'.", (AcceptableValueBase)(object)new AcceptableValueRange(16, 4096), Array.Empty())); FieldProbeDefaultStack = ((BaseUnityPlugin)this).Config.Bind("FieldProbe", "DefaultIncludeStack", false, "Default: whether to attach filtered call stack when a rule doesn't specify includeStack."); FieldProbeDefaultStackDepth = ((BaseUnityPlugin)this).Config.Bind("FieldProbe", "DefaultStackMaxDepth", 5, new ConfigDescription("Default max stack frames captured per snapshot when includeStack is enabled.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 30), Array.Empty())); _bepInExDir = Path.GetDirectoryName(Path.GetDirectoryName(typeof(BaseUnityPlugin).Assembly.Location)) ?? Paths.BepInExRootPath; try { LogFilter.Level = VerbosityCfg.Value; } catch (Exception ex) { Log.LogWarning((object)("[WHY_LAG] Failed to read LogVerbosity from cfg (possibly legacy 'Verbose' value): " + ex.Message + ". Falling back to Minimal. Fix: delete com.wuyachiyu.WhySoLaggy.cfg or set LogVerbosity=Minimal/Normal manually.")); LogFilter.Level = LogVerbosity.Minimal; } LagLogger.Initialize(_bepInExDir); AbuseLogger.Initialize(_bepInExDir); StructuredLogger.MaxLogFileSizeMB = MaxLogFileSizeMB.Value; StructuredLogger.Initialize(_bepInExDir); FpsTracker.SpikeThresholdMs = SpikeThresholdMs.Value; FpsTracker.ReportIntervalSeconds = ReportIntervalSeconds.Value; FpsTracker.EnableMemoryMonitor = EnableMemoryMonitor.Value; PluginProfiler.Enabled = EnablePluginProfiling.Value; PatchProfiler.Enabled = EnablePatchProfiling.Value; PatchProfiler.TopMethodCount = TopMethodCount.Value; PatchProfiler.MinReportMs = MinReportMs.Value; PerformanceDashboard.ShowDashboard = ShowDashboard.Value; FillIgnoreSet(IgnorePluginGuids.Value, PluginProfiler.IgnoreGuids); FillIgnoreSet(IgnorePatchMethods.Value, PatchProfiler.IgnoreMethods); _harmony = new Harmony("com.wuyachiyu.WhySoLaggy"); PatchProfiler.OwnHarmonyId = "com.wuyachiyu.WhySoLaggy"; Log.LogInfo((object)"[WHY_LAG] Config bound, loggers initialized"); } private static void FillIgnoreSet(string csv, HashSet set) { if (string.IsNullOrEmpty(csv) || set == null) { return; } string[] array = csv.Split(new char[1] { ',' }); for (int i = 0; i < array.Length; i++) { string text = array[i]?.Trim(); if (!string.IsNullOrEmpty(text)) { set.Add(text); } } } private static string ResolveFieldProbeRulesPath(string raw) { if (string.IsNullOrWhiteSpace(raw)) { return ""; } if (Path.IsPathRooted(raw)) { return raw; } return Path.Combine(Path.Combine(_bepInExDir ?? Paths.BepInExRootPath, "config"), raw); } [IteratorStateMachine(typeof(d__46))] private IEnumerator Start() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__46(0) { <>4__this = this }; } private void Update() { FpsTracker.Tick(); if (EnableAbuseDetection.Value) { NetworkAbuseDetector.Tick(); } if (RpcMonitor.Enabled) { RpcMonitor.PumpQueue(); } if (_profilingActive) { _reportFrameCount++; _reportTimer += Time.unscaledDeltaTime; if (FpsTracker.IsSpikeFrame) { PluginProfiler.WriteSpikeDetail(FpsTracker.CurrentFrameMs); PatchProfiler.WriteSpikeDetail(); } if (_reportTimer >= (float)ReportIntervalSeconds.Value) { LagLogger.Write(new string('-', 60)); PluginProfiler.WriteReport(_reportFrameCount); PatchProfiler.WriteReport(_reportFrameCount); StructuredLogger.Flush(); _reportFrameCount = 0; _reportTimer = 0f; } PluginProfiler.ResetFrameTimers(); PatchProfiler.ResetFrameTimers(); } } private void OnDestroy() { PhotonNetwork.RemoveCallbackTarget((object)this); LagLogger.Shutdown(); AbuseLogger.Shutdown(); StructuredLogger.Shutdown(); } private void OnEnable() { PhotonNetwork.AddCallbackTarget((object)this); } private void OnDisable() { PhotonNetwork.RemoveCallbackTarget((object)this); } public void OnEvent(EventData photonEvent) { if (EnableAbuseDetection.Value) { NetworkAbuseDetector.OnNetworkEvent(photonEvent.Code, photonEvent.Sender); NetworkAbuseDetector.OnRemoteRpcEvent(photonEvent); NetworkAbuseDetector.OnOwnershipEvent(photonEvent); } } private void OnGUI() { if (EnableAbuseDetection.Value) { AbuseNotificationUI.DrawGUI(); } PerformanceDashboard.DrawGUI(); } } }