Decompiled source of WhySoLaggy v1.0.3
WhySoLaggy.dll
Decompiled 3 weeks ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; 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<Notification> _notifications = new List<Notification>(); 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<Step> 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<string, Type> _typeCache = new Dictionary<string, Type>(StringComparer.Ordinal); private static readonly Dictionary<string, MemberReader> _memberCache = new Dictionary<string, MemberReader>(StringComparer.Ordinal); private static readonly object _cacheLock = new object(); public static CompiledExpr Compile(string expr) { CompiledExpr compiledExpr = new CompiledExpr { Source = expr, Steps = new List<Step>() }; 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<MethodBase, List<Rule>> _methodToRules = new Dictionary<MethodBase, List<Rule>>(); private static readonly object _rateLock = new object(); private static readonly Dictionary<int, int> _counter = new Dictionary<int, int>(); private static readonly Dictionary<int, long> _windowStart = new Dictionary<int, long>(); private static readonly HashSet<int> _overflowWarned = new HashSet<int>(); 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<string, object> 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<object> 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<Rule> list2 = new List<Rule>(); foreach (object item in list) { Dictionary<string, object> 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<object> 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<string> list4 = new List<string>(); List<ExpressionEvaluator.CompiledExpr> list5 = new List<ExpressionEvaluator.CompiledExpr>(); 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<Rule>(); _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<string, object> { { "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<char>()); 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<string, object> dictionary = new Dictionary<string, object> { { "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<string> hashSet = new HashSet<string>(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<Patch> list, string kind, string target, HashSet<string> 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<string, object> { { "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<string, int> _counter = new Dictionary<string, int>(StringComparer.Ordinal); private static readonly Dictionary<string, long> _windowStartTicks = new Dictionary<string, long>(StringComparer.Ordinal); private static readonly HashSet<string> _overflowWarnedInWindow = new HashSet<string>(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<string> hashSet = new HashSet<string>(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<string, object> { { "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<char>()); 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<string, object> AsObject(object v) { return v as Dictionary<string, object>; } public static List<object> AsArray(object v) { return v as List<object>; } public static string GetString(Dictionary<string, object> 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<string, object> 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<string, object> 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<string, object> ParseObject(string s, ref int pos) { Dictionary<string, object> dictionary = new Dictionary<string, object>(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<object> ParseArray(string s, ref int pos) { List<object> list = new List<object>(); 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<int, int> _instantiateByActor = new Dictionary<int, int>(); private static readonly Dictionary<int, int> _rpcByActor = new Dictionary<int, int>(); private static readonly Dictionary<int, int> _destroyByActor = new Dictionary<int, int>(); private static readonly Dictionary<int, Dictionary<string, int>> _rpcByActorMethod = new Dictionary<int, Dictionary<string, int>>(); private static readonly Dictionary<int, Dictionary<string, int>> _rpcByActorMethodTotal = new Dictionary<int, Dictionary<string, int>>(); public static int ActorMethodRateThreshold = 20; private static readonly Dictionary<int, int> _ownershipGrabbedByActor = new Dictionary<int, int>(); public static int OwnershipGrabRateThreshold = 10; private static readonly Dictionary<string, int> _prefabCount = new Dictionary<string, int>(); private static readonly Dictionary<string, int> _prefabTraceTaken = new Dictionary<string, int>(); private static float _lastTraceSampleTime; private const int TraceFirstNPerPrefab = 3; private const float TraceSampleWindowSeconds = 5f; private static bool _forceTraceNext; private static readonly LinkedList<ClientRpcRecord> _recentClientRpcs = new LinkedList<ClientRpcRecord>(); 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<string> 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<string, int>(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<string, int>(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<string, object> fields = new Dictionary<string, object> { { "RpcMethod", text }, { "SenderActor", sender }, { "SenderName", text2 ?? "<unknown>" }, { "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<string, object> dictionary = new Dictionary<string, object> { { "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<string, object> dictionary = new Dictionary<string, object> { { "PrefabName", prefabName ?? "<null>" }, { "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) ?? "<unknown>"; } } 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<int, int> dictionary; Dictionary<int, int> dictionary2; Dictionary<int, int> dictionary3; lock (_lock) { num = _localInstantiateCount + _remoteInstantiateCount; num2 = _localDestroyCount + _remoteDestroyCount; num3 = _localRpcCount + _remoteRpcCount; dictionary = new Dictionary<int, int>(_instantiateByActor); dictionary2 = new Dictionary<int, int>(_rpcByActor); dictionary3 = new Dictionary<int, int>(_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<int, int> actorMap) { try { int num = -1; int num2 = 0; if (actorMap != null) { foreach (KeyValuePair<int, int> item in actorMap) { if (item.Value > num2) { num = item.Key; num2 = item.Value; } } } Dictionary<string, object> dictionary = new Dictionary<string, object> { { "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<int, Dictionary<string, int>> item in _rpcByActorMethod) { foreach (KeyValuePair<string, int> 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<string, object> { { "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<int, int> 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<string, object> { { "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<int, PhotonView> 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<string, int> dictionary = new Dictionary<string, int>(); ValueIterator<int, PhotonView> 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<KeyValuePair<string, int>> list = new List<KeyValuePair<string, int>>(dictionary); list.Sort((KeyValuePair<string, int> a, KeyValuePair<string, int> b) => b.Value.CompareTo(a.Value)); foreach (KeyValuePair<string, int> 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<string, object> { { "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<KeyValuePair<string, int>> list = new List<KeyValuePair<string, int>>(_prefabCount); list.Sort((KeyValuePair<string, int> a, KeyValuePair<string, int> b) => b.Value.CompareTo(a.Value)); int num4 = 0; foreach (KeyValuePair<string, int> 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<int, Dictionary<string, int>> item5 in _rpcByActorMethodTotal) { foreach (KeyValuePair<string, int> 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<string, object> dictionary = new Dictionary<string, object> { { "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<int, int> dict, int actorNumber) { if (!dict.ContainsKey(actorNumber)) { dict[actorNumber] = 0; } dict[actorNumber]++; } private static void LogTopActors(Dictionary<int, int> dict, string action) { if (dict.Count == 0) { return; } List<KeyValuePair<int, int>> list = new List<KeyValuePair<int, int>>(dict); list.Sort((KeyValuePair<int, int> a, KeyValuePair<int, int> b) => b.Value.CompareTo(a.Value)); AbuseLogger.AlertDetail("[ABUSE] Top " + action + " sources (by ActorNumber):"); int num = 0; foreach (KeyValuePair<int, int> 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<KeyValuePair<string, int>> list = new List<KeyValuePair<string, int>>(_prefabCount); list.Sort((KeyValuePair<string, int> a, KeyValuePair<string, int> b) => b.Value.CompareTo(a.Value)); AbuseLogger.AlertDetail("[ABUSE] Top prefabs this interval:"); int num = 0; foreach (KeyValuePair<string, int> 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<string> IgnoreMethods = new HashSet<string>(StringComparer.Ordinal); public static string OwnHarmonyId; private static readonly Dictionary<string, MethodTimingData> _timings = new Dictionary<string, MethodTimingData>(); private static readonly Dictionary<string, string> _ownerMap = new Dictionary<string, string>(); private static readonly Dictionary<string, FrameMethodData> _frameTimers = new Dictionary<string, FrameMethodData>(); private static readonly Dictionary<string, int> _skipCounter = new Dictionary<string, int>(); private static bool _initialized; private static int _patchedCount; private static readonly StringBuilder _reportSb = new StringBuilder(1024); private static readonly List<KeyValuePair<string, MethodTimingData>> _reportSorted = new List<KeyValuePair<string, MethodTimingData>>(64); private static readonly List<KeyValuePair<string, FrameMethodData>> _spikeSorted = new List<KeyValuePair<string, FrameMethodData>>(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<MethodBase> 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; } st