using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using UnityEngine; using UnityEngine.Rendering; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName = ".NET Standard 2.0")] [assembly: AssemblyCompany("TheGodRunTheoryCompat")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("2.0.2.0")] [assembly: AssemblyInformationalVersion("2.0.2+225a7ea07af57655f16663f5d88f42a5b0427f82")] [assembly: AssemblyProduct("TheGodRunTheoryCompat")] [assembly: AssemblyTitle("TheGodRunTheoryCompat")] [assembly: AssemblyVersion("2.0.2.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace TheGodRunTheoryCompat { internal static class ClassicItemsPatch { private const string Clover = "ClassicItemsReturns.Items.Uncommon.ElitesDropItems"; private const string ArmsRace = "ClassicItemsReturns.Items.Uncommon.ArmsRace"; private const string EnergyCell = "ClassicItemsReturns.Items.Uncommon.EnergyCell"; private const string FireShield = "ClassicItemsReturns.Items.Common.FireShield"; private const string Jewel = "ClassicItemsReturns.Items.Uncommon.Jewel"; private const string Medallion = "ClassicItemsReturns.Items.Uncommon.RoyalMedallion"; private const string RustyJetpack = "ClassicItemsReturns.Items.Uncommon.RustyJetpack"; private const string SmartShopper = "ClassicItemsReturns.Items.Uncommon.SmartShopper"; private const string SnakeEyes = "ClassicItemsReturns.Items.Common.SnakeEyes"; private const string SnakeTracker = "ClassicItemsReturns.Items.Common.SnakeEyes+MasterSnakeEyesTracker"; private const string Thallium = "ClassicItemsReturns.Items.Rare.Thallium"; private const string ToughTimes = "ClassicItemsReturns.Items.Uncommon.BearArmor"; private static int cloverConstantReplacements; private static int energyCellConstantReplacements; private static int fireShieldThresholdReplacements; private static int fireShieldStackInjections; private static int rustyJetpackConstantReplacements; private static int snakeEyesCapReplacements; internal static void Install(Harmony harmony) { ApplyFieldsAndLanguage(); cloverConstantReplacements = 0; Patch(harmony, "ClassicItemsReturns.Items.Uncommon.ElitesDropItems", "RollForMaster", "CloverTranspiler"); if (cloverConstantReplacements != 2) { throw new InvalidOperationException($"56 Leaf Clover gameplay patch expected 2 constants but replaced {cloverConstantReplacements}."); } energyCellConstantReplacements = 0; Patch(harmony, "ClassicItemsReturns.Items.Uncommon.EnergyCell", "RecalculateStatsAPI_GetStatCoefficients", "EnergyCellTranspiler"); Patch(harmony, "ClassicItemsReturns.Items.Uncommon.EnergyCell+EnergyCellBehavior", "FixedUpdate", "EnergyCellTranspiler"); RequireCount("Energy Cell gameplay", energyCellConstantReplacements, 6); fireShieldThresholdReplacements = 0; fireShieldStackInjections = 0; Patch(harmony, "ClassicItemsReturns.Items.Common.FireShield", "ProcFireShield", "FireShieldTranspiler"); RequireCount("Fire Shield threshold", fireShieldThresholdReplacements, 1); RequireCount("Fire Shield stack scaling", fireShieldStackInjections, 1); rustyJetpackConstantReplacements = 0; Patch(harmony, "ClassicItemsReturns.Items.Uncommon.RustyJetpack", "RecalculateStatsAPI_GetStatCoefficients", "RustyJetpackTranspiler"); RequireCount("Rusty Jetpack gameplay", rustyJetpackConstantReplacements, 2); snakeEyesCapReplacements = 0; Patch(harmony, "ClassicItemsReturns.Items.Common.SnakeEyes+MasterSnakeEyesTracker", "Increment", "SnakeEyesCapTranspiler"); RequireCount("Snake Eyes stage cap", snakeEyesCapReplacements, 1); } internal static void ApplyFieldsAndLanguage() { BalanceConfig balance = Plugin.Balance; if (balance != null && !(ReflectionUtil.FindType("ClassicItemsReturns.Items.Uncommon.ArmsRace", "com.RiskySleeps.ClassicItemsReturns") == null)) { Set("ClassicItemsReturns.Items.Uncommon.ArmsRace", "cooldown", balance.ArmsRaceCooldown.Value); Set("ClassicItemsReturns.Items.Uncommon.ArmsRace", "missileCount", balance.ArmsRaceMissiles.Value); Set("ClassicItemsReturns.Items.Uncommon.ArmsRace", "missileCountPerStack", balance.ArmsRaceMissilesPerStack.Value); Set("ClassicItemsReturns.Items.Uncommon.ArmsRace", "damageCoeff", balance.ArmsRaceDamage.Value); ReflectionUtil.SetStaticField("ClassicItemsReturns.Items.Common.FireShield", "explosionDamageCoefficient", balance.FireShieldExplosion.Value, "com.RiskySleeps.ClassicItemsReturns"); ReflectionUtil.SetStaticField("ClassicItemsReturns.Items.Common.FireShield", "igniteDamageCoefficient", balance.FireShieldBurnBase.Value, "com.RiskySleeps.ClassicItemsReturns"); Set("ClassicItemsReturns.Items.Uncommon.Jewel", "moneyToGrant", balance.JewelGold.Value); Set("ClassicItemsReturns.Items.Uncommon.Jewel", "barrierInitial", balance.JewelBarrier.Value); Set("ClassicItemsReturns.Items.Uncommon.Jewel", "barrierStack", balance.JewelBarrierPerStack.Value); Set("ClassicItemsReturns.Items.Uncommon.RoyalMedallion", "procChance", balance.MedallionChance.Value); Set("ClassicItemsReturns.Items.Uncommon.RoyalMedallion", "buffDuration", balance.MedallionDuration.Value); Set("ClassicItemsReturns.Items.Uncommon.RoyalMedallion", "buffDurationStack", balance.MedallionDurationPerStack.Value); Set("ClassicItemsReturns.Items.Uncommon.SmartShopper", "moneyMult", balance.ShopperGold.Value); Set("ClassicItemsReturns.Items.Uncommon.SmartShopper", "moneyMultStack", balance.ShopperGold.Value); Set("ClassicItemsReturns.Items.Common.SnakeEyes", "critChance", balance.SnakeCrit.Value); Set("ClassicItemsReturns.Items.Common.SnakeEyes", "critDamageStack", balance.SnakeCritDamagePerStack.Value); Set("ClassicItemsReturns.Items.Rare.Thallium", "chance", balance.ThalliumChance.Value); Set("ClassicItemsReturns.Items.Rare.Thallium", "enemyAttackDamageCoef", balance.ThalliumDamage.Value); Set("ClassicItemsReturns.Items.Rare.Thallium", "enemyMoveSpeedCoef", balance.ThalliumSlow.Value); Set("ClassicItemsReturns.Items.Rare.Thallium", "duration", balance.ThalliumDuration.Value); Set("ClassicItemsReturns.Items.Rare.Thallium", "durationPerStack", balance.ThalliumDurationPerStack.Value); Set("ClassicItemsReturns.Items.Uncommon.BearArmor", "armor", balance.ToughTimesArmor.Value); Set("ClassicItemsReturns.Items.Uncommon.BearArmor", "armorPerStack", balance.ToughTimesArmor.Value); ReflectionUtil.AddLanguage("CLASSICITEMSRETURNS_ITEM_56LEAFCLOVER_DESCRIPTION", $"Elite enemies have a {balance.CloverBaseChance.Value:0.##}% chance (+{balance.CloverStackChance.Value:0.##}% chance per stack) to drop an item on death. Unaffected by luck."); ReflectionUtil.AddLanguage("CLASSICITEMSRETURNS_ITEM_ARMSRACE_DESCRIPTION", $"Drones periodically fire a barrage of {balance.ArmsRaceMissiles.Value} (+{balance.ArmsRaceMissilesPerStack.Value} per stack) missiles that deal {balance.ArmsRaceDamage.Value * 100f:0.#}% damage. Recharges every {balance.ArmsRaceCooldown.Value:0.##} seconds."); ReflectionUtil.AddLanguage("CLASSICITEMSRETURNS_ITEM_ENERGYCELL_DESCRIPTION", $"Increase attack speed by {balance.EnergyCellHealthy.Value * 100f:0.#}% per stack, increasing by up to another {balance.EnergyCellLowHealthExtra.Value * 100f:0.#}% per stack as health falls."); ReflectionUtil.AddLanguage("CLASSICITEMSRETURNS_ITEM_FIRESHIELD_DESCRIPTION", $"After taking more than {balance.FireShieldThreshold.Value * 100f:0.#}% of your maximum health as damage, explode for {balance.FireShieldExplosion.Value * 100f:0.#}% base damage. Additionally, enemies burn for {balance.FireShieldBurnBase.Value * 100f:0.#}% (+{balance.FireShieldBurnPerStack.Value * 100f:0.#}% per extra stack) base damage."); ReflectionUtil.AddLanguage("CLASSICITEMSRETURNS_ITEM_JEWEL_DESCRIPTION", $"Activating an interactable grants a temporary barrier for {balance.JewelBarrier.Value * 100f:0.#}% (+{balance.JewelBarrierPerStack.Value * 100f:0.#}% per stack) of maximum health and ${balance.JewelGold.Value}. Scales with time."); ReflectionUtil.AddLanguage("CLASSICITEMSRETURNS_ITEM_ROYALMEDALLION_DESCRIPTION", $"{balance.MedallionChance.Value:0.##}% chance on hitting a boss to drop a buffing orb that improves health regeneration, attack speed, movement speed, and base damage for {balance.MedallionDuration.Value:0.##}s (+{balance.MedallionDurationPerStack.Value:0.##}s per stack)."); ReflectionUtil.AddLanguage("CLASSICITEMSRETURNS_ITEM_RUSTYJETPACK_DESCRIPTION", $"Increase jump height by {balance.RustyJump.Value * 100f:0.#}% (+{balance.RustyJump.Value * 100f:0.#}% per stack) and gain a short airhop."); ReflectionUtil.AddLanguage("CLASSICITEMSRETURNS_ITEM_SMARTSHOPPER_DESCRIPTION", $"Enemies drop an extra {balance.ShopperGold.Value * 100f:0.#}% (+{balance.ShopperGold.Value * 100f:0.#}% per stack) gold on kill."); ReflectionUtil.AddLanguage("CLASSICITEMSRETURNS_ITEM_SNAKEEYES_DESCRIPTION", $"Using a shrine increases critical chance by {balance.SnakeCrit.Value:0.##}% and critical damage by 0% (+{balance.SnakeCritDamagePerStack.Value * 100f:0.##}% per item stack), up to {Math.Max(1, Math.Min(6, balance.SnakeStageActivations.Value))} times. Resets each stage."); ReflectionUtil.AddLanguage("CLASSICITEMSRETURNS_ITEM_THALLIUM_DESCRIPTION", $"On hit, {balance.ThalliumChance.Value:0.##}% chance to poison using {balance.ThalliumDamage.Value * 100f:0.#}% damage scaling and slow movement by {balance.ThalliumSlow.Value * 100f:0.#}% for {balance.ThalliumDuration.Value:0.##} seconds."); ReflectionUtil.AddLanguage("CLASSICITEMSRETURNS_ITEM_TOUGHTIMES_DESCRIPTION", $"Grants +{balance.ToughTimesArmor.Value:0.#} armor (+{balance.ToughTimesArmor.Value:0.#} per stack)."); } } private static void Set(string type, string field, object value) { ReflectionUtil.SetInstanceField(type, field, value, "com.RiskySleeps.ClassicItemsReturns"); } private static void Patch(Harmony harmony, string typeName, string methodName, string transpilerName) { //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.Method(ReflectionUtil.RequireType(typeName, "com.RiskySleeps.ClassicItemsReturns"), methodName, (Type[])null, (Type[])null) ?? throw new MissingMethodException(typeName, methodName); MethodInfo methodInfo2 = AccessTools.Method(typeof(ClassicItemsPatch), transpilerName, (Type[])null, (Type[])null); harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(methodInfo2), (HarmonyMethod)null, (HarmonyMethod)null); } private static IEnumerable CloverTranspiler(IEnumerable instructions) { foreach (CodeInstruction instruction in instructions) { if (instruction.opcode == OpCodes.Ldc_R4 && instruction.operand is float left) { if (Nearly(left, 4.5f)) { instruction.operand = Plugin.Balance.CloverBaseChance.Value; } else { if (!Nearly(left, 1.5f)) { yield return instruction; continue; } instruction.operand = Plugin.Balance.CloverStackChance.Value; } cloverConstantReplacements++; } yield return instruction; } } private static IEnumerable EnergyCellTranspiler(IEnumerable instructions) { foreach (CodeInstruction instruction in instructions) { if (instruction.opcode == OpCodes.Ldc_R4 && instruction.operand is float left) { if (Nearly(left, 0.2f)) { instruction.operand = Plugin.Balance.EnergyCellHealthy.Value; } else { if (!Nearly(left, 0.4f)) { yield return instruction; continue; } instruction.operand = Plugin.Balance.EnergyCellLowHealthExtra.Value; } energyCellConstantReplacements++; } yield return instruction; } } private static IEnumerable FireShieldTranspiler(IEnumerable source) { //IL_00e2: Unknown result type (might be due to invalid IL or missing references) //IL_00ec: Expected O, but got Unknown List list = new List(source); bool flag = false; for (int i = 0; i < list.Count; i++) { CodeInstruction val = list[i]; if (val.opcode == OpCodes.Ldc_R4 && val.operand is float left && Nearly(left, 0.049f)) { val.operand = Math.Max(0f, Plugin.Balance.FireShieldThreshold.Value - 0.001f); fireShieldThresholdReplacements++; } if (CodeInstructionExtensions.LoadsField(val, AccessTools.Field(ReflectionUtil.RequireType("ClassicItemsReturns.Items.Common.FireShield", "com.RiskySleeps.ClassicItemsReturns"), "igniteDamageCoefficient"), false)) { flag = true; } else if (flag && val.opcode == OpCodes.Conv_R4) { list.Insert(i + 1, new CodeInstruction(OpCodes.Call, (object)AccessTools.Method(typeof(ClassicItemsPatch), "FireShieldStackMultiplier", (Type[])null, (Type[])null))); flag = false; fireShieldStackInjections++; i++; } } return list; } private static float FireShieldStackMultiplier(float stacks) { return 1f + Plugin.Balance.FireShieldBurnPerStack.Value * Math.Max(0f, stacks - 1f); } private static IEnumerable RustyJetpackTranspiler(IEnumerable instructions) { foreach (CodeInstruction instruction in instructions) { if (instruction.opcode == OpCodes.Ldc_R4 && instruction.operand is float left && Nearly(left, 0.25f)) { instruction.operand = Plugin.Balance.RustyJump.Value; rustyJetpackConstantReplacements++; } yield return instruction; } } private static IEnumerable SnakeEyesCapTranspiler(IEnumerable instructions) { int cap = Math.Max(1, Math.Min(6, Plugin.Balance.SnakeStageActivations.Value)); foreach (CodeInstruction instruction in instructions) { if (instruction.opcode == OpCodes.Ldc_I4_6) { instruction.opcode = OpCodes.Ldc_I4; instruction.operand = cap; snakeEyesCapReplacements++; } yield return instruction; } } private static bool Nearly(float left, float right) { return Math.Abs(left - right) < 0.0001f; } private static void RequireCount(string patchName, int actual, int expected) { if (actual != expected) { throw new InvalidOperationException($"{patchName} patch expected {expected} replacement(s) but made {actual}."); } } } internal static class CloudburstPatch { private const string Glass = "Cloudburst.Items.Gray.GlassHarvester"; private const string Bismuth = "Cloudburst.Items.Green.BismuthEarrings"; private const string Fabinhoru = "Cloudburst.Items.Green.FabinhoruDagger"; private const string Japes = "Cloudburst.Items.Green.JapesCloak"; private const string Keycard = "Cloudburst.Items.Green.EnigmaticKeycard"; private static int glassReplacements; private static int japesArmorReplacements; private static int japesHealingReplacements; private static int japesDurationReplacements; private static int keycardDamageInjections; internal static void Install(Harmony harmony) { if (!(ReflectionUtil.FindType("Cloudburst.Items.Green.BismuthEarrings", "CloudBurstTeam.Cloudburst") == null)) { ApplyFieldsAndLanguage(); glassReplacements = 0; PatchTranspiler(harmony, "Cloudburst.Items.Gray.GlassHarvester", "RecalculateStatsAPI_GetStatCoefficients", "GlassTranspiler"); RequireCount("Glass Harvester", glassReplacements, 2); PatchPostfix(harmony, "Cloudburst.Items.Green.FabinhoruDagger", "RecalculateStatsAPI_GetStatCoefficients", "FabinhoruPostfix"); japesArmorReplacements = 0; PatchTranspiler(harmony, "Cloudburst.Items.Green.JapesCloak", "RecalculateStatsAPI_GetStatCoefficients", "JapesArmorTranspiler"); RequireCount("Jape's Cloak armor", japesArmorReplacements, 1); japesHealingReplacements = 0; PatchTranspiler(harmony, "Cloudburst.Items.Green.JapesCloak", "HealthComponent_Heal", "JapesHealingTranspiler"); RequireCount("Jape's Cloak healing", japesHealingReplacements, 1); japesDurationReplacements = 0; PatchTranspiler(harmony, "Cloudburst.Items.Green.JapesCloak", "GenericPickupController_AttemptGrant", "JapesDurationTranspiler"); RequireCount("Jape's Cloak duration", japesDurationReplacements, 1); keycardDamageInjections = 0; PatchTranspiler(harmony, "Cloudburst.Items.Green.EnigmaticKeycard", "GlobalEventManager_OnHitEnemy", "KeycardDamageTranspiler"); RequireCount("Enigmatic Keycard damage", keycardDamageInjections, 1); } } internal static void ApplyFieldsAndLanguage() { BalanceConfig balance = Plugin.Balance; if (balance != null && !(ReflectionUtil.FindType("Cloudburst.Items.Green.BismuthEarrings", "CloudBurstTeam.Cloudburst") == null)) { float num = Math.Max(0f, balance.CloudburstScale.Value); ReflectionUtil.SetStaticField("Cloudburst.Items.Green.BismuthEarrings", "BaseBarrier", 15f * num, "CloudBurstTeam.Cloudburst"); ReflectionUtil.SetStaticField("Cloudburst.Items.Green.BismuthEarrings", "StackingBarrier", 10f * num, "CloudBurstTeam.Cloudburst"); ReflectionUtil.SetStaticField("Cloudburst.Items.Green.EnigmaticKeycard", "Chance", 4f * num, "CloudBurstTeam.Cloudburst"); ReflectionUtil.AddLanguage("ITEM_GLASSHARVESTER_DESC", $"Gain 5% critical strike chance. Critical Strikes deal an additional {40f * num:0.#}% damage (+{30f * num:0.#}% per stack). Taking damage below 25% health breaks this item and grants experience."); ReflectionUtil.AddLanguage("ITEM_BARRIERONCRIT_DESCRIPTION", $"Gain 5% bleed chance. Applying bleed grants {15f * num:0.#} barrier (+{10f * num:0.#} per stack)."); ReflectionUtil.AddLanguage("ITEM_PICKUPBUFF_DESCRIPTION", $"Picking up an item grants a stacking buff for {10f * balance.JapesDurationScale.Value:0.#} seconds. Each buff retains Jape's original stacking rules with its armor and healing values reduced by {100f * (1f - num):0.#}%."); ReflectionUtil.AddLanguage("ITEM_ENIGMATICKEYCARD_DESCRIPTION", $"{4f * num:0.#}% chance on hit to spawn a seeking orb. Its original complete damage calculation is multiplied by {num:0.##}."); } } private static void PatchTranspiler(Harmony harmony, string typeName, string methodName, string patchName) { //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.Method(ReflectionUtil.RequireType(typeName, "CloudBurstTeam.Cloudburst"), methodName, (Type[])null, (Type[])null) ?? throw new MissingMethodException(typeName, methodName); harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(AccessTools.Method(typeof(CloudburstPatch), patchName, (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null); } private static void PatchPostfix(Harmony harmony, string typeName, string methodName, string patchName) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.Method(ReflectionUtil.RequireType(typeName, "CloudBurstTeam.Cloudburst"), methodName, (Type[])null, (Type[])null) ?? throw new MissingMethodException(typeName, methodName); harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(AccessTools.Method(typeof(CloudburstPatch), patchName, (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } private static IEnumerable GlassTranspiler(IEnumerable instructions) { float scale = Math.Max(0f, Plugin.Balance.CloudburstScale.Value); foreach (CodeInstruction instruction in instructions) { if (instruction.opcode == OpCodes.Ldc_R4 && instruction.operand is float left) { if (Nearly(left, 0.3f)) { instruction.operand = 0.3f * scale; } else { if (!Nearly(left, 0.1f)) { yield return instruction; continue; } instruction.operand = 0.1f * scale; } glassReplacements++; } yield return instruction; } } private static void FabinhoruPostfix(object sender, object args) { object staticField = ReflectionUtil.GetStaticField("Cloudburst.Items.Green.FabinhoruDagger", "fabinhorusBuff", "CloudBurstTeam.Cloudburst"); int buffCount = ReflectionUtil.GetBuffCount(sender, staticField); if (buffCount > 0) { float num = Math.Max(0f, Plugin.Balance.CloudburstScale.Value); float num2 = (float)buffCount * 15f + 15f; ReflectionUtil.AddFloatMember(args, "armorAdd", num2 * (1f - num)); } } private static IEnumerable JapesArmorTranspiler(IEnumerable instructions) { return ReplaceFloat(instructions, 5f, 5f * Math.Max(0f, Plugin.Balance.CloudburstScale.Value), delegate { japesArmorReplacements++; }); } private static IEnumerable JapesHealingTranspiler(IEnumerable instructions) { float num = Math.Max(0f, Plugin.Balance.CloudburstScale.Value); return ReplaceFloat(instructions, 1.3f, 1f + 0.3f * num, delegate { japesHealingReplacements++; }); } private static IEnumerable JapesDurationTranspiler(IEnumerable instructions) { return ReplaceFloat(instructions, 10f, 10f * Math.Max(0f, Plugin.Balance.JapesDurationScale.Value), delegate { japesDurationReplacements++; }); } private static IEnumerable KeycardDamageTranspiler(IEnumerable source) { //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Expected O, but got Unknown //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Expected O, but got Unknown List list = new List(source); FieldInfo fieldInfo = AccessTools.Field(ReflectionUtil.RequireType("Cloudburst.Items.Green.EnigmaticKeycard", "CloudBurstTeam.Cloudburst"), "BaseDamage"); bool flag = false; for (int i = 0; i < list.Count; i++) { if (CodeInstructionExtensions.LoadsField(list[i], fieldInfo, false)) { flag = true; } else if (flag && list[i].opcode == OpCodes.Add) { list.Insert(i + 1, new CodeInstruction(OpCodes.Ldc_R4, (object)Math.Max(0f, Plugin.Balance.CloudburstScale.Value))); list.Insert(i + 2, new CodeInstruction(OpCodes.Mul, (object)null)); keycardDamageInjections++; break; } } return list; } private static IEnumerable ReplaceFloat(IEnumerable instructions, float oldValue, float newValue, Action replaced) { foreach (CodeInstruction instruction in instructions) { if (instruction.opcode == OpCodes.Ldc_R4 && instruction.operand is float left && Nearly(left, oldValue)) { instruction.operand = newValue; replaced(); } yield return instruction; } } private static bool Nearly(float left, float right) { return Math.Abs(left - right) < 0.0001f; } private static void RequireCount(string patchName, int actual, int expected) { if (actual != expected) { throw new InvalidOperationException($"{patchName} patch expected {expected} replacement(s) but made {actual}."); } } } internal static class LotusGuard { private const string LotusType = "vanillaVoid.Items.CrystalLotus"; private static bool logged; internal static void Install(Harmony harmony) { //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Expected O, but got Unknown Type type = ReflectionUtil.FindType("vanillaVoid.Items.CrystalLotus", "com.Zenithrium.vanillaVoid"); if (!(type == null)) { MethodInfo methodInfo = AccessTools.Method(type, "LotusTeamBuff", (Type[])null, (Type[])null) ?? throw new MissingMethodException("vanillaVoid.Items.CrystalLotus", "LotusTeamBuff"); harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(AccessTools.Method(typeof(LotusGuard), "Finalizer", (Type[])null, (Type[])null)), (HarmonyMethod)null); } } private static Exception Finalizer(Exception __exception) { if (__exception is NullReferenceException) { if (!logged) { logged = true; Plugin.Instance.Log.LogWarning((object)"Crystalline Lotus skipped one invalid/destroyed visual target. Valid ward behavior remains active."); } return null; } return __exception; } } [HarmonyPatch] internal static class OppressiveAuraPhysicsPatch { private const string BehaviourTypeName = "NemesisRisingTides.Contents.Oppressive+AffixOppressive+NemesisAffixOppressiveBehaviour"; private static MethodBase TargetMethod() { Type type = ReflectionUtil.FindType("NemesisRisingTides.Contents.Oppressive+AffixOppressive+NemesisAffixOppressiveBehaviour", "prodzpod.NemesisRisingTides"); if (!(type == null)) { return AccessTools.Method(type, "FixedUpdate", (Type[])null, (Type[])null); } return null; } private static IEnumerable Transpiler(IEnumerable instructions) { MethodInfo overlapCapsule = AccessTools.Method(typeof(Physics), "OverlapCapsule", new Type[3] { typeof(Vector3), typeof(Vector3), typeof(float) }, (Type[])null); MethodInfo overlapBubble = AccessTools.Method(typeof(OppressiveAuraPhysicsPatch), "OverlapBubble", (Type[])null, (Type[])null); bool replaced = false; foreach (CodeInstruction instruction in instructions) { if (instruction.opcode == OpCodes.Call && object.Equals(instruction.operand, overlapCapsule)) { instruction.operand = overlapBubble; replaced = true; } yield return instruction; } if (!replaced) { throw new InvalidOperationException("Nemesis Rising Tides Oppressive OverlapCapsule call was not found."); } } private static Collider[] OverlapBubble(Vector3 center, Vector3 ignoredEndpoint, float radius) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) return Physics.OverlapSphere(center, radius); } } [HarmonyPatch] internal static class OppressiveAuraVisualPatch { private const string BehaviourTypeName = "NemesisRisingTides.Contents.Oppressive+AffixOppressive+NemesisAffixOppressiveBehaviour"; private static Mesh bubbleMesh; private static MethodBase TargetMethod() { Type type = ReflectionUtil.FindType("NemesisRisingTides.Contents.Oppressive+AffixOppressive+NemesisAffixOppressiveBehaviour", "prodzpod.NemesisRisingTides"); if (!(type == null)) { return AccessTools.Method(type, "Start", (Type[])null, (Type[])null); } return null; } private static void Postfix(object __instance) { if (__instance == null) { return; } object? obj = AccessTools.Field(__instance.GetType(), "aura")?.GetValue(__instance); Component val = (Component)((obj is Component) ? obj : null); if (!((Object)(object)val == (Object)null)) { EnsureBubbleMesh(); if (!((Object)(object)bubbleMesh == (Object)null)) { SetBubbleMesh(val.transform.Find("SphereOuter")); SetBubbleMesh(val.transform.Find("SphereInner")); } } } private static void EnsureBubbleMesh() { if (!((Object)(object)bubbleMesh != (Object)null)) { GameObject obj = GameObject.CreatePrimitive((PrimitiveType)0); MeshFilter component = obj.GetComponent(); bubbleMesh = (((Object)(object)component == (Object)null) ? null : component.sharedMesh); Object.Destroy((Object)(object)obj); } } private static void SetBubbleMesh(Transform visual) { //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)visual == (Object)null)) { MeshFilter component = ((Component)visual).GetComponent(); if ((Object)(object)component != (Object)null) { component.sharedMesh = bubbleMesh; } visual.localPosition = Vector3.zero; visual.localRotation = Quaternion.identity; visual.localScale = Vector3.one * 2f; } } } internal static class PerformancePatches { private const string EffectComponentType = "RoR2.EffectComponent"; private const string CorpseType = "RoR2.Corpse"; private const string DamageNumberManagerType = "RoR2.DamageNumberManager"; private static readonly BindingFlags AnyInstance = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static int? originalCorpseLimit; internal static void Install(Harmony harmony) { PatchPostfix(harmony, "RoR2.EffectComponent", "Start", "EffectComponentStartPostfix"); PatchPostfix(harmony, "RoR2.DamageNumberManager", "Awake", "DamageNumberManagerAwakePostfix"); Plugin.Balance.PerformanceEnabled.SettingChanged += SettingsChanged; Plugin.Balance.MaximumCorpses.SettingChanged += SettingsChanged; ApplyGlobalSettings(); } internal static void RegisterRiskOfOptions() { Type type = ReflectionUtil.RequireType("RiskOfOptions.ModSettingsManager", "com.rune580.riskofoptions"); Type optionType = ReflectionUtil.RequireType("RiskOfOptions.Options.CheckBoxOption", "com.rune580.riskofoptions"); Type optionType2 = ReflectionUtil.RequireType("RiskOfOptions.Options.IntSliderOption", "com.rune580.riskofoptions"); MethodInfo methodInfo = type.GetMethod("SetModDescription", BindingFlags.Static | BindingFlags.Public, null, new Type[3] { typeof(string), typeof(string), typeof(string) }, null) ?? throw new MissingMethodException(type.FullName, "SetModDescription"); MethodInfo methodInfo2 = null; MethodInfo[] methods = type.GetMethods(BindingFlags.Static | BindingFlags.Public); foreach (MethodInfo methodInfo3 in methods) { ParameterInfo[] parameters = methodInfo3.GetParameters(); if (methodInfo3.Name == "AddOption" && parameters.Length == 3 && parameters[1].ParameterType == typeof(string) && parameters[2].ParameterType == typeof(string)) { methodInfo2 = methodInfo3; break; } } if (methodInfo2 == null) { throw new MissingMethodException(type.FullName, "AddOption(option, modGuid, modName)"); } methodInfo.Invoke(null, new object[3] { "Pack compatibility, balance, stability, and optional client performance controls.", "com.lucie.thegodruntheorycompat", "The GodRun Theory Compatibility" }); AddOption(methodInfo2, optionType, (ConfigEntryBase)(object)Plugin.Balance.PerformanceEnabled); AddOption(methodInfo2, optionType, (ConfigEntryBase)(object)Plugin.Balance.EffectRendererOptimization); AddOption(methodInfo2, optionType2, (ConfigEntryBase)(object)Plugin.Balance.MaximumParticles); AddOption(methodInfo2, optionType2, (ConfigEntryBase)(object)Plugin.Balance.MaximumCorpses); AddOption(methodInfo2, optionType2, (ConfigEntryBase)(object)Plugin.Balance.MaximumDamageNumbers); AddOption(methodInfo2, optionType, (ConfigEntryBase)(object)Plugin.Balance.DisableEffectLightShadows); } private static void AddOption(MethodInfo addOption, Type optionType, ConfigEntryBase entry) { object obj = (optionType.GetConstructor(new Type[1] { ((object)entry).GetType() }) ?? throw new MissingMethodException(optionType.FullName, ".ctor(" + ((object)entry).GetType().Name + ")")).Invoke(new object[1] { entry }); addOption.Invoke(null, new object[3] { obj, "com.lucie.thegodruntheorycompat", "The GodRun Theory Compatibility" }); } private static void SettingsChanged(object sender, EventArgs eventArgs) { ApplyGlobalSettings(); } private static void ApplyGlobalSettings() { FieldInfo fieldInfo = ReflectionUtil.RequireType("RoR2.Corpse").GetField("maxCorpses", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) ?? throw new MissingFieldException("RoR2.Corpse", "maxCorpses"); if (!originalCorpseLimit.HasValue) { originalCorpseLimit = (int)fieldInfo.GetValue(null); } int num = (Plugin.Balance.PerformanceEnabled.Value ? Math.Max(0, Plugin.Balance.MaximumCorpses.Value) : originalCorpseLimit.Value); fieldInfo.SetValue(null, num); } private static void EffectComponentStartPostfix(object __instance) { if (Plugin.Balance.PerformanceEnabled.Value) { Component val = (Component)((__instance is Component) ? __instance : null); if (val != null) { OptimizeEffect(val.gameObject); } } } private static void DamageNumberManagerAwakePostfix(object __instance) { if (Plugin.Balance.PerformanceEnabled.Value && __instance != null) { FieldInfo field = __instance.GetType().GetField("maxDamageNums", AnyInstance); if (!(field == null)) { int val = (int)field.GetValue(__instance); int val2 = Math.Max(0, Plugin.Balance.MaximumDamageNumbers.Value); field.SetValue(__instance, Math.Min(val, val2)); } } } private static void OptimizeEffect(GameObject effect) { //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) if ((Object)(object)effect == (Object)null) { return; } if (Plugin.Balance.EffectRendererOptimization.Value) { int num = Math.Max(1, Plugin.Balance.MaximumParticles.Value); ParticleSystem[] componentsInChildren = effect.GetComponentsInChildren(true); for (int i = 0; i < componentsInChildren.Length; i++) { MainModule main = componentsInChildren[i].main; if (((MainModule)(ref main)).maxParticles > num) { ((MainModule)(ref main)).maxParticles = num; } } Renderer[] componentsInChildren2 = effect.GetComponentsInChildren(true); foreach (Renderer val in componentsInChildren2) { if (val is ParticleSystemRenderer || val is TrailRenderer || val is LineRenderer) { val.shadowCastingMode = (ShadowCastingMode)0; val.receiveShadows = false; val.motionVectorGenerationMode = (MotionVectorGenerationMode)2; } } } if (Plugin.Balance.DisableEffectLightShadows.Value) { Light[] componentsInChildren3 = effect.GetComponentsInChildren(true); for (int i = 0; i < componentsInChildren3.Length; i++) { componentsInChildren3[i].shadows = (LightShadows)0; } } } private static void PatchPostfix(Harmony harmony, string typeName, string methodName, string patchName) { //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.Method(ReflectionUtil.RequireType(typeName), methodName, (Type[])null, (Type[])null) ?? throw new MissingMethodException(typeName, methodName); MethodInfo methodInfo2 = AccessTools.Method(typeof(PerformancePatches), patchName, (Type[])null, (Type[])null) ?? throw new MissingMethodException(typeof(PerformancePatches).FullName, patchName); harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(methodInfo2), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } } [BepInPlugin("com.lucie.thegodruntheorycompat", "The GodRun Theory Compatibility", "2.0.2")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] public sealed class Plugin : BaseUnityPlugin { public const string PluginGuid = "com.lucie.thegodruntheorycompat"; public const string PluginName = "The GodRun Theory Compatibility"; public const string PluginVersion = "2.0.2"; internal const string ClassicItemsReturnsGuid = "com.RiskySleeps.ClassicItemsReturns"; internal const string CloudburstGuid = "CloudBurstTeam.Cloudburst"; internal const string VanillaVoidGuid = "com.Zenithrium.vanillaVoid"; internal const string NemesisRisingTidesGuid = "prodzpod.NemesisRisingTides"; internal const string RiskOfOptionsGuid = "com.rune580.riskofoptions"; private Harmony harmony; internal static Plugin Instance { get; private set; } internal static BalanceConfig Balance { get; private set; } internal ManualLogSource Log => ((BaseUnityPlugin)this).Logger; private void Awake() { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Expected O, but got Unknown Instance = this; Balance = new BalanceConfig(((BaseUnityPlugin)this).Config); harmony = new Harmony("com.lucie.thegodruntheorycompat"); InstallOptional("Oppressive spherical aura", delegate { PatchClass(typeof(OppressiveAuraPhysicsPatch)); PatchClass(typeof(OppressiveAuraVisualPatch)); }); InstallOptional("ClassicItemsReturns balance", delegate { ClassicItemsPatch.Install(harmony); }); InstallOptional("Cloudburst balance", delegate { CloudburstPatch.Install(harmony); }); InstallOptional("Crystalline Lotus null guard", delegate { LotusGuard.Install(harmony); }); InstallOptional("base-game runtime stability guards", delegate { RuntimeStabilityPatches.Install(harmony); }); InstallOptional("client performance controls", delegate { PerformancePatches.Install(harmony); }); InstallOptional("Risk of Options integration", PerformancePatches.RegisterRiskOfOptions); ((BaseUnityPlugin)this).Logger.LogInfo((object)"The GodRun Theory Compatibility 2.0.2 initialized. Numeric patches are event-driven and perform no per-frame inventory scans."); } private void Start() { ClassicItemsPatch.ApplyFieldsAndLanguage(); CloudburstPatch.ApplyFieldsAndLanguage(); } private void PatchClass(Type patchType) { harmony.CreateClassProcessor(patchType).Patch(); } private void InstallOptional(string name, Action install) { try { install(); ((BaseUnityPlugin)this).Logger.LogInfo((object)("Applied: " + name)); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Skipped " + name + ": " + ex.GetType().Name + ": " + ex.Message)); } } private void OnDestroy() { Harmony obj = harmony; if (obj != null) { obj.UnpatchSelf(); } } } internal sealed class BalanceConfig { internal readonly ConfigEntry CloverBaseChance; internal readonly ConfigEntry CloverStackChance; internal readonly ConfigEntry ArmsRaceMissiles; internal readonly ConfigEntry ArmsRaceMissilesPerStack; internal readonly ConfigEntry ArmsRaceDamage; internal readonly ConfigEntry ArmsRaceCooldown; internal readonly ConfigEntry EnergyCellHealthy; internal readonly ConfigEntry EnergyCellLowHealthExtra; internal readonly ConfigEntry FireShieldThreshold; internal readonly ConfigEntry FireShieldExplosion; internal readonly ConfigEntry FireShieldBurnBase; internal readonly ConfigEntry FireShieldBurnPerStack; internal readonly ConfigEntry JewelBarrier; internal readonly ConfigEntry JewelBarrierPerStack; internal readonly ConfigEntry JewelGold; internal readonly ConfigEntry MedallionChance; internal readonly ConfigEntry MedallionDuration; internal readonly ConfigEntry MedallionDurationPerStack; internal readonly ConfigEntry RustyJump; internal readonly ConfigEntry ShopperGold; internal readonly ConfigEntry SnakeCrit; internal readonly ConfigEntry SnakeCritDamagePerStack; internal readonly ConfigEntry SnakeStageActivations; internal readonly ConfigEntry ThalliumChance; internal readonly ConfigEntry ThalliumDamage; internal readonly ConfigEntry ThalliumSlow; internal readonly ConfigEntry ThalliumDuration; internal readonly ConfigEntry ThalliumDurationPerStack; internal readonly ConfigEntry ToughTimesArmor; internal readonly ConfigEntry CloudburstScale; internal readonly ConfigEntry JapesDurationScale; internal readonly ConfigEntry EquipmentSlotGuard; internal readonly ConfigEntry DroneVendorGuard; internal readonly ConfigEntry DelusionChestGuard; internal readonly ConfigEntry PerformanceEnabled; internal readonly ConfigEntry EffectRendererOptimization; internal readonly ConfigEntry MaximumParticles; internal readonly ConfigEntry MaximumCorpses; internal readonly ConfigEntry MaximumDamageNumbers; internal readonly ConfigEntry DisableEffectLightShadows; internal BalanceConfig(ConfigFile config) { //IL_0489: Unknown result type (might be due to invalid IL or missing references) //IL_0493: Expected O, but got Unknown //IL_04b8: Unknown result type (might be due to invalid IL or missing references) //IL_04c2: Expected O, but got Unknown //IL_04ea: Unknown result type (might be due to invalid IL or missing references) //IL_04f4: Expected O, but got Unknown CloverBaseChance = config.Bind("ClassicItemsReturns - 56 Leaf Clover", "Base elite drop chance percent", 0.5f, "Chance with one stack. Requires a restart."); CloverStackChance = config.Bind("ClassicItemsReturns - 56 Leaf Clover", "Additional chance percent per stack", 0.1f, "Additional percentage points for each extra stack. Requires a restart."); ArmsRaceMissiles = config.Bind("ClassicItemsReturns - Arms Race", "Base missiles", 2, "Missiles in the first-stack barrage."); ArmsRaceMissilesPerStack = config.Bind("ClassicItemsReturns - Arms Race", "Missiles per extra stack", 1, "Additional missiles per stack."); ArmsRaceDamage = config.Bind("ClassicItemsReturns - Arms Race", "Damage coefficient", 0.6f, "1.0 means 100% base damage."); ArmsRaceCooldown = config.Bind("ClassicItemsReturns - Arms Race", "Recharge seconds", 15f, "Time required to reload a barrage."); EnergyCellHealthy = config.Bind("ClassicItemsReturns - Energy Cell", "Healthy attack speed per stack", 0.08f, "0.08 means 8%. Requires a restart."); EnergyCellLowHealthExtra = config.Bind("ClassicItemsReturns - Energy Cell", "Low health extra attack speed per stack", 0.16f, "Added progressively as health falls, producing 24% total at low health. Requires a restart."); FireShieldThreshold = config.Bind("ClassicItemsReturns - Fire Shield", "Trigger health fraction", 0.08f, "Fraction of maximum combined health lost in one hit. Requires a restart."); FireShieldExplosion = config.Bind("ClassicItemsReturns - Fire Shield", "Explosion damage coefficient", 1.2f, "1.2 means 120% base damage."); FireShieldBurnBase = config.Bind("ClassicItemsReturns - Fire Shield", "Burn base damage coefficient", 1f, "1.0 means 100% base damage."); FireShieldBurnPerStack = config.Bind("ClassicItemsReturns - Fire Shield", "Burn coefficient per extra stack", 0.75f, "0.75 means 75% additional base damage. Requires a restart."); JewelBarrier = config.Bind("ClassicItemsReturns - Locked Jewel", "Barrier fraction", 0.1f, "0.10 means 10% maximum health."); JewelBarrierPerStack = config.Bind("ClassicItemsReturns - Locked Jewel", "Barrier fraction per extra stack", 0.05f, "0.05 means 5% maximum health."); JewelGold = config.Bind("ClassicItemsReturns - Locked Jewel", "Base gold reward", 4, "Value before difficulty/time scaling."); MedallionChance = config.Bind("ClassicItemsReturns - Royal Medallion", "Orb chance percent", 5f, "Chance on a valid boss hit."); MedallionDuration = config.Bind("ClassicItemsReturns - Royal Medallion", "Buff duration", 6f, "Seconds with one item."); MedallionDurationPerStack = config.Bind("ClassicItemsReturns - Royal Medallion", "Duration per extra stack", 3f, "Additional seconds per stack."); RustyJump = config.Bind("ClassicItemsReturns - Rusty Jetpack", "Jump power per stack", 0.12f, "0.12 means 12%. Requires a restart."); ShopperGold = config.Bind("ClassicItemsReturns - Smart Shopper", "Extra enemy gold per stack", 0.1f, "0.10 means 10%."); SnakeCrit = config.Bind("ClassicItemsReturns - Snake Eyes", "Critical chance per activation", 3f, "Percentage points per shrine activation."); SnakeCritDamagePerStack = config.Bind("ClassicItemsReturns - Snake Eyes", "Critical damage per item stack and activation", 0.025f, "0.025 means 2.5%."); SnakeStageActivations = config.Bind("ClassicItemsReturns - Snake Eyes", "Maximum activations per stage", 4, "Clamped between one and six. Requires a restart."); ThalliumChance = config.Bind("ClassicItemsReturns - Thallium", "Proc chance percent", 5f, "Scales with proc coefficient."); ThalliumDamage = config.Bind("ClassicItemsReturns - Thallium", "Victim damage coefficient", 0.6f, "0.6 means 60% of the greater attacker/victim damage value per tick calculation."); ThalliumSlow = config.Bind("ClassicItemsReturns - Thallium", "Movement reduction", 0.4f, "0.4 means 40%."); ThalliumDuration = config.Bind("ClassicItemsReturns - Thallium", "Base duration", 2f, "Seconds."); ThalliumDurationPerStack = config.Bind("ClassicItemsReturns - Thallium", "Duration per extra stack", 0.75f, "Stored upstream but unused by CIR 3.6.26; retained for forward compatibility."); ToughTimesArmor = config.Bind("ClassicItemsReturns - Tough Times", "Armor per stack", 8f, "Permanent armor per item stack."); CloudburstScale = config.Bind("Cloudburst", "Finished item numerical scale", 0.5f, "Scales Glass Harvester crit damage, Bismuth barrier, Fabinhoru armor reduction, Jape healing/armor, and Keycard chance/damage. Requires a restart."); JapesDurationScale = config.Bind("Cloudburst", "Japes Cloak duration scale", 0.6f, "0.6 changes ten seconds to six seconds. Requires a restart."); EquipmentSlotGuard = config.Bind("Stability", "Guard uninitialized equipment slots", true, "Prevents null equipment-set arrays from throwing every fixed update."); DroneVendorGuard = config.Bind("Stability", "Guard malformed drone vendor terminals", true, "Skips missing terminals or interactors while preserving valid drone dispatches."); DelusionChestGuard = config.Bind("Stability", "Guard malformed Delusion chests", true, "Disables only a malformed DelusionChestController when its Start method throws a null reference."); PerformanceEnabled = config.Bind("Performance", "Enable performance module", true, "Master switch for local visual performance changes. Does not alter combat or networking."); EffectRendererOptimization = config.Bind("Performance", "Optimize spawned effect renderers", true, "Caps particles and disables shadows and motion vectors on spawned particle, trail, and line effects."); MaximumParticles = config.Bind("Performance", "Maximum particles per effect system", 400, new ConfigDescription("Upper limit for each newly spawned effect particle system.", (AcceptableValueBase)(object)new AcceptableValueRange(64, 2000), Array.Empty())); MaximumCorpses = config.Bind("Performance", "Maximum retained corpses", 10, new ConfigDescription("RoR2 normally retains up to 25 corpses. Lower values reduce late-stage renderer and physics load.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 25), Array.Empty())); MaximumDamageNumbers = config.Bind("Performance", "Maximum simultaneous damage numbers", 40, new ConfigDescription("Caps the existing client-side damage-number pool. Set to 0 to hide damage numbers.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 200), Array.Empty())); DisableEffectLightShadows = config.Bind("Performance", "Disable shadows on spawned effect lights", true, "Keeps effect lighting but removes its dynamic shadow cost."); } } internal static class ReflectionUtil { private const BindingFlags AnyStatic = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; private const BindingFlags AnyInstance = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly Dictionary TypeCache = new Dictionary(StringComparer.Ordinal); internal static Type FindType(string name, string pluginGuid = null) { string key = (pluginGuid ?? string.Empty) + "|" + name; lock (TypeCache) { if (TypeCache.TryGetValue(key, out var value)) { return value; } } Type type = null; if (!string.IsNullOrEmpty(pluginGuid) && Chainloader.PluginInfos.TryGetValue(pluginGuid, out var value2) && (Object)(object)value2.Instance != (Object)null) { type = ((object)value2.Instance).GetType().Assembly.GetType(name, throwOnError: false, ignoreCase: false); } if (type == null) { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { try { type = assembly.GetType(name, throwOnError: false, ignoreCase: false); } catch { } if (type != null) { break; } } } if (type != null) { lock (TypeCache) { TypeCache[key] = type; } } return type; } internal static Type RequireType(string name, string pluginGuid = null) { return FindType(name, pluginGuid) ?? throw new TypeLoadException(name); } internal static object GetSingleton(string typeName, string pluginGuid = null) { Type type = RequireType(typeName, pluginGuid); while (type != null) { PropertyInfo property = type.GetProperty("Instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (property != null) { object value = property.GetValue(null, null); if (value != null) { return value; } } FieldInfo fieldInfo = type.GetField("Instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) ?? type.GetField("instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (fieldInfo != null) { object value2 = fieldInfo.GetValue(null); if (value2 != null) { return value2; } } type = type.BaseType; } throw new MissingMemberException(typeName, "Instance"); } internal static void SetInstanceField(string typeName, string fieldName, object value, string pluginGuid = null) { object singleton = GetSingleton(typeName, pluginGuid); FieldInfo fieldInfo = singleton.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? throw new MissingFieldException(typeName, fieldName); fieldInfo.SetValue(singleton, ConvertValue(value, fieldInfo.FieldType)); } internal static void SetStaticField(string typeName, string fieldName, object value, string pluginGuid = null) { FieldInfo fieldInfo = RequireType(typeName, pluginGuid).GetField(fieldName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) ?? throw new MissingFieldException(typeName, fieldName); fieldInfo.SetValue(null, ConvertValue(value, fieldInfo.FieldType)); } internal static object GetStaticField(string typeName, string fieldName, string pluginGuid = null) { return (RequireType(typeName, pluginGuid).GetField(fieldName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) ?? throw new MissingFieldException(typeName, fieldName)).GetValue(null); } internal static int GetBuffCount(object body, object buffDef) { if (body == null || buffDef == null) { return 0; } MethodInfo[] methods = body.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo in methods) { ParameterInfo[] parameters = methodInfo.GetParameters(); if (methodInfo.Name == "GetBuffCount" && parameters.Length == 1 && parameters[0].ParameterType.IsInstanceOfType(buffDef)) { return (int)methodInfo.Invoke(body, new object[1] { buffDef }); } } return 0; } internal static void AddFloatMember(object target, string name, float delta) { if (target == null) { return; } FieldInfo field = target.GetType().GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { field.SetValue(target, Convert.ToSingle(field.GetValue(target), CultureInfo.InvariantCulture) + delta); return; } PropertyInfo property = target.GetType().GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (property != null && property.CanRead && property.CanWrite) { property.SetValue(target, Convert.ToSingle(property.GetValue(target, null), CultureInfo.InvariantCulture) + delta, null); } } internal static void AddLanguage(string token, string text) { Type type = FindType("R2API.LanguageAPI", "com.bepis.r2api.language"); if (type == null) { throw new TypeLoadException("R2API.LanguageAPI"); } (AccessTools.Method(type, "Add", new Type[2] { typeof(string), typeof(string) }, (Type[])null) ?? throw new MissingMethodException("R2API.LanguageAPI", "Add(string, string)")).Invoke(null, new object[2] { token, text }); } private static object ConvertValue(object value, Type destination) { if (value == null || destination.IsInstanceOfType(value)) { return value; } return Convert.ChangeType(value, destination, CultureInfo.InvariantCulture); } } internal static class RuntimeStabilityPatches { [HarmonyPatch] private static class EquipmentSlotStartGuard { private static readonly Type SlotType = ReflectionUtil.RequireType("RoR2.EquipmentSlot"); private static readonly FieldInfo ActiveSet = AccessTools.Field(SlotType, "k__BackingField"); private static MethodBase TargetMethod() { return AccessTools.Method(SlotType, "Start", (Type[])null, (Type[])null); } private static Exception Finalizer(object __instance, Exception __exception) { if (__exception is NullReferenceException && ActiveSet.GetValue(__instance) == null) { ActiveSet.SetValue(__instance, new byte[8]); LogOnce(ref equipmentLogged, "Recovered an EquipmentSlot that started before its run/components were fully available."); return null; } return __exception; } } [HarmonyPatch] private static class EquipmentSlotInventoryGuard { private static readonly Type SlotType = ReflectionUtil.RequireType("RoR2.EquipmentSlot"); private static readonly FieldInfo ActiveSet = AccessTools.Field(SlotType, "k__BackingField"); private static readonly PropertyInfo CharacterBody = AccessTools.Property(SlotType, "characterBody"); private static readonly Type BodyType = ReflectionUtil.RequireType("RoR2.CharacterBody"); private static readonly PropertyInfo Inventory = AccessTools.Property(BodyType, "inventory"); private static readonly Type InventoryType = ReflectionUtil.RequireType("RoR2.Inventory"); private static readonly PropertyInfo InventoryActiveSet = AccessTools.Property(InventoryType, "activeEquipmentSet"); private static MethodBase TargetMethod() { return AccessTools.Method(SlotType, "UpdateInventory", (Type[])null, (Type[])null); } private static bool Prefix(object __instance) { object value = CharacterBody.GetValue(__instance, null); if (Missing(value)) { LogOnce(ref equipmentLogged, "Skipped an EquipmentSlot inventory update with no CharacterBody."); return false; } object value2 = Inventory.GetValue(value, null); if (Missing(value2)) { LogOnce(ref equipmentLogged, "Skipped an EquipmentSlot inventory update before its Inventory was available."); return false; } byte[] array = InventoryActiveSet.GetValue(value2, null) as byte[]; byte[] array2 = ActiveSet.GetValue(__instance) as byte[]; int num = Math.Max(8, (array != null) ? array.Length : 0); if (array2 == null || array2.Length < num) { ActiveSet.SetValue(__instance, new byte[num]); LogOnce(ref equipmentLogged, "Initialized a missing EquipmentSlot active-equipment array before UpdateInventory."); } return true; } } [HarmonyPatch] private static class DroneVendorDispatchGuard { private static readonly Type ControllerType = ReflectionUtil.RequireType("RoR2.DroneVendorMultiShopController"); private static readonly FieldInfo Terminals = AccessTools.Field(ControllerType, "_terminals"); private static readonly FieldInfo Purchase = AccessTools.Field(ControllerType, "_purchaseInteraction"); private static readonly FieldInfo Interactor = AccessTools.Field(ControllerType, "_interactor"); private static readonly Type TerminalType = ReflectionUtil.RequireType("RoR2.DroneVendorTerminalBehavior"); private static readonly FieldInfo TerminalPurchase = AccessTools.Field(TerminalType, "purchaseInteraction"); private static readonly MethodInfo Dispatch = AccessTools.Method(TerminalType, "DispatchDrone", (Type[])null, (Type[])null); private static MethodBase TargetMethod() { return AccessTools.Method(ControllerType, "DispatchDrone", (Type[])null, (Type[])null); } private static bool Prefix(object __instance) { Array array = Terminals.GetValue(__instance) as Array; object value = Purchase.GetValue(__instance); object value2 = Interactor.GetValue(__instance); if (array == null || Missing(value) || Missing(value2)) { LogOnce(ref vendorLogged, "Skipped malformed Drone Vendor dispatch data instead of throwing a null reference."); return false; } foreach (object item in array) { if (!Missing(item) && TerminalPurchase.GetValue(item) == value) { Dispatch.Invoke(item, new object[1] { value2 }); } } return false; } } [HarmonyPatch] private static class DelusionChestStartGuard { private static readonly Type ControllerType = ReflectionUtil.RequireType("RoR2.DelusionChestController"); private static MethodBase TargetMethod() { return AccessTools.Method(ControllerType, "Start", (Type[])null, (Type[])null); } private static Exception Finalizer(object __instance, Exception __exception) { if (__exception is NullReferenceException) { Behaviour val = (Behaviour)((__instance is Behaviour) ? __instance : null); if ((Object)(object)val != (Object)null) { val.enabled = false; } LogOnce(ref delusionLogged, "Disabled one malformed DelusionChestController after its required component was missing."); return null; } return __exception; } } private static readonly object LogLock = new object(); private static bool equipmentLogged; private static bool vendorLogged; private static bool delusionLogged; internal static void Install(Harmony harmony) { BalanceConfig balance = Plugin.Balance; if (balance.EquipmentSlotGuard.Value) { harmony.CreateClassProcessor(typeof(EquipmentSlotStartGuard)).Patch(); harmony.CreateClassProcessor(typeof(EquipmentSlotInventoryGuard)).Patch(); } if (balance.DroneVendorGuard.Value) { harmony.CreateClassProcessor(typeof(DroneVendorDispatchGuard)).Patch(); } if (balance.DelusionChestGuard.Value) { harmony.CreateClassProcessor(typeof(DelusionChestStartGuard)).Patch(); } } private static bool Missing(object value) { if (value == null) { return true; } Object val = (Object)((value is Object) ? value : null); if (val != null) { return !Object.op_Implicit(val); } return false; } private static void LogOnce(ref bool flag, string message) { lock (LogLock) { if (!flag) { flag = true; Plugin.Instance.Log.LogWarning((object)message); } } } } }