Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of LoopLagLimiter v1.2.0
patchers/LoopLagLimiter.Patcher.dll
Decompiled 21 hours agousing System; using System.Collections.Generic; using System.Reflection; using System.Runtime.CompilerServices; using BepInEx.Logging; using Mono.Cecil; using Mono.Collections.Generic; [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: AssemblyVersion("0.0.0.0")] namespace LoopLagLimiter.Patcher; public static class LoopLagLimiterPatcher { private static readonly string[][] targets = new string[7][] { new string[2] { "RoR2.CharacterMaster", "GiveMoney" }, new string[2] { "RoR2.CharacterMaster", "set_money" }, new string[2] { "RoR2.Run", "GetDifficultyScaledCost" }, new string[2] { "RoR2.PurchaseInteraction", "set_Networkcost" }, new string[2] { "RoR2.PurchaseInteraction", "set_cost" }, new string[2] { "RoR2.PurchaseInteraction", "SetCost" }, new string[2] { "RoR2.PickupDropletController", "CreatePickupDroplet" } }; public static IEnumerable<string> TargetDLLs { get; } = new string[1] { "RoR2.dll" }; public static void Patch(AssemblyDefinition assembly) { //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) ManualLogSource val = Logger.CreateLogSource("LoopLagLimiter.Patcher"); int num = 0; string[][] array = targets; foreach (string[] array2 in array) { try { TypeDefinition type = assembly.MainModule.GetType(array2[0]); if (type == null) { continue; } Enumerator<MethodDefinition> enumerator = type.Methods.GetEnumerator(); try { while (enumerator.MoveNext()) { MethodDefinition current = enumerator.Current; if (((MemberReference)current).Name == array2[1] && !current.NoInlining) { current.NoInlining = true; num++; } } } finally { ((IDisposable)enumerator/*cast due to .constrained prefix*/).Dispose(); } } catch (Exception ex) { val.LogWarning((object)("Could not mark " + array2[0] + "." + array2[1] + ": " + ex.Message)); } } val.LogInfo((object)("Disabled inlining for " + num + " method(s) so LoopLagLimiter's money, price and item-drop hooks always run.")); Logger.Sources.Remove((ILogSource)(object)val); } }
plugins/LoopLagLimiter.dll
Decompiled 21 hours 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.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Threading; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using UnityEngine; using UnityEngine.SceneManagement; [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: AssemblyVersion("0.0.0.0")] namespace LoopLagLimiter; internal static class ChainGuard { private sealed class Pending { public MethodBase Method; public object Instance; public object[] Args; } private static readonly Dictionary<MethodBase, int> depth = new Dictionary<MethodBase, int>(); private static readonly Queue<Pending> queue = new Queue<Pending>(); public static int Deferred; public static int Dropped; public static void Install(Harmony h) { //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Expected O, but got Unknown //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Expected O, but got Unknown Type type = TypeFinder.Find("RoR2.GlobalEventManager"); if (type == null) { Plugin.Log.LogWarning((object)"Chain guard skipped: GlobalEventManager not found"); return; } HarmonyMethod val = new HarmonyMethod(typeof(ChainGuard).GetMethod("Prefix", BindingFlags.Static | BindingFlags.NonPublic)); val.priority = 800; HarmonyMethod val2 = new HarmonyMethod(typeof(ChainGuard).GetMethod("Finalizer", BindingFlags.Static | BindingFlags.NonPublic)); int num = 0; MethodInfo[] methods = type.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo in methods) { if (!(methodInfo.Name != "ProcIgniteOnKill") && !(methodInfo.ReturnType != typeof(void)) && !methodInfo.ContainsGenericParameters) { try { MethodInfo methodInfo2 = methodInfo; HarmonyMethod val3 = val; HarmonyMethod val4 = val2; h.Patch((MethodBase)methodInfo2, val3, (HarmonyMethod)null, (HarmonyMethod)null, val4, (HarmonyMethod)null); depth[methodInfo] = 0; num++; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Chain guard could not hook " + methodInfo.Name + ": " + ex.Message)); } } } Plugin.Log.LogInfo((object)((num <= 0) ? "Chain guard: ProcIgniteOnKill not found, skipped." : "Chain guard hooked GlobalEventManager.ProcIgniteOnKill (Gasoline).")); } private static bool Prefix(MethodBase __originalMethod, object __instance, object[] __args, out bool __state) { __state = false; try { int value = Plugin.MaxOnKillChainDepth.Value; depth.TryGetValue(__originalMethod, out var value2); if (value > 0 && value2 >= value) { if (queue.Count < 5000) { queue.Enqueue(new Pending { Method = __originalMethod, Instance = __instance, Args = (object[])__args.Clone() }); Deferred++; Limiter.CountPublic("Gasoline chain deferred to next tick"); } else { Dropped++; Limiter.CountPublic("Gasoline chain proc dropped (queue full)"); } return false; } depth[__originalMethod] = value2 + 1; __state = true; } catch { } return true; } private static Exception Finalizer(Exception __exception, MethodBase __originalMethod, bool __state) { if (__state && depth.TryGetValue(__originalMethod, out var value)) { depth[__originalMethod] = Math.Max(0, value - 1); } return __exception; } public static void Drain() { if (queue.Count == 0) { return; } int val = Math.Max(1, Plugin.MaxDeferredProcsPerTick.Value); int num = Math.Min(val, queue.Count); for (int i = 0; i < num; i++) { Pending pending = queue.Dequeue(); try { Object val2 = (Object)((pending.Args.Length <= 2) ? null : /*isinst with value type is only supported in some contexts*/); if (object.ReferenceEquals(val2, null) || Object.op_Implicit(val2)) { pending.Method.Invoke(pending.Instance, pending.Args); } } catch (Exception ex) { Plugin.Log.LogDebug((object)("Deferred proc failed: " + (ex.InnerException ?? ex).Message)); } } } public static void OnSceneChange() { queue.Clear(); foreach (MethodBase item in new List<MethodBase>(depth.Keys)) { depth[item] = 0; } } } internal static class Deep { private static Harmony h; private static readonly Dictionary<MethodBase, string> keyOf = new Dictionary<MethodBase, string>(); private static readonly HashSet<MethodBase> esmMethods = new HashSet<MethodBase>(); private static readonly HashSet<MethodBase> splitMethods = new HashSet<MethodBase>(); private static FieldInfo esmStateField; private static readonly string[] skipPrefixes = new string[15] { "UnityEngine", "Unity.", "System", "mscorlib", "netstandard", "Mono.", "MonoMod", "BepInEx", "0Harmony", "HarmonyX", "MMHOOK", "Microsoft", "Newtonsoft", "Rewired", "LoopLagLimiter" }; private static readonly string[] updateNames = new string[3] { "Update", "FixedUpdate", "LateUpdate" }; public static bool Enabled { get; private set; } public static void Enable() { //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Expected O, but got Unknown //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Expected O, but got Unknown //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Expected O, but got Unknown if (Enabled) { return; } Stopwatch stopwatch = Stopwatch.StartNew(); if (h == null) { h = new Harmony("com.Videowiz92.LoopLagLimiter.deep"); } HarmonyMethod val = new HarmonyMethod(typeof(Deep).GetMethod("Prefix", BindingFlags.Static | BindingFlags.NonPublic)); HarmonyMethod val2 = new HarmonyMethod(typeof(Deep).GetMethod("Finalizer", BindingFlags.Static | BindingFlags.NonPublic)); Type type = TypeFinder.Find("RoR2.EntityStateMachine"); if (type != null) { esmStateField = AccessTools.Field(type, "state"); } HashSet<string> hashSet = new HashSet<string>(); string[] array = (Plugin.DeepSplitTypes.Value ?? string.Empty).Split(';'); foreach (string text in array) { if (text.Trim().Length > 0) { hashSet.Add(text.Trim()); } } int num = 0; int num2 = 0; Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { string name = assembly.GetName().Name; if (assembly.IsDynamic || Skip(name)) { continue; } foreach (Type item in SafeTypes(assembly)) { if (item == null || item.IsAbstract || item.ContainsGenericParameters || !typeof(MonoBehaviour).IsAssignableFrom(item)) { continue; } string[] array2 = updateNames; foreach (string text2 in array2) { MethodInfo method; try { method = item.GetMethod(text2, BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null); } catch { continue; } if (method == null || method.IsAbstract || method.ReturnType != typeof(void)) { continue; } try { if (method.GetMethodBody() != null) { Harmony obj2 = h; MethodInfo methodInfo = method; HarmonyMethod val3 = val; HarmonyMethod val4 = val2; obj2.Patch((MethodBase)methodInfo, val3, (HarmonyMethod)null, (HarmonyMethod)null, val4, (HarmonyMethod)null); string value = ((!(name == "RoR2")) ? ("[" + name + "] ") : string.Empty) + item.FullName + "." + text2; keyOf[method] = value; if (item == type) { esmMethods.Add(method); } if (hashSet.Contains(item.FullName) || hashSet.Contains(item.Name)) { splitMethods.Add(method); } num++; } } catch { num2++; } } } } Enabled = true; Plugin.Log.LogWarning((object)$"LoopLagLimiter deep profile ON: {num} update methods patched ({num2} skipped) in {stopwatch.ElapsedMilliseconds} ms."); } public static void Disable() { if (Enabled) { try { h.UnpatchSelf(); } catch (Exception ex) { Plugin.Log.LogError((object)ex); } keyOf.Clear(); esmMethods.Clear(); splitMethods.Clear(); Enabled = false; Plugin.Log.LogWarning((object)"LoopLagLimiter deep profile OFF."); } } private static bool Skip(string asmName) { string[] array = skipPrefixes; foreach (string value in array) { if (asmName.StartsWith(value, StringComparison.Ordinal)) { return true; } } return false; } private static IEnumerable<Type> SafeTypes(Assembly a) { try { return a.GetTypes(); } catch (ReflectionTypeLoadException ex) { return ex.Types; } catch { return new Type[0]; } } private static void Prefix(out long __state) { __state = ((!Plugin.Reporting) ? Stopwatch.GetTimestamp() : (-1)); } private static Exception Finalizer(Exception __exception, MethodBase __originalMethod, object __instance, long __state) { if (__state < 0) { return __exception; } long dt = Stopwatch.GetTimestamp() - __state; if (!keyOf.TryGetValue(__originalMethod, out var value)) { return __exception; } if (esmStateField != null && esmMethods.Contains(__originalMethod) && __instance != null) { try { object value2 = esmStateField.GetValue(__instance); value = value + " [" + ((value2 != null) ? value2.GetType().FullName : "null") + "]"; } catch { } } else if (splitMethods.Contains(__originalMethod)) { try { value = value + " {" + Probes.ObjName(__instance) + "}"; } catch { } } try { Add(Frame.Current.Deep, value, dt); Add(Frame.Window.Deep, value, dt); } catch { } return __exception; } private static void Add(Dictionary<string, SrcStat> d, string key, long dt) { if (!d.TryGetValue(key, out var value)) { value = (d[key] = new SrcStat()); } value.Calls++; value.Ticks += dt; } } internal static class Fixes { private static readonly Dictionary<MethodBase, int> suppressed = new Dictionary<MethodBase, int>(); public static readonly Dictionary<string, int> Counts = new Dictionary<string, int>(); public static void Install(Harmony h, string list) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Expected O, but got Unknown //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Expected O, but got Unknown HarmonyMethod val = new HarmonyMethod(typeof(Fixes).GetMethod("FinalizerVoid", BindingFlags.Static | BindingFlags.NonPublic)); HarmonyMethod val2 = new HarmonyMethod(typeof(Fixes).GetMethod("FinalizerBool", BindingFlags.Static | BindingFlags.NonPublic)); string[] array = (list ?? string.Empty).Split(';'); foreach (string text in array) { string text2 = text.Trim(); if (text2.Length == 0) { continue; } int num = text2.LastIndexOf(':'); if (num <= 0) { Plugin.Log.LogWarning((object)("Bad guard entry (use Type:Method): " + text2)); continue; } Type type = TypeFinder.Find(text2.Substring(0, num)); string text3 = text2.Substring(num + 1); if (type == null) { Plugin.Log.LogWarning((object)("Guard skipped, type not found: " + text2)); continue; } int num2 = 0; MethodInfo[] methods = type.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo in methods) { if (methodInfo.Name != text3 || methodInfo.IsAbstract || methodInfo.ContainsGenericParameters) { continue; } HarmonyMethod val3 = ((methodInfo.ReturnType == typeof(void)) ? val : ((!(methodInfo.ReturnType == typeof(bool))) ? null : val2)); if (val3 == null) { Plugin.Log.LogWarning((object)("Guard skipped (returns " + methodInfo.ReturnType.Name + "): " + text2)); continue; } try { MethodInfo methodInfo2 = methodInfo; HarmonyMethod val4 = val3; h.Patch((MethodBase)methodInfo2, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, val4, (HarmonyMethod)null); suppressed[methodInfo] = 0; num2++; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not guard " + text2 + ": " + ex.Message)); } } if (num2 > 0) { Plugin.Log.LogInfo((object)("Guarding " + text2 + " against NullReferenceException (" + num2 + " overload(s)).")); } else { Plugin.Log.LogWarning((object)("Guard skipped, method not found: " + text2)); } } } private static Exception Handle(Exception ex, MethodBase original) { if (!(ex is NullReferenceException)) { return ex; } string text = original.DeclaringType.Name + "." + original.Name; Counts.TryGetValue(text, out var value); Counts[text] = value + 1; if (value == 0) { Plugin.Log.LogWarning((object)("LoopLagLimiter suppressed a NullReferenceException in " + text + " (further ones are counted silently and listed in reports). First one:\n" + ex)); } return null; } private static Exception FinalizerVoid(Exception __exception, MethodBase __originalMethod) { return (__exception != null) ? Handle(__exception, __originalMethod) : null; } private static Exception FinalizerBool(Exception __exception, MethodBase __originalMethod, ref bool __result) { if (__exception == null) { return null; } Exception ex = Handle(__exception, __originalMethod); if (ex == null) { __result = false; } return ex; } } internal static class Limiter { internal sealed class ProjectileTracker : MonoBehaviour { public string Key; private bool counted; public void Begin(string key) { Key = key; Count(alive: true); } private void OnEnable() { if (Key != null) { Count(alive: true); } } private void OnDisable() { Count(alive: false); } private void Count(bool alive) { if (Key != null && alive != counted) { counted = alive; AliveProjectiles.TryGetValue(Key, out var value); AliveProjectiles[Key] = Math.Max(0, value + (alive ? 1 : (-1))); TotalAliveProjectiles = Math.Max(0, TotalAliveProjectiles + (alive ? 1 : (-1))); } } } public static readonly Dictionary<string, int> Counts = new Dictionary<string, int>(); private static readonly Dictionary<string, LinkedList<GameObject>> forces = new Dictionary<string, LinkedList<GameObject>>(); public static readonly Dictionary<string, int> AliveProjectiles = new Dictionary<string, int>(); private static PropertyInfo serverActive; private static MethodInfo serverDestroy; private static Type netIdentityType; private static FieldInfo[] orbLists; private static int orbSweepTick; private static FieldInfo creditField; private static readonly string[] livingTypes = new string[5] { "RoR2.CharacterBody", "RoR2.CharacterMaster", "RoR2.HealthComponent", "RoR2.Deployable", "RoR2.TeamComponent" }; private static Type[] livingTypeCache; private static readonly Dictionary<string, bool> spawnsSomething = new Dictionary<string, bool>(); private static bool moneyResolved; private static Func<object, uint> getMoney; private static Action<object, uint> setMoney; private static PropertyInfo masterList; private static int moneySweepTick; private static readonly Dictionary<Type, int> orbCounts = new Dictionary<Type, int>(); private static readonly Dictionary<Type, int> orbScratch = new Dictionary<Type, int>(); private static readonly Dictionary<string, int> inFlight = new Dictionary<string, int>(); private static int inFlightFrame = -1; private static int inFlightTotal; public static int TotalAliveProjectiles; private static float inFlightFixed = -1f; private static readonly HashSet<string> reported = new HashSet<string>(); public static void Install(Harmony h) { //IL_0324: Unknown result type (might be due to invalid IL or missing references) //IL_032b: Expected O, but got Unknown //IL_0406: Unknown result type (might be due to invalid IL or missing references) //IL_040d: Expected O, but got Unknown //IL_02c6: Unknown result type (might be due to invalid IL or missing references) //IL_02d4: Expected O, but got Unknown //IL_054a: Unknown result type (might be due to invalid IL or missing references) //IL_0551: Expected O, but got Unknown //IL_020f: Unknown result type (might be due to invalid IL or missing references) //IL_021d: Expected O, but got Unknown Type type = TypeFinder.Find("UnityEngine.Networking.NetworkServer"); if (type != null) { serverActive = AccessTools.Property(type, "active"); serverDestroy = AccessTools.Method(type, "Destroy", new Type[1] { typeof(GameObject) }, (Type[])null); } netIdentityType = TypeFinder.Find("UnityEngine.Networking.NetworkIdentity"); PatchFirst(h, "RoR2.RadialForce", new string[3] { "Start", "OnEnable", "Awake" }, null, "RadialForcePostfix"); Type type2 = TypeFinder.Find("RoR2.Orbs.OrbManager"); if (type2 != null) { List<FieldInfo> list = new List<FieldInfo>(); FieldInfo[] fields = type2.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo fieldInfo in fields) { if (typeof(IList).IsAssignableFrom(fieldInfo.FieldType)) { list.Add(fieldInfo); } } orbLists = list.ToArray(); PatchFirst(h, "RoR2.Orbs.OrbManager", new string[1] { "FixedUpdate" }, "OrbManagerPrefix", null); } Type type3 = TypeFinder.Find("RoR2.CombatDirector"); if (type3 != null) { creditField = AccessTools.Field(type3, "monsterCredit"); if (creditField != null && creditField.FieldType == typeof(float)) { PatchFirst(h, "RoR2.CombatDirector", new string[1] { "FixedUpdate" }, "DirectorPrefix", null); } else { Plugin.Log.LogWarning((object)"Camp credit cap skipped: CombatDirector.monsterCredit not found"); } } Type type4 = TypeFinder.Find("RoR2.CharacterMaster"); MethodInfo methodInfo = ((!(type4 == null)) ? type4.GetMethod("GiveMoney", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(uint) }, null) : null); if (methodInfo != null) { try { h.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(Limiter).GetMethod("GiveMoneyPrefix", BindingFlags.Static | BindingFlags.NonPublic)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Plugin.Log.LogInfo((object)"Money overflow guard hooked CharacterMaster.GiveMoney."); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Money overflow guard: " + ex.Message)); } } else { Plugin.Log.LogWarning((object)"Money overflow guard skipped: CharacterMaster.GiveMoney(uint) not found"); } MethodInfo methodInfo2 = ((!(type4 == null)) ? type4.GetMethod("set_money", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(uint) }, null) : null); if (methodInfo2 != null) { try { h.Patch((MethodBase)methodInfo2, new HarmonyMethod(typeof(Limiter).GetMethod("MoneySetterPrefix", BindingFlags.Static | BindingFlags.NonPublic)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Plugin.Log.LogInfo((object)"Money overflow guard hooked CharacterMaster.money setter."); } catch (Exception ex2) { Plugin.Log.LogWarning((object)("Money setter guard: " + ex2.Message)); } } int num = 0; HarmonyMethod val = new HarmonyMethod(typeof(Limiter).GetMethod("ScaledCostPostfix", BindingFlags.Static | BindingFlags.NonPublic)); Type type5 = TypeFinder.Find("RoR2.Run"); if (type5 != null) { MethodInfo[] methods = type5.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo3 in methods) { if (methodInfo3.Name == "GetDifficultyScaledCost" && methodInfo3.ReturnType == typeof(int) && !methodInfo3.ContainsGenericParameters) { try { MethodInfo methodInfo4 = methodInfo3; HarmonyMethod val2 = val; h.Patch((MethodBase)methodInfo4, (HarmonyMethod)null, val2, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); num++; } catch (Exception ex3) { Plugin.Log.LogWarning((object)("Price guard: " + ex3.Message)); } } } } HarmonyMethod val3 = new HarmonyMethod(typeof(Limiter).GetMethod("CostSetterPrefix", BindingFlags.Static | BindingFlags.NonPublic)); Type type6 = TypeFinder.Find("RoR2.PurchaseInteraction"); if (type6 != null) { string[] array = new string[3] { "set_Networkcost", "set_cost", "SetCost" }; foreach (string name in array) { MethodInfo method = type6.GetMethod(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(int) }, null); if (method != null) { try { h.Patch((MethodBase)method, val3, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); num++; } catch (Exception ex4) { Plugin.Log.LogWarning((object)("Price guard: " + ex4.Message)); } } } } if (num > 0) { Plugin.Log.LogInfo((object)("Price overflow guard hooked " + num + " method(s).")); } else { Plugin.Log.LogWarning((object)"Price overflow guard skipped: no cost methods found"); } PatchFirst(h, "RoR2.Projectile.ProjectileController", new string[2] { "Start", "Awake" }, null, "ProjectileStartPostfix"); HarmonyMethod val4 = new HarmonyMethod(typeof(Limiter).GetMethod("FirePrefix", BindingFlags.Static | BindingFlags.NonPublic)); val4.priority = 600; foreach (MethodInfo item in ProjectileFireMethods()) { try { h.Patch((MethodBase)item, val4, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } catch (Exception ex5) { Plugin.Log.LogWarning((object)("Projectile cap: " + ex5.Message)); } } } private static void PatchFirst(Harmony h, string typeName, string[] names, string prefix, string postfix) { //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) Type type = TypeFinder.Find(typeName); if (type == null) { Plugin.Log.LogWarning((object)("Limiter skipped, type not found: " + typeName)); return; } foreach (string text in names) { MethodInfo methodInfo = AccessTools.DeclaredMethod(type, text, Type.EmptyTypes, (Type[])null); if (!(methodInfo == null)) { try { h.Patch((MethodBase)methodInfo, (prefix == null) ? ((HarmonyMethod)null) : new HarmonyMethod(typeof(Limiter).GetMethod(prefix, BindingFlags.Static | BindingFlags.NonPublic)), (postfix == null) ? ((HarmonyMethod)null) : new HarmonyMethod(typeof(Limiter).GetMethod(postfix, BindingFlags.Static | BindingFlags.NonPublic)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Plugin.Log.LogInfo((object)("Limiter hooked " + typeName + "." + text)); return; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Limiter could not hook " + typeName + "." + text + ": " + ex.Message)); return; } } } Plugin.Log.LogWarning((object)("Limiter skipped, no hookable method on " + typeName)); } private static void DirectorPrefix(Component __instance) { try { if (creditField == null || object.ReferenceEquals(__instance, null) || !IsServer()) { return; } float num = (float)creditField.GetValue(__instance); float num2 = Plugin.MaxCampDirectorCredits.Value; if (num2 <= 0f || num <= num2) { return; } string text = ((Object)__instance.gameObject).name ?? string.Empty; bool flag = Plugin.CapHalcyonWaveDirector.Value && text.StartsWith("Activation and Tier Change Wave", StringComparison.OrdinalIgnoreCase); if (!flag) { string[] array = (Plugin.CampDirectorNames.Value ?? string.Empty).Split(';'); foreach (string text2 in array) { if (text2.Trim().Length > 0 && text.StartsWith(text2.Trim(), StringComparison.OrdinalIgnoreCase)) { flag = true; break; } } } if (flag) { creditField.SetValue(__instance, num2); Count("Camp director credits capped (" + text + ")"); Plugin.Log.LogInfo((object)$"LoopLagLimiter capped '{text}' monster credits from {num:0} to {num2:0}."); } } catch (Exception e) { OnceError("Camp credit cap", e); } } internal static List<MethodInfo> ProjectileFireMethods() { List<MethodInfo> list = new List<MethodInfo>(); Type type = TypeFinder.Find("RoR2.Projectile.ProjectileManager"); if (type == null) { return list; } MethodInfo[] methods = type.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); MethodInfo[] array = methods; foreach (MethodInfo methodInfo in array) { if (methodInfo.Name == "FireProjectileServer" && methodInfo.ReturnType == typeof(void) && !methodInfo.ContainsGenericParameters) { list.Add(methodInfo); } } if (list.Count == 0) { MethodInfo[] array2 = methods; foreach (MethodInfo methodInfo2 in array2) { if (methodInfo2.Name == "FireProjectile" && methodInfo2.ReturnType == typeof(void) && !methodInfo2.ContainsGenericParameters) { list.Add(methodInfo2); } } } return list; } internal static void CountPublic(string key) { Count(key); } internal static void KillPublic(GameObject go) { Kill(go); } internal static bool TryGetMoney(object master, out uint money) { money = 0u; if (master == null || !ResolveMoney(master.GetType())) { return false; } try { money = getMoney(master); return true; } catch { return false; } } private static void Count(string key) { Counts.TryGetValue(key, out var value); Counts[key] = value + 1; } internal static bool IsServer() { try { return serverActive == null || (bool)serverActive.GetValue(null, null); } catch { return true; } } private static void Kill(GameObject go) { if (!object.ReferenceEquals(go, null) && Object.op_Implicit((Object)(object)go)) { bool flag = false; if (netIdentityType != null) { Component component = go.GetComponent(netIdentityType); flag = !object.ReferenceEquals(component, null) && Object.op_Implicit((Object)(object)component); } if (flag && serverDestroy != null) { serverDestroy.Invoke(null, new object[1] { go }); } else { Object.Destroy((Object)(object)go); } } } private static void RadialForcePostfix(Component __instance) { try { if (!IsServer() || object.ReferenceEquals(__instance, null) || !Object.op_Implicit((Object)(object)__instance)) { return; } GameObject gameObject = __instance.gameObject; if (!IsFreeStandingEffect(gameObject)) { return; } string text = Probes.Clean(((Object)gameObject).name); if (!forces.TryGetValue(text, out var value)) { value = new LinkedList<GameObject>(); forces[text] = value; } LinkedListNode<GameObject> linkedListNode = value.First; while (linkedListNode != null) { LinkedListNode<GameObject> next = linkedListNode.Next; if (!Object.op_Implicit((Object)(object)linkedListNode.Value)) { value.Remove(linkedListNode); } linkedListNode = next; } if (value.Contains(gameObject)) { return; } value.AddLast(gameObject); int value2 = Plugin.MaxRadialForcesPerPrefab.Value; if (value2 > 0) { while (value.Count > value2) { GameObject value3 = value.First.Value; value.RemoveFirst(); Kill(value3); Count("RadialForce removed (" + text + ")"); } } } catch (Exception e) { OnceError("RadialForce cap", e); } } private static Type[] LivingTypes() { if (livingTypeCache == null) { List<Type> list = new List<Type>(); string[] array = livingTypes; foreach (string fullName in array) { Type type = TypeFinder.Find(fullName); if (type != null) { list.Add(type); } } livingTypeCache = list.ToArray(); } return livingTypeCache; } internal static bool IsFreeStandingEffect(GameObject go) { if (object.ReferenceEquals(go, null) || !Object.op_Implicit((Object)(object)go)) { return false; } Transform transform = go.transform; if (!object.ReferenceEquals(transform, null) && !object.ReferenceEquals(transform.parent, null)) { return false; } Type[] array = LivingTypes(); foreach (Type type in array) { Component component = go.GetComponent(type); if (!object.ReferenceEquals(component, null) && Object.op_Implicit((Object)(object)component)) { return false; } } return true; } internal static bool SpawnsAllies(GameObject prefab, string key) { if (spawnsSomething.TryGetValue(key, out var value)) { return value; } value = false; string text = null; try { Transform transform = prefab.transform; Component[] array = ((!object.ReferenceEquals(transform, null)) ? ((Component)transform).GetComponents(typeof(Component)) : null); if (array != null) { Component[] array2 = array; foreach (Component val in array2) { if (!object.ReferenceEquals(val, null)) { string name = ((object)val).GetType().Name; if (!Has(name, "Deployable") && (Has(name, "Master") || Has(name, "Minion") || Has(name, "Summon") || Has(name, "Clone") || (Has(name, "Spawn") && (Has(name, "Body") || Has(name, "Character") || Has(name, "Ally"))))) { value = true; text = "component " + name; break; } } } } if (!value && (Has(key, "Clone") || Has(key, "Summon") || Has(key, "Minion"))) { value = true; text = "name"; } } catch { value = true; text = "error while checking (if in doubt, never block)"; } spawnsSomething[key] = value; if (value) { Plugin.Log.LogInfo((object)("Projectile '" + key + "' spawns a character (" + text + "); projectile caps and limits will never block it.")); } return value; } private static bool Has(string s, string part) { return s.IndexOf(part, StringComparison.OrdinalIgnoreCase) >= 0; } private static void ScaledCostPostfix(ref int __result) { if (__result < 0 && Plugin.PreventPriceOverflow.Value) { __result = int.MaxValue; Count("Price overflow fixed (scaled cost)"); } } private static void CostSetterPrefix(ref int __0) { if (__0 < 0 && Plugin.PreventPriceOverflow.Value) { __0 = int.MaxValue; Count("Price overflow fixed (cost set)"); } } private static bool ResolveMoney(Type t) { if (moneyResolved) { return getMoney != null; } moneyResolved = true; PropertyInfo p = AccessTools.Property(t, "money"); if (p != null && p.PropertyType == typeof(uint) && p.CanRead && p.CanWrite) { getMoney = (object o) => (uint)p.GetValue(o, null); setMoney = delegate(object o, uint v) { p.SetValue(o, v, null); }; } else { FieldInfo f = AccessTools.Field(t, "_money") ?? AccessTools.Field(t, "money"); if (f != null && f.FieldType == typeof(uint)) { getMoney = (object o) => (uint)f.GetValue(o); setMoney = delegate(object o, uint v) { f.SetValue(o, v); }; } } Plugin.Log.LogInfo((object)((getMoney == null) ? "Money guard: CharacterMaster money (uint) not found; guard disabled." : ("Money guard: using CharacterMaster money (" + ((!(p != null)) ? "field" : "property") + ")."))); return getMoney != null; } private static bool GiveMoneyPrefix(object __instance, uint __0) { try { if (!Plugin.PreventMoneyOverflow.Value || __instance == null || !ResolveMoney(__instance.GetType())) { return true; } uint num = getMoney(__instance); if ((ulong)((long)num + (long)__0) <= 2147483647uL) { return true; } if (num < int.MaxValue) { setMoney(__instance, 2147483647u); } Count("Money gain stopped at the maximum (overflow prevented)"); return false; } catch { return true; } } private static void MoneySetterPrefix(ref uint __0) { if (__0 > int.MaxValue && Plugin.PreventMoneyOverflow.Value) { __0 = 2147483647u; Count("Money clamped to the maximum (setter)"); } } internal static void MoneySweep() { if (!Plugin.PreventMoneyOverflow.Value || ++moneySweepTick % 10 != 0 || !IsServer()) { return; } try { if (masterList == null) { Type type = TypeFinder.Find("RoR2.CharacterMaster"); if (type == null || !ResolveMoney(type)) { return; } masterList = type.GetProperty("readOnlyInstancesList", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (masterList == null) { return; } } if (!(masterList.GetValue(null, null) is IEnumerable enumerable)) { return; } foreach (object item in enumerable) { if (item != null && getMoney(item) > int.MaxValue) { setMoney(item, 2147483647u); Count("Money clamped to the maximum (backstop)"); } } } catch (Exception e) { OnceError("Money sweep", e); } } internal static bool OrbAllowed(Type t) { int value = Plugin.MaxAliveOrbsPerType.Value; if (value <= 0 || t == null) { return true; } orbCounts.TryGetValue(t, out var value2); if (value2 >= value) { Count("Orb not created, cap reached (" + t.Name + ")"); return false; } orbCounts[t] = value2 + 1; return true; } private static void RecountOrbs(object manager) { orbCounts.Clear(); FieldInfo[] array = orbLists; foreach (FieldInfo fieldInfo in array) { if (!(fieldInfo.GetValue(manager) is IList list)) { continue; } orbScratch.Clear(); for (int j = 0; j < list.Count; j++) { object obj = list[j]; if (obj != null) { Type type = obj.GetType(); orbScratch.TryGetValue(type, out var value); orbScratch[type] = value + 1; } } foreach (KeyValuePair<Type, int> item in orbScratch) { orbCounts.TryGetValue(item.Key, out var value2); if (item.Value > value2) { orbCounts[item.Key] = item.Value; } } } } private static void OrbManagerPrefix(object __instance) { if (orbLists == null || __instance == null) { return; } try { if (Plugin.MaxAliveOrbsPerType.Value > 0) { RecountOrbs(__instance); } } catch (Exception e) { OnceError("Orb count", e); } if (!Plugin.DropDeadTargetOrbs.Value || ++orbSweepTick % 5 != 0) { return; } try { FieldInfo[] array = orbLists; foreach (FieldInfo fieldInfo in array) { if (!(fieldInfo.GetValue(__instance) is IList list)) { continue; } for (int num = list.Count - 1; num >= 0; num--) { object obj = list[num]; if (obj != null) { object obj2 = Probes.Member(obj, "target"); Object val = (Object)((obj2 is Object) ? obj2 : null); if (!object.ReferenceEquals(val, null) && !Object.op_Implicit(val)) { list.RemoveAt(num); Count("Orb dropped, target gone (" + obj.GetType().Name + ")"); } } } } } catch (Exception e2) { OnceError("Orb sweep", e2); } } private static void ProjectileStartPostfix(Component __instance) { try { if (object.ReferenceEquals(__instance, null) || !Object.op_Implicit((Object)(object)__instance)) { return; } GameObject gameObject = __instance.gameObject; if (object.ReferenceEquals(gameObject.GetComponent(typeof(ProjectileTracker)), null)) { ProjectileTracker projectileTracker = gameObject.AddComponent<ProjectileTracker>(); if (!object.ReferenceEquals(projectileTracker, null)) { projectileTracker.Begin(Probes.Clean(((Object)gameObject).name)); } } } catch (Exception e) { OnceError("Projectile tracker", e); } } private static bool FirePrefix(object[] __args) { try { if (!IsServer()) { return true; } object obj = ((__args == null || __args.Length <= 0) ? null : __args[0]); object obj2 = ((!(obj is GameObject)) ? Probes.Member(obj, "projectilePrefab") : obj); GameObject val = (GameObject)((obj2 is GameObject) ? obj2 : null); if (object.ReferenceEquals(val, null) || !Object.op_Implicit((Object)(object)val)) { return true; } string text = Probes.Clean(((Object)val).name); if (LoadManager.IsMaulingRock(val, text) && !LoadManager.RockAllowed()) { return false; } if (Plugin.MaxAliveProjectilesPerPrefab.Value <= 0 && Plugin.MaxFiredPerKindPerTick.Value <= 0 && Plugin.MaxAliveProjectilesTotal.Value <= 0) { return true; } if (SpawnsAllies(val, text)) { return true; } int frameCount = Time.frameCount; float fixedTime = Time.fixedTime; if (frameCount != inFlightFrame || fixedTime != inFlightFixed) { inFlight.Clear(); inFlightTotal = 0; inFlightFrame = frameCount; inFlightFixed = fixedTime; } AliveProjectiles.TryGetValue(text, out var value); inFlight.TryGetValue(text, out var value2); int value3 = Plugin.MaxFiredPerKindPerTick.Value; if (value3 > 0 && value2 >= value3) { Count("Projectile not fired, per-tick fire cap reached (" + text + ")"); return false; } int value4 = Plugin.MaxAliveProjectilesPerPrefab.Value; if (value4 > 0 && value + value2 >= value4) { Count("Projectile not fired, cap reached (" + text + ")"); return false; } int value5 = Plugin.MaxAliveProjectilesTotal.Value; if (value5 > 0 && TotalAliveProjectiles + inFlightTotal >= value5 && value + value2 >= Math.Max(1, value5 / 10)) { Count("Projectile not fired, total cap reached (" + text + ")"); return false; } inFlight[text] = value2 + 1; inFlightTotal++; } catch (Exception e) { OnceError("Projectile cap", e); } return true; } private static void OnceError(string what, Exception e) { if (reported.Add(what)) { Plugin.Log.LogError((object)("LoopLagLimiter " + what + " failed (reported once): " + e)); } } public static void OnSceneChange() { orbCounts.Clear(); inFlight.Clear(); inFlightTotal = 0; forces.Clear(); AliveProjectiles.Clear(); TotalAliveProjectiles = 0; } } internal static class LoadManager { internal sealed class CountTracker : MonoBehaviour { public bool Droplet; private bool counted; private bool started; public void Begin(bool droplet) { Droplet = droplet; Set(alive: true); } private void OnEnable() { if (!counted && started) { Set(alive: true); } } private void OnDisable() { Set(alive: false); } private void Set(bool alive) { started = true; if (alive != counted) { counted = alive; if (Droplet) { airDroplets = Math.Max(0, airDroplets + (alive ? 1 : (-1))); } else { groundItems = Math.Max(0, groundItems + (alive ? 1 : (-1))); } } } } private sealed class Pending { public MethodBase Method; public object[] Args; public float Gap; } public static float HeavyEndedAt = float.NegativeInfinity; private static float rockCredit; private static Type maulingRockType; private static bool maulingRockLooked; private static readonly Dictionary<string, bool> isRock = new Dictionary<string, bool>(); private static long lastTs = Stopwatch.GetTimestamp(); private static double avgFrameMs = 16.0; private static float belowSince = -1f; private static float aboveSince = -1f; private static float sceneStart; private static int warnTick; private static float nextWarnAt; private static bool teleporterWasCharged; private static MethodInfo sendBroadcast; private static Type simpleMsgType; private static readonly string[] neverTouch = new string[6] { "GenericPickupController", "PickupDropletController", "PickupPickerController", "CharacterBody", "CharacterMaster", "HealthComponent" }; private static readonly List<Type> sweepTypes = new List<Type>(); private static int sweepTicks; private static bool sweepNow; private static readonly HashSet<GameObject> killedThisFrame = new HashSet<GameObject>(); private static int killedFrame = -1; private static int groundItems; private static int airDroplets; private static readonly Queue<Pending> queue = new Queue<Pending>(); private static int depth; private static bool releasing; private static int pendingThisTick; private static int pendingFrame = -1; private static float pendingFixed = -1f; private static float lastDropAt = float.NegativeInfinity; private static float lastReleaseAt = float.NegativeInfinity; public static bool HeavyLoad { get; private set; } public static int ItemsOnGround => groundItems + airDroplets; public static int Queued => queue.Count; public static bool IsMaulingRock(GameObject prefab, string key) { if (isRock.TryGetValue(key, out var value)) { return value; } if (!maulingRockLooked) { maulingRockLooked = true; maulingRockType = TypeFinder.Find("MaulingRock") ?? TypeFinder.Find("RoR2.MaulingRock"); } value = key.StartsWith("SMMaulingRock", StringComparison.Ordinal) || (maulingRockType != null && !object.ReferenceEquals(prefab.GetComponent(maulingRockType), null)); isRock[key] = value; return value; } public static bool RockAllowed() { if (!Plugin.PauseSkyMeadowRocks.Value) { return true; } if (HeavyLoad) { Limiter.CountPublic("Sky Meadow rock not fired (heavy load)"); return false; } float value = Plugin.RockRampSeconds.Value; float num = Time.realtimeSinceStartup - HeavyEndedAt; if (value <= 0f || num >= value) { return true; } rockCredit += Math.Max(0f, num / value); if (rockCredit >= 1f) { rockCredit -= 1f; return true; } Limiter.CountPublic("Sky Meadow rock not fired (ramping back up)"); return false; } public static void OnFrame() { long timestamp = Stopwatch.GetTimestamp(); double num = (double)(timestamp - lastTs) * 1000.0 / (double)Stopwatch.Frequency; lastTs = timestamp; if (num > 5000.0) { num = 5000.0; } avgFrameMs += (num - avgFrameMs) * Math.Min(1.0, num / 1000.0); float realtimeSinceStartup = Time.realtimeSinceStartup; int value = Plugin.HeavyLoadFps.Value; if (value <= 0 || realtimeSinceStartup - sceneStart < 10f) { SetHeavy(v: false); return; } double num2 = 1000.0 / Math.Max(1.0, avgFrameMs); if (num2 < (double)value) { aboveSince = -1f; if (belowSince < 0f) { belowSince = realtimeSinceStartup; } if (!HeavyLoad && realtimeSinceStartup - belowSince >= 2f) { SetHeavy(v: true); } } else if (num2 > (double)(value + 5)) { belowSince = -1f; if (aboveSince < 0f) { aboveSince = realtimeSinceStartup; } if (HeavyLoad && realtimeSinceStartup - aboveSince >= 3f) { SetHeavy(v: false); } } } private static void SetHeavy(bool v) { if (HeavyLoad != v) { HeavyLoad = v; if (v) { sweepNow = true; sweepTicks = 0; } else { HeavyEndedAt = Time.realtimeSinceStartup; rockCredit = 0f; } Plugin.Log.LogInfo((object)((!v) ? ("Heavy load over: gold nuggets and item drops back to normal" + ((queue.Count <= 0) ? "." : (" (" + queue.Count + " queued items will drop over the next few seconds)."))) : ("Heavy load detected (below " + Plugin.HeavyLoadFps.Value + " FPS): gold nuggets and physics pickups paused and cleared, item drops queue past the limit."))); } } public static void WarnTick() { //IL_0171: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Unknown result type (might be due to invalid IL or missing references) if (queue.Count == 0 || !Plugin.WarnBeforeLeaving.Value || ++warnTick % 30 != 0 || !Limiter.IsServer()) { return; } try { float realtimeSinceStartup = Time.realtimeSinceStartup; List<Vector3> list = new List<Vector3>(); bool flag = false; Type type = TypeFinder.Find("RoR2.TeleporterInteraction"); if (type != null) { PropertyInfo property = type.GetProperty("instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); Component val = (Component)((!(property == null)) ? /*isinst with value type is only supported in some contexts*/: null); if (!object.ReferenceEquals(val, null) && Object.op_Implicit((Object)(object)val)) { object obj = Probes.Member(val, "isCharged"); if (obj is bool && (bool)obj) { flag = true; list.Add(val.transform.position); } } } Type type2 = TypeFinder.Find("RoR2.SceneExitController"); if (type2 != null) { Object[] array = Object.FindObjectsOfType(type2); foreach (Object val2 in array) { Component val3 = (Component)(object)((val2 is Component) ? val2 : null); if (!object.ReferenceEquals(val3, null) && Object.op_Implicit((Object)(object)val3) && (!(type != null) || object.ReferenceEquals(val3.gameObject.GetComponent(type), null))) { list.Add(val3.transform.position); } } } bool flag2 = flag && !teleporterWasCharged; teleporterWasCharged = flag; if ((flag2 || (list.Count > 0 && PlayerNear(list, Plugin.WarnRadius.Value))) && (flag2 || !(realtimeSinceStartup < nextWarnAt))) { nextWarnAt = realtimeSinceStartup + 15f; Broadcast("<size=150%><b><color=#FF2A2A>WAIT! " + queue.Count + " item" + ((queue.Count != 1) ? "s have" : " has") + " not dropped yet (item queue). Leaving now loses " + ((queue.Count != 1) ? "them" : "it") + ".</color></b></size>"); } } catch (Exception ex) { Plugin.Log.LogDebug((object)("Queue warning: " + ex.Message)); } } private static bool PlayerNear(List<Vector3> targets, float radius) { //IL_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00dc: Unknown result type (might be due to invalid IL or missing references) //IL_00e1: 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_00e5: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Unknown result type (might be due to invalid IL or missing references) //IL_00ec: Unknown result type (might be due to invalid IL or missing references) Type type = TypeFinder.Find("RoR2.CharacterBody"); PropertyInfo propertyInfo = ((!(type == null)) ? type.GetProperty("readOnlyInstancesList", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) : null); IEnumerable enumerable = ((!(propertyInfo == null)) ? (propertyInfo.GetValue(null, null) as IEnumerable) : null); if (enumerable == null) { return false; } float num = radius * radius; foreach (object item in enumerable) { Component val = (Component)((item is Component) ? item : null); if (object.ReferenceEquals(val, null) || !Object.op_Implicit((Object)(object)val)) { continue; } object obj = Probes.Member(val, "isPlayerControlled"); if (!(obj is bool) || !(bool)obj) { continue; } Vector3 position = val.transform.position; foreach (Vector3 target in targets) { Vector3 val2 = position - target; if (((Vector3)(ref val2)).sqrMagnitude <= num) { return true; } } } return false; } private static void Broadcast(string text) { if (sendBroadcast == null) { Type type = TypeFinder.Find("RoR2.Chat"); simpleMsgType = ((!(type == null)) ? type.GetNestedType("SimpleChatMessage", BindingFlags.Public | BindingFlags.NonPublic) : null); Type type2 = ((!(type == null)) ? (type.GetNestedType("ChatMessageBase", BindingFlags.Public | BindingFlags.NonPublic) ?? TypeFinder.Find("RoR2.ChatMessageBase")) : null); if (type != null && type2 != null) { sendBroadcast = type.GetMethod("SendBroadcastChat", BindingFlags.Static | BindingFlags.Public, null, new Type[1] { type2 }, null); } if (sendBroadcast == null || simpleMsgType == null) { Plugin.Log.LogWarning((object)("Queue warning: chat API not found. " + text)); return; } } object obj = Activator.CreateInstance(simpleMsgType); FieldInfo fieldInfo = AccessTools.Field(simpleMsgType, "baseToken"); if (!(fieldInfo == null)) { fieldInfo.SetValue(obj, text); sendBroadcast.Invoke(null, new object[1] { obj }); Limiter.CountPublic("Warned players: items still queued"); } } public static void OnSceneChange() { HeavyEndedAt = float.NegativeInfinity; rockCredit = 0f; teleporterWasCharged = false; nextWarnAt = 0f; sceneStart = Time.realtimeSinceStartup; belowSince = (aboveSince = -1f); HeavyLoad = false; if (queue.Count > 0) { Plugin.Log.LogInfo((object)(queue.Count + " queued item drop(s) were left behind with the stage (same as items left on the ground).")); } queue.Clear(); groundItems = 0; airDroplets = 0; } public static void Install(Harmony h) { //IL_0112: Unknown result type (might be due to invalid IL or missing references) //IL_0119: Expected O, but got Unknown //IL_013b: Unknown result type (might be due to invalid IL or missing references) //IL_0142: Expected O, but got Unknown string[] array = new string[6] { "RoR2.MoneyPickup", "RoR2.HealthPickup", "RoR2.AmmoPickup", "RoR2.BuffPickup", "RoR2.GravitatePickup", "RoR2.VelocityRandomOnStart" }; foreach (string typeName in array) { HookMessage(h, typeName, "PhysicsPickupPostfix"); } string[] array2 = new string[4] { "RoR2.MoneyPickup", "RoR2.HealthPickup", "RoR2.AmmoPickup", "RoR2.BuffPickup" }; foreach (string fullName in array2) { Type type = TypeFinder.Find(fullName); if (type != null) { sweepTypes.Add(type); } } HookMessage(h, "RoR2.GenericPickupController", "PickupStartPostfix"); HookMessage(h, "RoR2.PickupDropletController", "DropletStartPostfix"); Type type2 = TypeFinder.Find("RoR2.PickupDropletController"); if (!(type2 != null)) { return; } HarmonyMethod val = new HarmonyMethod(typeof(LoadManager).GetMethod("DropPrefix", BindingFlags.Static | BindingFlags.NonPublic)); val.priority = 200; HarmonyMethod val2 = new HarmonyMethod(typeof(LoadManager).GetMethod("DropFinalizer", BindingFlags.Static | BindingFlags.NonPublic)); int num = 0; MethodInfo[] methods = type2.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo in methods) { if (methodInfo.Name == "CreatePickupDroplet" && methodInfo.ReturnType == typeof(void) && !methodInfo.ContainsGenericParameters) { try { MethodInfo methodInfo2 = methodInfo; HarmonyMethod val3 = val; HarmonyMethod val4 = val2; h.Patch((MethodBase)methodInfo2, val3, (HarmonyMethod)null, (HarmonyMethod)null, val4, (HarmonyMethod)null); num++; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Item queue: " + ex.Message)); } } } Plugin.Log.LogInfo((object)("Item drop queue hooked " + num + " CreatePickupDroplet overload(s).")); } private static void HookMessage(Harmony h, string typeName, string postfix) { //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Expected O, but got Unknown Type type = TypeFinder.Find(typeName); if (type == null) { Plugin.Log.LogDebug((object)("LoadManager: " + typeName + " not found")); return; } string[] array = new string[3] { "Start", "OnEnable", "Awake" }; foreach (string text in array) { MethodInfo methodInfo = AccessTools.DeclaredMethod(type, text, Type.EmptyTypes, (Type[])null); if (!(methodInfo == null)) { try { MethodInfo methodInfo2 = methodInfo; HarmonyMethod val = new HarmonyMethod(typeof(LoadManager).GetMethod(postfix, BindingFlags.Static | BindingFlags.NonPublic)); h.Patch((MethodBase)methodInfo2, (HarmonyMethod)null, val, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); break; } catch (Exception ex) { Plugin.Log.LogWarning((object)("LoadManager could not hook " + typeName + "." + text + ": " + ex.Message)); break; } } } } private static string Classify(GameObject root) { Component[] array = ((Component)root.transform).GetComponentsInChildren(typeof(Component)); if (array == null) { array = ((Component)root.transform).GetComponents(typeof(Component)); } if (array == null) { return null; } bool flag = false; bool flag2 = false; Component[] array2 = array; foreach (Component val in array2) { if (object.ReferenceEquals(val, null)) { continue; } string name = ((object)val).GetType().Name; string[] array3 = neverTouch; foreach (string text in array3) { if (name == text) { return null; } } if (name == "MoneyPickup") { flag = true; } else if (name == "GravitatePickup" || name.EndsWith("Pickup", StringComparison.Ordinal)) { flag2 = true; } } if (flag || ((Object)root).name.StartsWith("BonusMoneyPack", StringComparison.Ordinal)) { return "money"; } return (!flag2) ? null : "pickup"; } private static void PhysicsPickupPostfix(Component __instance) { try { if (Limiter.IsServer() && !object.ReferenceEquals(__instance, null) && Object.op_Implicit((Object)(object)__instance)) { TryRemove(RootOf(__instance), sweep: false); } } catch { } } private static GameObject RootOf(Component c) { return (c.transform == null || object.ReferenceEquals(c.transform.root, null)) ? c.gameObject : ((Component)c.transform.root).gameObject; } private static bool TryRemove(GameObject root, bool sweep) { if (object.ReferenceEquals(root, null) || !Object.op_Implicit((Object)(object)root)) { return false; } if (killedFrame != Time.frameCount) { killedFrame = Time.frameCount; killedThisFrame.Clear(); } if (killedThisFrame.Contains(root)) { return false; } string text = Classify(root); if (text == null) { return false; } string key; if (text == "money") { if (!Plugin.PauseGoldNuggets.Value) { return false; } bool flag = AllPlayersMaxMoney(); if (!HeavyLoad && !flag) { return false; } key = (sweep ? ((!flag) ? "Gold nugget removed from the ground (heavy load)" : "Gold nugget removed from the ground (money already at maximum)") : ((!flag) ? "Gold nugget skipped (heavy load)" : "Gold nugget skipped (money already at maximum)")); } else { if (!Plugin.PausePhysicsPickups.Value || !HeavyLoad) { return false; } key = ((!sweep) ? "Physics pickup skipped (heavy load): " : "Physics pickup removed from the ground (heavy load): ") + Probes.Clean(((Object)root).name); } killedThisFrame.Add(root); Limiter.KillPublic(root); Limiter.CountPublic(key); return true; } public static void SweepTick() { if (!HeavyLoad || sweepTypes.Count == 0 || (!sweepNow && ++sweepTicks < 60)) { return; } sweepNow = false; sweepTicks = 0; if (!Plugin.SweepPickupsOnHeavyLoad.Value || !Limiter.IsServer() || (!Plugin.PauseGoldNuggets.Value && !Plugin.PausePhysicsPickups.Value)) { return; } int num = 0; try { foreach (Type sweepType in sweepTypes) { Object[] array = Object.FindObjectsOfType(sweepType); if (array == null) { continue; } Object[] array2 = array; foreach (Object val in array2) { Component val2 = (Component)(object)((val is Component) ? val : null); if (!object.ReferenceEquals(val2, null) && Object.op_Implicit((Object)(object)val2) && TryRemove(RootOf(val2), sweep: true)) { num++; } } } } catch (Exception ex) { Plugin.Log.LogDebug((object)("Pickup sweep failed: " + ex.Message)); } if (num > 0) { Plugin.Log.LogInfo((object)("Heavy load: removed " + num + " pickup(s) already on the ground.")); } } private static bool AllPlayersMaxMoney() { if (!Plugin.PreventMoneyOverflow.Value) { return false; } Type type = TypeFinder.Find("RoR2.CharacterMaster"); PropertyInfo propertyInfo = ((!(type == null)) ? type.GetProperty("readOnlyInstancesList", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) : null); IEnumerable enumerable = ((!(propertyInfo == null)) ? (propertyInfo.GetValue(null, null) as IEnumerable) : null); if (enumerable == null) { return false; } int num = 0; foreach (object item in enumerable) { if (item != null && Probes.Member(item, "playerCharacterMasterController") != null) { num++; if (!Limiter.TryGetMoney(item, out var money) || money < int.MaxValue) { return false; } } } return num > 0; } private static void Track(Component c, bool droplet) { try { if (object.ReferenceEquals(c, null) || !Object.op_Implicit((Object)(object)c)) { return; } GameObject gameObject = c.gameObject; if (object.ReferenceEquals(gameObject.GetComponent(typeof(CountTracker)), null)) { CountTracker countTracker = gameObject.AddComponent<CountTracker>(); if (!object.ReferenceEquals(countTracker, null)) { countTracker.Begin(droplet); } } } catch { } } private static void PickupStartPostfix(Component __instance) { Track(__instance, droplet: false); } private static void DropletStartPostfix(Component __instance) { Track(__instance, droplet: true); } private static bool DropPrefix(MethodBase __originalMethod, object[] __args, out bool __state) { __state = false; try { if (releasing || depth > 0) { depth++; __state = true; return true; } int value = Plugin.MaxItemsOnGround.Value; if (value > 0 && HeavyLoad && Limiter.IsServer()) { int frameCount = Time.frameCount; float fixedTime = Time.fixedTime; if (frameCount != pendingFrame || fixedTime != pendingFixed) { pendingFrame = frameCount; pendingFixed = fixedTime; pendingThisTick = 0; } if (ItemsOnGround + pendingThisTick >= value || queue.Count > 0) { float gap = Math.Max(0f, Math.Min(2f, fixedTime - lastDropAt)); lastDropAt = fixedTime; queue.Enqueue(new Pending { Method = __originalMethod, Args = (object[])__args.Clone(), Gap = gap }); Limiter.CountPublic("Item drop queued (too many items on the ground)"); return false; } pendingThisTick++; } lastDropAt = Time.fixedTime; depth++; __state = true; } catch { } return true; } private static Exception DropFinalizer(Exception __exception, bool __state) { if (__state) { depth = Math.Max(0, depth - 1); } return __exception; } public static void Tick() { if (queue.Count == 0) { return; } if (!Limiter.IsServer()) { Plugin.Log.LogInfo((object)(queue.Count + " queued item drop(s) discarded: the run ended.")); queue.Clear(); return; } int value = Plugin.MaxItemsOnGround.Value; float fixedTime = Time.fixedTime; if (fixedTime < lastReleaseAt) { lastReleaseAt = float.NegativeInfinity; } float num = Plugin.QueuedItemsPerSecond.Value; int num2 = 0; while (queue.Count > 0 && (!HeavyLoad || value <= 0 || ItemsOnGround + num2 < value)) { Pending pending = queue.Peek(); float num3 = pending.Gap; if (!HeavyLoad && num > 0f) { num3 = Math.Max(num3, 1f / num); } if (fixedTime - lastReleaseAt < num3) { break; } Pending pending2 = queue.Dequeue(); lastReleaseAt = fixedTime; releasing = true; num2++; try { pending2.Method.Invoke(null, pending2.Args); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Queued item drop failed: " + (ex.InnerException ?? ex).Message)); } finally { releasing = false; } } } } [BepInPlugin("com.Videowiz92.LoopLagLimiter", "LoopLagLimiter", "1.2.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] public class Plugin : BaseUnityPlugin { internal sealed class OptionInfo { public ConfigEntryBase Entry; public float Min; public float Max; public float Step; public bool Restart; } public const string GUID = "com.Videowiz92.LoopLagLimiter"; public const string NAME = "LoopLagLimiter"; public const string VERSION = "1.2.0"; internal static ManualLogSource Log; internal static int MainThreadId; internal static bool Reporting; internal static readonly List<OptionInfo> Options = new List<OptionInfo>(); internal static ConfigEntry<bool> InstallLimits; internal static ConfigEntry<bool> InstallProcLimits; internal static ConfigEntry<bool> RegisterRiskOfOptions; internal static ConfigEntry<bool> StartupTrace; internal static ConfigEntry<float> MaxCatchUpSeconds; internal static ConfigEntry<int> MaxRadialForcesPerPrefab; internal static ConfigEntry<bool> DropDeadTargetOrbs; internal static ConfigEntry<int> MaxCampDirectorCredits; internal static ConfigEntry<string> CampDirectorNames; internal static ConfigEntry<int> MaxAliveProjectilesPerPrefab; internal static ConfigEntry<int> MaxAliveProjectilesTotal; internal static ConfigEntry<int> MaxEffectsPerTick; internal static ConfigEntry<int> MaxOnKillChainDepth; internal static ConfigEntry<int> MaxAliveOrbsPerType; internal static ConfigEntry<int> MaxFiredPerKindPerTick; internal static ConfigEntry<bool> PreventMoneyOverflow; internal static ConfigEntry<bool> PreventPriceOverflow; internal static ConfigEntry<bool> CapHalcyonWaveDirector; internal static ConfigEntry<int> MaxDeferredProcsPerTick; internal static ConfigEntry<int> HeavyLoadFps; internal static ConfigEntry<bool> PauseGoldNuggets; internal static ConfigEntry<bool> PausePhysicsPickups; internal static ConfigEntry<bool> SweepPickupsOnHeavyLoad; internal static ConfigEntry<bool> PauseSkyMeadowRocks; internal static ConfigEntry<float> RockRampSeconds; internal static ConfigEntry<int> MaxItemsOnGround; internal static ConfigEntry<int> QueuedItemsPerSecond; internal static ConfigEntry<bool> WarnBeforeLeaving; internal static ConfigEntry<float> WarnRadius; internal static ConfigEntry<bool> SuppressKnownExceptions; internal static ConfigEntry<string> SuppressList; internal static ConfigEntry<string> ExtraProcLimits; internal static ConfigEntry<bool> DiagnosticsEnabled; internal static ConfigEntry<int> StallThresholdMs; internal static ConfigEntry<float> ReportCooldownSeconds; internal static ConfigEntry<int> MaxReportFiles; internal static ConfigEntry<bool> AttributeSources; internal static ConfigEntry<string> DeepSplitTypes; internal static ConfigEntry<string> InspectTypes; internal static ConfigEntry<KeyboardShortcut> DeepToggleKey; internal static ConfigEntry<KeyboardShortcut> ReportKey; private Harmony harmony; private bool diagnostics; private float defaultMaxDeltaTime = -1f; private long lastFrameTs; private float lastReportRealtime = -9999f; private int reportsWritten; private bool sceneLoadedThisFrame; private string reportDir; private readonly int[] gcAtFrameStart = new int[3]; private int traceFrames; private static readonly string[,] RenamedSections = new string[4, 2] { { "3. Fixes", "4. Fixes" }, { "4. Proc limits", "5. Proc limits" }, { "5. Diagnostics", "6. Diagnostics" }, { "6. Heavy load", "3. Heavy load" } }; private void Awake() { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Expected O, but got Unknown //IL_013d: Unknown result type (might be due to invalid IL or missing references) //IL_0147: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; MainThreadId = Thread.CurrentThread.ManagedThreadId; BindConfig(); MigrateRenamedSections(); harmony = new Harmony("com.Videowiz92.LoopLagLimiter"); Trace("Awake: config bound"); if (SuppressKnownExceptions.Value) { Fixes.Install(harmony, SuppressList.Value); } Trace("Awake: exception guards done"); if (InstallLimits.Value) { Limiter.Install(harmony); ChainGuard.Install(harmony); LoadManager.Install(harmony); } else { Log.LogWarning((object)"Troubleshooting: limit hooks NOT installed"); } Trace("Awake: limit hooks done"); if (InstallProcLimits.Value) { ProcLimits.Install(harmony); } else { Log.LogWarning((object)"Troubleshooting: proc limit hooks NOT installed"); } Trace("Awake: proc limit hooks done"); diagnostics = DiagnosticsEnabled.Value; reportDir = Path.Combine(Paths.BepInExRootPath, "LoopLagLimiter"); if (diagnostics) { Probes.Install(harmony); Application.logMessageReceived += new LogCallback(OnUnityLog); } SceneManager.sceneLoaded += delegate(Scene s, LoadSceneMode m) { Trace("scene loaded: " + ((Scene)(ref s)).name); sceneLoadedThisFrame = true; Limiter.OnSceneChange(); ChainGuard.OnSceneChange(); LoadManager.OnSceneChange(); ProcLimits.OnSceneChange(); ApplyCatchUpCap(log: false); }; MaxCatchUpSeconds.SettingChanged += delegate { ApplyCatchUpCap(log: true); }; if (RegisterRiskOfOptions.Value) { try { RiskOfOptionsCompat.TryRegister(); } catch (Exception ex) { Log.LogWarning((object)("Risk of Options integration failed (the mod still works, edit the .cfg instead): " + ex.Message)); } } Trace("Awake: Risk of Options step done"); lastFrameTs = Stopwatch.GetTimestamp(); Log.LogInfo((object)("LoopLagLimiter 1.2.0 loaded." + ((!diagnostics) ? string.Empty : (" Diagnostics ON: F9 = report now, F10 = toggle deep profile. Reports go to " + reportDir)))); } private void Start() { Trace("Start: begin"); defaultMaxDeltaTime = Time.maximumDeltaTime; ApplyCatchUpCap(log: true); Trace("Start: end (maximumDeltaTime=" + Time.maximumDeltaTime + ")"); } internal static void Trace(string msg) { if (StartupTrace != null && StartupTrace.Value) { Log.LogInfo((object)("[trace] " + msg)); } } private void LateUpdate() { if (StartupTrace.Value && traceFrames < 400) { traceFrames++; if (traceFrames == 1 || traceFrames == 10 || traceFrames == 100 || traceFrames == 400) { Trace("frame " + traceFrames + " reached (Time.frameCount=" + Time.frameCount + ")"); } } } private void ApplyCatchUpCap(bool log) { float value = MaxCatchUpSeconds.Value; float num = ((!(value < 0f)) ? Math.Max(value, Time.fixedDeltaTime) : defaultMaxDeltaTime); if (!(num <= 0f) && !(Math.Abs(Time.maximumDeltaTime - num) < 0.0001f)) { Time.maximumDeltaTime = num; if (log) { Log.LogInfo((object)$"Max catch-up per frame set to {num:0.###} s (about {num / Time.fixedDeltaTime:0} physics ticks)."); } } } private ConfigEntry<T> B<T>(string section, string key, T def, string desc, float min = 0f, float max = 0f, float step = 1f, bool restart = false) { ConfigEntry<T> val = ((BaseUnityPlugin)this).Config.Bind<T>(section, key, def, desc); Options.Add(new OptionInfo { Entry = (ConfigEntryBase)(object)val, Min = min, Max = max, Step = step, Restart = restart }); return val; } private void MigrateRenamedSections() { //IL_012f: Unknown result type (might be due to invalid IL or missing references) //IL_0136: Expected O, but got Unknown //IL_00b9: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Expected O, but got Unknown try { PropertyInfo property = typeof(ConfigFile).GetProperty("OrphanedEntries", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); Dictionary<ConfigDefinition, string> dictionary = ((!(property == null)) ? (property.GetValue(((BaseUnityPlugin)this).Config, null) as Dictionary<ConfigDefinition, string>) : null); if (dictionary == null || dictionary.Count == 0) { return; } int num = 0; foreach (OptionInfo option in Options) { ConfigDefinition definition = option.Entry.Definition; for (int i = 0; i < RenamedSections.GetLength(0); i++) { if (!(definition.Section != RenamedSections[i, 1])) { ConfigDefinition key = new ConfigDefinition(RenamedSections[i, 0], definition.Key); if (dictionary.TryGetValue(key, out var value)) { option.Entry.SetSerializedValue(value); dictionary.Remove(key); num++; } } } } ConfigDefinition key2 = new ConfigDefinition("2. Limits", "Max visual effects per tick"); if (dictionary.TryGetValue(key2, out var value2)) { if (int.TryParse(value2.Trim(), out var result) && result > 0) { MaxEffectsPerTick.Value = result; } dictionary.Remove(key2); num++; } if (num > 0) { ((BaseUnityPlugin)this).Config.Save(); Log.LogInfo((object)("Carried over " + num + " setting(s) from renumbered config sections.")); } } catch (Exception ex) { Log.LogWarning((object)("Config section migration skipped: " + ex.Message)); } } private void BindConfig() { //IL_0765: Unknown result type (might be due to invalid IL or missing references) //IL_079e: Unknown result type (might be due to invalid IL or missing references) InstallLimits = B("0. Troubleshooting", "Install limit hooks", def: true, "Troubleshooting: set false to skip all hooks in sections 2 (Limits) and 3 (Heavy load).", 0f, 0f, 1f, restart: true); InstallProcLimits = B("0. Troubleshooting", "Install proc limit hooks", def: true, "Troubleshooting: set false to skip all hooks in section 5 (Proc limits).", 0f, 0f, 1f, restart: true); RegisterRiskOfOptions = B("0. Troubleshooting", "Register Risk of Options page", def: true, "Set false to skip the in-game settings page (if Risk of Options causes problems).", 0f, 0f, 1f, restart: true); StartupTrace = B("0. Troubleshooting", "Startup trace logging", def: false, "Logs each startup step (and the first few hundred frames) so a stuck boot can be pinpointed."); MaxCatchUpSeconds = B("1. General", "Max catch-up per frame (seconds)", 0.1f, "After a slow frame Unity runs extra physics ticks to catch up, which makes the next frame slow too (a lag spiral). This caps the catch-up: 0.1 = at most ~6 ticks per frame, so the game only slows down when below ~10 FPS. 0 = one tick per frame (always slow-motion below 60 FPS). -1 = leave the game's default alone. Based on NoFrameSkip by McAwesome; don't run both.", -1f, 0.35f, 0.01f); MaxRadialForcesPerPrefab = B("2. Limits", "Max pull effects per prefab", 25, "0 = off. Keeps at most this many RadialForce pull objects of one kind alive; the oldest is removed first. Sentient Meat Hook spawns a pull (MeatHookDelayedForce) on every proc and big builds stack hundreds, each searching for targets every physics tick. Only the pull is affected, not hook damage. Host only.", 0f, 200f); DropDeadTargetOrbs = B("2. Limits", "Drop orbs whose target died", def: true, "Removes orbs (Polylute, Meat Hook, vine and lightning orbs) whose target has been destroyed. They can never hit anything but otherwise keep updating, and can even outlive the stage."); MaxCampDirectorCredits = B("2. Limits", "Max camp director credits", 3000, "0 = off. When the teleporter finishes, its leftover monster credits go to the stage's camp director. Deep into a loop that can be hundreds of thousands of credits, spent on an endless post-teleporter wave. This clamps camp directors to this many credits. The main and teleporter directors are never touched. Host only.", 0f, 50000f, 500f); CampDirectorNames = B("2. Limits", "Camp director names", "Camp", "Semicolon-separated GameObject name prefixes of the directors the credit cap applies to."); CapHalcyonWaveDirector = B("2. Limits", "Also cap Halcyon Shrine wave credits", def: true, "Apply the credit cap to the Halcyon Shrine's wave director (\"Activation and Tier Change Wave\"), which was seen holding 650,000+ credits deep into a loop."); MaxAliveProjectilesPerPrefab = B("2. Limits", "Max alive projectiles per kind", 500, "0 = off. If this many projectiles of one kind (StickyBomb, FireMeatBall, LightningStake...) already exist, new ones of that kind are not fired until some expire. Covers every item. A heavily looped run had 10,577 Sticky Bombs alive at once (14 s of a 24 s frame). Only matters at that kind of scale. Host only.", 0f, 5000f, 50f); MaxAliveProjectilesTotal = B("2. Limits", "Max alive projectiles total", 1000, "0 = off. Once this many projectiles of all kinds exist together, the big kinds (those with more than a tenth of this number alive, e.g. 100+ Sticky Bombs) stop firing until the total drops. Small kinds (your skills, rare items, enemy attacks) keep working. Testing saw 1,437 projectiles alive with every kind under its own cap. Host only.", 0f, 10000f, 50f); MaxAliveOrbsPerType = B("2. Limits", "Max alive orbs per kind", 300, "0 = off. If this many orbs of one kind (Polylute, Meat Hook, Ukulele, healing orbs...) are already in flight, new ones of that kind are not created until some land. Covers every item. A heavily looped run had 1,500 Polylute orbs at once. Host only.", 0f, 3000f, 50f); MaxFiredPerKindPerTick = B("2. Limits", "Max projectiles fired per kind per tick", 30, "0 = off. At most this many projectiles of one kind may be fired per physics tick (60 ticks per second). Catches items that create and destroy projectiles faster than they live: Ghost Blades are capped by the game at 30 alive, so each new blade destroys the oldest, and 4,960 were created and destroyed in one 33-second frame. Covers every item; projectiles that spawn allies are never limited. Host only.", 0f, 500f, 5f); PreventMoneyOverflow = B("2. Limits", "Prevent money overflow", def: true, "Money is stored as an unsigned 32-bit number. Deep into a loop, per-kill gold makes it wrap past about 4.29 billion back to almost nothing, and the HUD shows anything above about 2.15 billion as negative. This stops gains at 2,147,483,647 instead. Host only."); PreventPriceOverflow = B("2. Limits", "Prevent price overflow", def: true, "Chest, shrine and shop prices are base cost times a difficulty multiplier. Deep into a loop the result no longer fits and turns into a negative price. Negative prices become 2,147,483,647 instead, the same number money stops at, so you can still buy things once your money reaches the maximum. Host only."); MaxOnKillChainDepth = B("2. Limits", "Max Gasoline chain depth", 20, "0 = off. Gasoline's on-kill explosion runs inside the death that triggered it, so a pack of low-health enemies can chain hundreds of explosions deep in one go and crash the game with a stack overflow (a total freeze). Past this depth the next explosion is queued for the next physics tick instead. Every explosion still happens. Host only.", 0f, 100f); MaxDeferredProcsPerTick = B("2. Limits", "Max queued Gasoline explosions per tick", 200, "How many queued Gasoline explosions run per physics tick (keeps a huge chain from becoming one huge frame).", 10f, 2000f, 10f); MaxEffectsPerTick = B("2. Limits", "Visual effects per tick limit", 75, "0 = off. At most this many visual effects (explosions, lightning strikes, sparks...) are spawned per physics tick; the rest are skipped. Purely visual: damage and procs are unaffected. Testing cut total stall time by about 70%. Lower = faster but fewer effects on screen.", 0f, 1000f, 5f); HeavyLoadFps = B("3. Heavy load", "Heavy load below FPS", 15, "0 = never. When the average frame rate stays below this for 2 seconds, the game counts as under heavy load until it recovers (above this + 5 FPS for 3 seconds). The settings below only act under heavy load, so normal runs are untouched.", 0f, 60f); PauseGoldNuggets = B("3. Heavy load", "Pause gold nuggets", def: true, "Under heavy load, gold nuggets (Ghor's Tome bonus money packs) are not spawned: at that point enemies die instantly and hundreds of nuggets on the ground just add lag. They are also skipped whenever every player's money is already at the maximum (money overflow guard), since they couldn't give anything. Host only."); PausePhysicsPickups = B("3. Heavy load", "Pause other physics pickups", def: true, "Under heavy load, other pickups that drop on the ground and fly to players are not spawned either: Monster Tooth healing orbs, Bandolier ammo packs, buff pickups, and modded variants (e.g. ItemQualities' quality tiers). Testing saw 100+ healing orbs and 70+ ammo packs at once. Real items, item drops and characters are never affected. Host only."); SweepPickupsOnHeavyLoad = B("3. Heavy load", "Clear pickups already on the ground", def: true, "When heavy load starts, and every second while it lasts, gold nuggets and other physics pickups already lying on the ground are removed too (only the kinds paused above). Real items are never removed. Host only."); PauseSkyMeadowRocks = B("3. Heavy load", "Pause Sky Meadow falling rocks", def: true, "Sky Meadow keeps firing falling rocks for the whole stage, and they take hits like enemies, so every proc chain lands on them too. Under heavy load no new rocks fall; rocks already falling are left alone. Host only."); RockRampSeconds = B("3. Heavy load", "Rock ramp-up after heavy load (seconds)", 60f, "After heavy load ends, falling rocks come back gradually: their rate climbs from none to normal over this many seconds. If heavy load returns, they stop again. 0 = back to normal at once.", 0f, 300f, 10f); MaxItemsOnGround = B("3. Heavy load", "Max items on the ground", 50, "0 = off. Under heavy load, once this many items are on the ground or falling, new item drops wait in a queue. Each time the count drops below the limit (someone picks an item up), the next queued item drops where it would have. Queued items left when the stage ends are lost, the same as items left on the ground. Host only.", 0f, 500f, 10f); QueuedItemsPerSecond = B("3. Heavy load", "Queued items released per second", 2, "When heavy load ends, queued items drop at this rate until the queue is empty.", 1f, 50f); WarnBeforeLeaving = B("3. Heavy load", "Warn before leaving with queued items", def: true, "While items are still queued, post a big red chat warning when a player is near a fully charged teleporter or a portal (at most every 15 seconds), and once as soon as the teleporter finishes charging. Everyone sees it, with or without the mod. Host only."); WarnRadius = B("3. Heavy load", "Warning distance (meters)", 30f, "How close a player has to be to a charged teleporter or portal.", 5f, 100f, 5f); SuppressKnownExceptions = B("4. Fixes", "Suppress known exception spam", def: true, "Catch NullReferenceExceptions from the methods below so the attack continues instead of aborting, and the full stack trace isn't logged every time (e.g. Resonance Disc beams hitting a blocker shield).", 0f, 0f, 1f, restart: true); SuppressList = B("4. Fixes", "Methods to guard", "RoR2.HurtBox:SimpleColliderBlockFilter;RoR2.AttackSpeedPerNearbyCollider:ServerUpdateValuesFromInventory", "Semicolon-separated Type:Method list. Only void or bool methods (bool returns false on error).", 0f, 0f, 1f, restart: true); ProcLimits.Bind(((BaseUnityPlugin)this).Config); foreach (ProcLimits.Limit limit in ProcLimits.Limits) { Options.Add(new OptionInfo { Entry = (ConfigEntryBase)(object)limit.PerSecond, Min = 0f, Max = 200f, Step = 1f }); } ExtraProcLimits = B("5. Proc limits", "Extra limits", string.Empty, "More per-second limits as Name=N;Name=N where Name is an orb class (e.g. VineOrb, LightningOrb.Tesla) or a projectile prefab (e.g. LightningStake). Use diagnostics reports to find names."); DiagnosticsEnabled = B("6. Diagnostics", "Enable diagnostics", def: false, "Time the game's hot spots and write a report to BepInEx/LoopLagLimiter whenever a frame stalls. Small overhead; turn on to find the cause of lag in your own runs.", 0f, 0f, 1f, restart: true); StallThresholdMs = B("6. Diagnostics", "Stall threshold (ms)", 250, "A frame that takes at least this long gets a report.", 50f, 2000f, 50f); ReportCooldownSeconds = B("6. Diagnostics", "Report cooldown (seconds)", 3f, "Minimum real time between automatic reports.", 0f, 30f); MaxReportFiles = B("6. Diagnostics", "Max report files per session", 60, "Stops writing report files after this many in one session.", 0f, 500f, 10f); AttributeSources = B("6. Diagnostics", "Attribute sources", def: true, "Record who triggered each probe (projectile, orb type, attacker...)."); DeepSplitTypes = B("6. Diagnostics", "Deep profile: split by object", "RoR2.RadialForce;RoR2.RTPCController;RoR2.DelayBlast;AkGameObj;RoR2.Projectile.ProjectileImpactExplosion;RoR2.ExperienceOrbBehavior", "Component types whose deep-profile time is broken down per GameObject name."); InspectTypes = B("6. Diagnostics", "Inspect component types", "RoR2.RadialForce;RoR2.RTPCController;AkGameObj;RoR2.DelayBlast", "Reports count these components and list which objects carry them."); DeepToggleKey = B<KeyboardShortcut>("6. Diagnostics", "Deep profile toggle key", new KeyboardShortcut((KeyCode)291, Array.Empty<KeyCode>()), "Times every component update in the game and all mods (hitch of a few seconds when toggled)."); ReportKey = B<KeyboardShortcut>("6. Diagnostics", "Report-now key", new KeyboardShortcut((KeyCode)290, Array.Empty<KeyCode>()), "Writes a report covering everything since the last one."); } private void OnDestroy() { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Expected O, but got Unknown if (diagnostics) { Application.logMessageReceived -= new LogCallback(OnUnityLog); } } private static void OnUnityLog(string condition, string stackTrace, LogType type) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Invalid comparison between Unknown and I4 //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Invalid comparison between Unknown and I4 if (Reporting || ((int)type != 0 && (int)type != 4 && (int)type != 1) || Thread.CurrentThread.ManagedThreadId != MainThreadId) { return; } string text = string.Empty; if (!string.IsNullOrEmpty(stackTrace)) { int num = stackTrace.IndexOf('\n'); text = ((num <= 0) ? stackTrace : stackTrace.Substring(0, num)); int num2 = text.IndexOf(" (at ", StringComparison.Ordinal); if (num2 > 0) { text = text.Substring(0, num2); } } string text2 = condition ?? string.Empty; if (text2.Length > 100) { text2 = text2.Substring(0, 100); } string key = ((text.Length <= 0) ? text2 : (text2 + " @ " + text)); Frame.Current.UnityErrors++; Frame.Window.UnityErrors++; Inc(Frame.Current.ErrorKinds, key); Inc(Frame.Window.ErrorKinds, key); } internal static void Inc(Dictionary<string, int> d, string key) { d.TryGetValue(key, out var value); d[key] = value + 1; } private void FixedUpdate() { try { ChainGuard.Drain(); } catch (Exception ex) { Log.LogDebug((object)ex.Message); } try { if (InstallLimits.Value) { Limiter.MoneySweep(); } } catch (Exception ex2) { Log.LogDebug((object)ex2.Message); } try { if (InstallLimits.Value) { LoadManager.Tick(); LoadManager.WarnTick(); LoadManager.SweepTick(); } } catch (Exception ex3) { Log.LogDebug((object)ex3.Message); } if (diagnostics) { Frame.Current.FixedSteps++; Frame.Window.FixedSteps++; } } private void Update() { //IL_00e0: Unknown result type (might be due to invalid IL or missing references) //IL_00e5: Unknown result type (might be due to invalid IL or missing references) //IL_0126: Unknown result type (might be due to invalid IL or missing references) //IL_012b: Unknown result type (might be due to invalid IL or missing references) try { LoadManager.OnFrame(); } catch { } if (!diagnostics) { return; } long timestamp = Stopwatch.GetTimestamp(); double num = (double)(timestamp - lastFrameTs) * 1000.0 / (double)Stopwatch.Frequency; lastFrameTs = timestamp; Frame.Current.FrameMs = num; Frame.Window.FrameMs += num; Frame.Window.Frames++; if (num > Frame.Window.WorstFrameMs) { Frame.Window.WorstFrameMs = num; } for (int i = 0; i < 3; i++) { Frame.Current.GcDelta[i] = GC.CollectionCount(i) - gcAtFrameStart[i]; Frame.Window.GcDelta[i] += Frame.Current.GcDelta[i]; } try { KeyboardShortcut value = DeepToggleKey.Value; if (((KeyboardShortcut)(ref value)).IsDown()) { if (Deep.Enabled) { Deep.Disable(); } else { Deep.Enable(); } lastFrameTs = Stopwatch.GetTimestamp(); ResetFrame(); return; } KeyboardShortcut value2 = ReportKey.Value; if (((KeyboardShortcut)(ref value2)).IsDown()) { WriteReport(Frame.Window, "manual: everything since last report", isWindow: true); Frame.Window = new FrameStats(); } } catch (Exception ex) { Log.LogError((object)ex); } bool flag = sceneLoadedThisFrame; sceneLoadedThisFrame = false; if (num >= (double)StallThresholdMs.Value && Time.frameCount > 10) { float realtimeSinceStartup = Time.realtimeSinceStartup; bool flag2 = realtimeSinceStartup - lastReportRealtime >= ReportCooldownSeconds.Value || num >= (double)ReportCooldownSeconds.Value * 1000.0; if (!flag && flag2) { lastReportRealtime = realtimeSinceStartup; WriteReport(Frame.Current, $"STALL: one frame took {num:0} ms", isWindow: false); } } ResetFrame(); } private void ResetFrame() { Frame.Current.Clear(); for (int i = 0; i < 3; i++) { gcAtFrameStart[i] = GC.CollectionCount(i); } Probes.ResetTickCounters(); } private void WriteReport(FrameStats s, string title, bool isWindow) { Reporting = true; try { string text = ReportBuilder.Build(s, title, isWindow); string[] array = text.Split('\n'); foreach (string text2 in array) { Log.LogWarning((object)text2.TrimEnd('\r')); } if (reportsWritten < MaxReportFiles.Value) { Directory.CreateDirectory(reportDir); string text3 = Path.Combine(reportDir, $"LoopLagLimiter_{DateTime.Now:yyyyMMdd_HHmmss}_f{Time.frameCount}.txt"); File.WriteAllText(text3, text); reportsWritten++; Log.LogWarning((object)("Report saved: " + text3)); } } catch (Exception ex) { Log.LogError((object)("Failed to write report: " + ex)); } finally { Reporting = false; } lastFrameTs = Stopwatch.GetTimestamp(); } } internal static class Probes { private sealed class Def { public string Name; public string TypeName; public string[] Methods; public Func<object, object[], string> Attr; public ConfigEntry<int> Cap; } public const int MaxProbes = 24; private static readonly List<Def> defs = new List<Def>(); private static readonly Dictionary<MethodBase, int> probeOf = new Dictionary<MethodBase, int>(); private static readonly int[] depth = new int[24]; private static readonly string[] currentSource = new string[24]; private static readonly int[] tickCount = new int[24]; private static readonly bool[] returnsVoid = new bool[24]; private static int lastTickFrame = -1; private static float lastTickFixed = -1f; private static MethodInfo effectDefGetter; private static bool effectLookupFailed; private static readonly Dictionary<Type, Dictionary<string, Func<object, object>>> getters = new Dictionary<Type, Dictionary<string, Func<object, object>>>(); public static int Count => defs.Count; public static string NameOf(int i) { return (i >= defs.Count) ? "?" : defs[i].Name; } private static void Add(string name, string type, string[] methods, Func<object, object[], string> attr = null, ConfigEntry<int> cap = null) { defs.Add(new Def { Name = name, TypeName = type, Methods = methods, Attr = attr, Cap = cap }); } public static void Install(Harmony h) { //IL_0413: Unknown result type (might be due to invalid IL or missing references) //IL_0419: Expected O, but got Unknown //IL_042f: Unknown result type (might be due to invalid IL or missing references) //IL_0435: Expected O, but got Unknown Add("HealthComponent.TakeDamage", "RoR2.HealthComponent", new string[2] { "TakeDamage", "TakeDamageProcess" }, (object i, object[] a) => DamageSource(Arg(a, 0))); Add("GlobalEventManager.OnHitEnemy", "RoR2.GlobalEventManager", new string[2] { "OnHitEnemy", "ProcessHitEnemy" }, (object i, object[] a) => DamageSource(Arg(a, 0))); Add("GlobalEventManager.OnHitAll", "RoR2.GlobalEventManager", new string[2] { "OnHitAll", "OnHitAllProcess" }, (object i, object[] a) => DamageSource(Arg(a, 0))); Add("GlobalEventManager.OnCharacterDeath", "RoR2.GlobalEventManager", new string[1] { "OnCharacterDeath" }); Add("CharacterBody.RecalculateStats", "RoR2.CharacterBody", new string[1] { "RecalculateStats" }, (object i, object[] a) => ObjName(i)); Add("CharacterBody.OnInventoryChanged", "RoR2.CharacterBody", new string[1] { "OnInventoryChanged" }, (object i, object[] a) => ObjName(i)); Add("OrbManager.AddOrb", "RoR2.Orbs.OrbManager", new string[1] { "AddOrb" }, (object i, object[] a) => TypeName(Arg(a, 0))); Add("OrbManager.FixedUpdate", "RoR2.Orbs.OrbManager", new string[1] { "FixedUpdate" }); Add("ProjectileManager.FireProjectile", "RoR2.Projectile.ProjectileManager", new string[2] { "FireProjectile", "FireProjectileServer" }, (object i, object[] a) => ProjectileSource(a)); Add("BulletAttack.Fire", "RoR2.BulletAttack", new string[1] { "Fire" }, (object i, object[] a) => ObjName(Member(i, "owner"))); Add("BlastAttack.Fire", "RoR2.BlastAttack", new string[1] { "Fire" }, (object i, object[] a) => ObjName(Member(i, "inflictor") ?? Member(i, "attacker"))); Add("OverlapAttack.Fire", "RoR2.OverlapAttack", new string[1] { "Fire" }, (object i, object[] a) => ObjName(Member(i, "attacker"))); Add("EffectManager.SpawnEffect", "RoR2.EffectManager", new string[1] { "SpawnEffect" }, (object i, object[] a) => EffectSource(Arg(a, 0))); Add("DotController.InflictDot", "RoR2.DotController", new string[1] { "InflictDot" }); Add("PickupDroplet.Create", "RoR2.PickupDropletController", new string[1] { "CreatePickupDroplet" }); Add("GenericPickup.AttemptGrant", "RoR2.GenericPickupController", new string[1] { "AttemptGrant" }); Add("CombatDirector.FixedUpdate", "RoR2.CombatDirector", new string[1] { "FixedUpdate" }, (object i, object[] a) => ObjName(i)); Add("CombatDirector.Spawn", "RoR2.CombatDirector", new string[2] { "AttemptSpawnOnTarget", "Spawn" }, (object i, object[] a) => ObjName(i)); Add("BepInEx log write", "BepInEx.Logging.Logger", new string[1] { "InternalLogEvent" }, (object i, object[] a) => LogSource(Arg(a, 1))); HarmonyMethod val = new HarmonyMethod(typeof(Probes).GetMethod("Prefix", BindingFlags.Static | BindingFlags.NonPublic)); HarmonyMethod val2 = new HarmonyMethod(typeof(Probes).GetMethod("Finalizer", BindingFlags.Static | BindingFlags.NonPublic)); val.priority = 800; val2.priority = 0; int num = 0; for (int num2 = 0; num2 < defs.Count; num2++) { Def def = defs[num2]; Type type = TypeFinder.Find(def.TypeName); if (type == null) { Plugin.Log.LogWarning((object)("Probe skipped, type not found: " + def.TypeName)); continue; } bool flag = true; int num3 = 0; MethodInfo[] methods = type.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo in methods) { if (Array.IndexOf(def.Methods, methodInfo.Name) < 0 || methodInfo.IsAbstract || methodInfo.ContainsGenericParameters) { continue; } try { MethodInfo methodInfo2 = methodInfo; HarmonyMethod val3 = val; HarmonyMethod val4 = val2; h.Patch((MethodBase)methodInfo2, val3, (HarmonyMethod)null, (HarmonyMethod)null, val4, (HarmonyMethod)null); probeOf[methodInfo] = num2; if (methodInfo.ReturnType != typeof(void)) { flag = false; } num3++; num++; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not probe " + def.TypeName + "." + methodInfo.Name + ": " + ex.Message)); } } returnsVoid[num2] = flag; if (num3 == 0) { Plugin.Log.LogWarning((object)("Probe skipped, no methods found: " + def.Name)); } } Plugin.Log.LogInfo((object)("Probes installed on " + num + " methods.")); } public static void ResetTickCounters() { for (int i = 0; i < tickCount.Length; i++) { tickCount[i] = 0; } } private static bool Prefix(MethodBase __originalMethod, object __instance, object[] __args, out long __state) { __state = -3L; try { return PrefixImpl(__originalMethod, __instance, __args, ref __state); } catch { __state = -3L; return true; } } private static bool PrefixImpl(MethodBase __originalMethod, object __instance, object[] __args, ref long __state) { if (Plugin.Reporting || Thread.CurrentThread.ManagedThreadId != Plugin.MainThreadId) { return true; } if (!probeOf.TryGetValue(__originalMethod, out var value)) { return true; } if (depth[value] > 0) { depth[value]++; __state = -1L; return true; } Def def = defs[value]; string text = null; if (def.Attr != null && Plugin.AttributeSources.Value) { try { text = def.Attr(__instance, __args); } catch { text = "?"; } } if (def.Cap != null && def.Cap.Value > 0 && returnsVoid[value]) { int frameCount = Time.frameCount; float fixedTime = Time.fixedTime; if (frameCount != lastTickFrame || fixedTime != lastTickFixed) { lastTickFrame = frameCount; lastTickFixed = fixedTime; ResetTickCounters(); } if (++tickCount[value] > def.Cap.Value) { Record(value, text, 0L, skipped: true); __state = -2L; return false; } } depth[value] = 1; currentSource[value] = text; __state = Stopwatch.GetTimestamp(); return true; } private static Exception Finalizer(Exception __exception, MethodBase __originalMethod, long __state) { if (__state == -3 || __state == -2) { return __exception; } if (!probeOf.TryGetValue(__originalMethod, out var value)) { return __exception; } if (__state == -1) { depth[value]--; return __exception; } depth[value] = 0; try { Record(value, currentSource[value], Stopwatch.GetTimestamp() - __state, skipped: false); } catch { } currentSource[value] = null; return __exception; } private static void Record(int id, string src, long ticks, bool skipped) { Rec(Frame.Current.Probes[id], src, ticks, skipped); Rec(Frame.Window.Probes[id], src, ticks, skipped); } private static void Rec(ProbeStat p, string src, long ticks, bool skipped) { if (skipped) { p.Skipped++; } else { p.Calls++; p.Ticks += ticks; } if (src == null) { return; } if (!p.Sources.TryGetValue(src, out var value)) { if (p.Sources.Count >= 400) { src = "(other)"; } if (!p.Sources.TryGetValue(src, out value)) { value = new SrcStat(); p.Sources[src] = value; } } if (skipped) { value.Skipped++; return; } value.Calls++; value.Ticks += ticks; } private static object Arg(object[] a, int i) { return (a == null || a.Length <= i) ? null : a[i]; } private static string TypeName(object o) { return (o != null) ? o.GetType().Name : "null"; } internal static string ObjName(object o) { if (o == null) { return "none"; } Object val = (Object)((o is Object) ? o : null); if (!object.ReferenceEquals(val, null)) { if (!Object.op_Implicit(val)) { return "(destroyed)"; } Component val2 = (Component)(object)((val is Component) ? val : null); string n = (object.ReferenceEquals(val2, null) ? val.name : ((Object)val2.gameObject).name); return Clean(n); } return o.GetType().Name; } internal static string Clean(string n) { if (n == null) { return "null"; } return (!n.EndsWith("(Clone)", StringComparison.Ordinal)) ? n : n.Substring(0, n.Length - 7); } private static string DamageSource(object damageInfo) { if (damageInfo == null) { return "null"; } object obj = Member(damageInfo, "inflictor"); object obj2 = Member(damageInfo, "attacker"); string text = ObjName(obj2); Object val = (Object)((obj is Object) ? obj : null); if (!object.ReferenceEquals(val, null) && Object.op_Implicit(val) && !object.ReferenceEquals(obj, obj2)) { string text2 = ObjName(obj); if (text2 != text) { return text + " via " + text2; } } return text; } private static string ProjectileSource(object[] a) { object obj = Arg(a, 0); if (obj is GameObject) { return ObjName(obj); } if (obj != null) { return ObjName(Member(obj, "projectilePrefab")); } return "null"; } private static string EffectSource(object first) { if (first is GameObject) { return ObjName(first); } if (first != null) { try { if (effectDefGetter == null && !effectLookupFailed) { Type type = TypeFinder.Find("RoR2.EffectCatalog"); effectDefGetter = ((!(type == null)) ? AccessTools.Method(type, "GetEffectDef", (Type[])null, (Type[])null) : null); effectLookupFailed = effectDefGetter == null; } if (effectDefGetter != null) { object o = effectDefGetter.Invoke(null, new object[1] { first }); object obj = Member(o, "prefab") ?? Member(o, "prefabName"); if (obj != null) { return (!(obj is string)) ? ObjName(obj) : ((string)obj); } } } catch { effectLookupFailed = true; } return "EffectIndex " + first; } return "null"; } private static string LogSource(object args) { //IL_003d: Unknown result type (might be due to invalid IL or missing references) LogEventArgs e = (LogEventArgs)((args is LogEventArgs) ? args : null); if (e == null || e.Source == null) { return "?"; } return string.Concat(e.Source.SourceName, " [", e.Level, "]"); } internal static object Member(object o, string name) { if (o == null) { return null; } Type type = o.GetType(); if (!getters.TryGetValue(type, out var value)) { value = new Dictionary<string, Func<object, object>>(); getters[type] = value; } if (!value.TryGetValue(name, out var value2)) { FieldInfo f = AccessTools.Field(type, name); if (f != null && !f.IsStatic) { value2 = (object x) => f.GetValue(x); } else { PropertyInfo p = AccessTools.Property(type, name); value2 = ((!(p != null) || p.GetIndexParameters().Length != 0 || !p.CanRead || p.GetGetMethod(nonPublic: true).IsStatic) ? ((Func<object, object>)((object x) => (object)null)) : ((Func<object, object>)((object x) => p.GetValue(x, null)))); } value[name] = value2; } return value2(o); } } internal static class ProcLimits { internal sealed class Limit { public string Label; public string[] Keys; public ConfigEntry<int> PerSecond; } public static readonly List<Limit> Limits = new List<Limit>(); private static readonly Dictionary<string, ConfigEntry<int>> byKey = new Dictionary<string, ConfigEntry<int>>(StringComparer.OrdinalIgnoreCase); private static readonly Dictionary<string, int> extra = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); private static readonly Dictionary<string, float[]> buckets = new Dictionary<string, float[]>(); private static string extraParsedFrom; private static FieldInfo lightningTypeField; private static int effectTickCount; private static int effectTickFrame = -1; private static float effectTickFixed = -1f; public static void Bind(ConfigFile cfg) { Add(cfg, "5. Proc limits", "Sentient Meat Hook", "hook orbs (BounceOrb)", new string[1] { "BounceOrb" }); Add(cfg, "5. Proc limits", "Polylute", "lightning orbs (VoidLightningOrb)", new string[1] { "VoidLightningOrb" }); Add(cfg, "5. Proc limits", "Ukulele", "chain lightning orbs", new string[1] { "LightningOrb.Ukulele" }); Add(cfg, "5. Proc limits", "Plasma Shrimp", "missile orbs (MissileVoidOrb)", new string[1] { "MissileVoidOrb" }); Add(cfg, "5. Proc limits", "Charged Perforator", "lightning strikes (SimpleLightningStrikeOrb)", new string[1] { "SimpleLightningStrikeOrb" }); Add(cfg, "5. Proc limits", "AtG Missile", "missiles (MissileProjectile)", new string[1] { "MissileProjectile" }); Add(cfg, "5. Proc limits", "Sticky Bomb", "bombs (StickyBomb)", new string[1] { "StickyBomb" }); Add(cfg, "5. Proc limits", "Molten Perforator", "fireballs (FireMeatBall)", new string[1] { "FireMeatBall" }); Add(cfg, "5. Proc limits", "Ceremonial Dagger", "daggers (DaggerProjectile)", new string[1] { "DaggerProjectile" }); Add(cfg, "5. Proc limits", "Bundle of Fireworks", "fireworks (FireworkProjectile, tripled by Pocket I.C.B.M.)", new string[1] { "FireworkProjectile" }); } private static void Add(ConfigFile cfg, string section, string item, string what, string[] keys) { ConfigEntry<int> val = cfg.Bind<int>(section, item + " per second", 0, "0 = no limit. At most this many " + item + " " + what + " may be created per second (host only). Covers every copy of the item, including modded quality tiers. Fewer procs means less damage."); Limits.Add(new Limit { Label = item, Keys = keys, PerSecond = val }); foreach (string key in keys) { byKey[key] = val; } } public static void Install(Harmony h) { Type type = TypeFinder.Find("RoR2.Orbs.OrbManager"); if