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 M4A1Fix v1.0.1
plugins/M4A1Fix/M4A1Fix.dll
Decompiled a day agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Logging; using HarmonyLib; using M4A1Fix.Compatibility; using M4A1Fix.Patches; using M4A1Mod.Survivors.M4A1.Utils; using Microsoft.CodeAnalysis; using RoR2; using UnityEngine; using UnityEngine.Networking; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("M4A1Fix")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("M4A1Fix")] [assembly: AssemblyTitle("M4A1Fix")] [assembly: AssemblyVersion("1.0.0.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 M4A1Fix { [BepInPlugin("com.m4a1fix.revivepriority", "M4A1Fix", "1.0.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] public sealed class Plugin : BaseUnityPlugin { internal const string PluginGuid = "com.m4a1fix.revivepriority"; internal const string PluginName = "M4A1Fix"; internal const string PluginVersion = "1.0.0"; internal const string M4A1PluginGuid = "com.Snoresville.M4A1"; private Harmony _revivePriorityHarmony; private Harmony _shrineAiRecoveryHarmony; private void Awake() { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Expected O, but got Unknown ((BaseUnityPlugin)this).Logger.LogInfo((object)"M4A1Fix 1.0.0 loaded."); _revivePriorityHarmony = new Harmony("com.m4a1fix.revivepriority"); if (!RevivePriorityPatch.TryInstall(_revivePriorityHarmony, ((BaseUnityPlugin)this).Logger, out var target, out var reason)) { ((BaseUnityPlugin)this).Logger.LogError((object)("RevivePriority disabled: " + reason)); return; } ((BaseUnityPlugin)this).Logger.LogInfo((object)"RevivePriority enabled."); if (!ShrineAiRecoveryPatch.TryInstall(((BaseUnityPlugin)this).Logger, target, out _shrineAiRecoveryHarmony, out reason)) { ((BaseUnityPlugin)this).Logger.LogError((object)("ShrineAIRecovery disabled: " + reason)); } else { ((BaseUnityPlugin)this).Logger.LogInfo((object)"ShrineAIRecovery enabled."); } } private void OnDestroy() { if (_shrineAiRecoveryHarmony != null) { _shrineAiRecoveryHarmony.UnpatchSelf(); } if (_revivePriorityHarmony != null) { _revivePriorityHarmony.UnpatchSelf(); } } } } namespace M4A1Fix.Patches { internal static class RevivePriorityPatch { private static bool _transpilerMatched; internal static bool TryInstall(Harmony harmony, ManualLogSource log, out MethodInfo target, out string reason) { //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Expected O, but got Unknown target = null; reason = null; if (harmony == null) { reason = "Harmony owner was not created."; return false; } if (!M4A1Compatibility.TryValidateRevivePriorityTarget(out target, out reason)) { return false; } _transpilerMatched = false; MethodInfo methodInfo = AccessTools.Method(typeof(RevivePriorityPatch), "CharacterMasterOnBodyDeathTranspiler", (Type[])null, (Type[])null); if (methodInfo == null) { reason = "revive-priority transpiler method could not be resolved."; return false; } harmony.Patch((MethodBase)target, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(methodInfo), (HarmonyMethod)null, (HarmonyMethod)null); if (!_transpilerMatched || !HasOwnedTranspiler(target, "com.m4a1fix.revivepriority")) { harmony.UnpatchSelf(); reason = "exact M4A1 OnBodyDeath IL pattern did not match; no targeted patch remains installed."; return false; } return true; } internal static bool IsHealthy(MethodInfo exactTarget) { if (_transpilerMatched && exactTarget != null) { return HasOwnedTranspiler(exactTarget, "com.m4a1fix.revivepriority"); } return false; } private static bool ShouldDelegateShrineToVanilla(CharacterBody body) { if (NetworkServer.active && (Object)(object)body != (Object)null) { return body.HasBuff(Buffs.ExtraLifeBuff); } return false; } private static IEnumerable<CodeInstruction> CharacterMasterOnBodyDeathTranspiler(IEnumerable<CodeInstruction> instructions, ILGenerator generator) { //IL_0124: Unknown result type (might be due to invalid IL or missing references) //IL_012e: Expected O, but got Unknown //IL_0135: Unknown result type (might be due to invalid IL or missing references) //IL_013f: Expected O, but got Unknown //IL_0147: Unknown result type (might be due to invalid IL or missing references) //IL_0151: Expected O, but got Unknown //IL_015e: Unknown result type (might be due to invalid IL or missing references) //IL_0168: Expected O, but got Unknown List<CodeInstruction> list = new List<CodeInstruction>(instructions); int num = FindOrigOnBodyDeathInvoke(list); int num2 = FindGetSomeChildDummyLink(list); if (num < 3 || num2 < 1) { return list; } int num3 = num - 3; int num4 = num2 - 1; if (!IsLdarg(list[num3], 0) || !IsLdarg(list[num3 + 1], 1) || !IsLdarg(list[num3 + 2], 2) || !IsLdarg(list[num4], 2)) { return list; } int num5 = FindConditionalBranchTo(list, num4, num3); if (num5 <= 0 || !IsLocalLoad(list[num5 - 1])) { return list; } int index = num5 - 1; Label label = generator.DefineLabel(); Label continueOriginalDecisionLabel = generator.DefineLabel(); list[num3].labels.Add(label); list[index].labels.Add(continueOriginalDecisionLabel); CodeInstruction item = CloneWithoutLabelsOrBlocks(list[index]); MethodInfo methodInfo = AccessTools.Method(typeof(RevivePriorityPatch), "ShouldDelegateShrineToVanilla", (Type[])null, (Type[])null); List<CodeInstruction> list2 = new List<CodeInstruction> { item, new CodeInstruction(OpCodes.Brtrue, (object)continueOriginalDecisionLabel), new CodeInstruction(OpCodes.Ldarg_2, (object)null), new CodeInstruction(OpCodes.Call, (object)methodInfo), new CodeInstruction(OpCodes.Brtrue, (object)label) }; list2[0].labels.AddRange(list[index].labels.Where((Label label2) => !label2.Equals(continueOriginalDecisionLabel))); list[index].labels.RemoveAll((Label label2) => !label2.Equals(continueOriginalDecisionLabel)); list2[0].blocks.AddRange(list[index].blocks); list[index].blocks.Clear(); list.InsertRange(index, list2); _transpilerMatched = true; return list; } private static int FindOrigOnBodyDeathInvoke(List<CodeInstruction> codes) { for (int i = 0; i < codes.Count; i++) { MethodInfo methodInfo = codes[i].operand as MethodInfo; if (!(methodInfo == null) && !(methodInfo.Name != "Invoke") && !(methodInfo.DeclaringType == null) && !(methodInfo.DeclaringType.Name != "orig_OnBodyDeath")) { ParameterInfo[] parameters = methodInfo.GetParameters(); if (methodInfo.ReturnType == typeof(void) && parameters.Length == 2 && parameters[0].ParameterType == typeof(CharacterMaster) && parameters[1].ParameterType == typeof(CharacterBody)) { return i; } } } return -1; } private static int FindGetSomeChildDummyLink(List<CodeInstruction> codes) { for (int i = 0; i < codes.Count; i++) { MethodInfo methodInfo = codes[i].operand as MethodInfo; if (methodInfo != null && methodInfo.Name == "GetSomeChildDummyLink" && methodInfo.DeclaringType != null && methodInfo.DeclaringType.FullName == "M4A1Mod.Survivors.M4A1.Utils.M4A1") { return i; } } return -1; } private static int FindConditionalBranchTo(List<CodeInstruction> codes, int targetIndex, int searchEndExclusive) { List<Label> labels = codes[targetIndex].labels; for (int num = searchEndExclusive - 1; num >= 0; num--) { if (codes[num].opcode.FlowControl == FlowControl.Cond_Branch && codes[num].operand is Label item && labels.Contains(item)) { return num; } } return -1; } private static bool IsLdarg(CodeInstruction instruction, int index) { if (index == 0 && instruction.opcode == OpCodes.Ldarg_0) { return true; } if (index == 1 && instruction.opcode == OpCodes.Ldarg_1) { return true; } if (index == 2 && instruction.opcode == OpCodes.Ldarg_2) { return true; } if (index == 3 && instruction.opcode == OpCodes.Ldarg_3) { return true; } if (instruction.opcode != OpCodes.Ldarg && instruction.opcode != OpCodes.Ldarg_S) { return false; } if (instruction.operand is byte b) { return b == index; } if (instruction.operand is short num) { return num == index; } if (instruction.operand is ParameterInfo parameterInfo) { return parameterInfo.Position == index; } return false; } private static bool IsLocalLoad(CodeInstruction instruction) { if (!(instruction.opcode == OpCodes.Ldloc) && !(instruction.opcode == OpCodes.Ldloc_S) && !(instruction.opcode == OpCodes.Ldloc_0) && !(instruction.opcode == OpCodes.Ldloc_1) && !(instruction.opcode == OpCodes.Ldloc_2)) { return instruction.opcode == OpCodes.Ldloc_3; } return true; } private static CodeInstruction CloneWithoutLabelsOrBlocks(CodeInstruction instruction) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Expected O, but got Unknown return new CodeInstruction(instruction.opcode, instruction.operand); } private static bool HasOwnedTranspiler(MethodBase target, string owner) { Patches patchInfo = Harmony.GetPatchInfo(target); if (patchInfo == null || patchInfo.Transpilers == null) { return false; } foreach (Patch transpiler in patchInfo.Transpilers) { if (transpiler != null && transpiler.owner == owner) { return true; } } return false; } } internal static class ShrineAiRecoveryPatch { private const string M4A1BodyName = "M4A1Body"; private const string DummyLinkDeathMethodName = "CharacterMaster_OnBodyDeath"; private const string ShrineRespawnMethodName = "RespawnExtraLifeShrine"; private const string HarmonyOwner = "com.m4a1fix.revivepriority/shrine-ai-recovery"; private const BindingFlags DeclaredMethodFlags = BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; private static ManualLogSource _log; internal static bool TryInstall(ManualLogSource log, MethodInfo revivePriorityTarget, out Harmony harmony, out string reason) { //IL_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00cc: Expected O, but got Unknown //IL_00d2: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Expected O, but got Unknown _log = log; harmony = null; reason = null; if (!RevivePriorityPatch.IsHealthy(revivePriorityTarget)) { reason = "RevivePriority is not healthy on the exact M4A1 death target."; return false; } if (!TryValidateExactM4A1Plugin(out var assembly, out reason)) { return false; } Type type = assembly.GetType("M4A1Mod.Survivors.M4A1.Hooks.DummyLink", throwOnError: false); if (type == null) { reason = "exact M4A1 DummyLink type is missing."; return false; } if (!TryResolveSingleDeclaredMethod(type, "CharacterMaster_OnBodyDeath", out var method, out reason)) { return false; } if (!method.Equals(revivePriorityTarget) || !HasOwnedTranspiler(method, "com.m4a1fix.revivepriority")) { reason = "RevivePriority is not installed on the exact M4A1 CharacterMaster_OnBodyDeath target."; return false; } if (!TryResolveSingleDeclaredMethod(typeof(CharacterMaster), "RespawnExtraLifeShrine", out var method2, out reason)) { return false; } MethodInfo method3 = typeof(ShrineAiRecoveryPatch).GetMethod("RespawnExtraLifeShrinePostfix", BindingFlags.Static | BindingFlags.NonPublic); if (method3 == null) { reason = "ShrineAIRecovery postfix method could not be resolved."; return false; } try { harmony = new Harmony("com.m4a1fix.revivepriority/shrine-ai-recovery"); harmony.Patch((MethodBase)method2, (HarmonyMethod)null, new HarmonyMethod(method3), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); if (!HasOwnedPostfix(method2, "com.m4a1fix.revivepriority/shrine-ai-recovery")) { harmony.UnpatchSelf(); harmony = null; reason = "Harmony did not report the ShrineAIRecovery postfix after installation."; return false; } return true; } catch (Exception ex) { try { if (harmony != null) { harmony.UnpatchSelf(); } } catch { } harmony = null; reason = "installation exception: " + ex.GetType().Name + ": " + ex.Message; return false; } } private static bool TryValidateExactM4A1Plugin(out Assembly assembly, out string reason) { assembly = null; reason = null; if (!Chainloader.PluginInfos.TryGetValue("com.Snoresville.M4A1", out var value) || value == null) { reason = "required M4A1 plugin is not loaded."; return false; } string location; try { location = value.Location; } catch (Exception ex) { reason = "M4A1 PluginInfo.Location read failed: " + ex.GetType().Name + ": " + ex.Message; return false; } if (string.IsNullOrWhiteSpace(location) || !File.Exists(location)) { reason = "loaded M4A1 plugin file location is missing or unreadable."; return false; } if (!TrySha256(location, out var hashHex, out reason)) { return false; } if (!string.Equals(hashHex, "600188D306782F6C1B26EA1BDA4A615BFF9AC4437318873D5728CD58B26A7154", StringComparison.OrdinalIgnoreCase)) { reason = "loaded M4A1 plugin SHA-256 mismatch."; return false; } if ((Object)(object)value.Instance == (Object)null) { reason = "loaded M4A1 PluginInfo has no live plugin instance."; return false; } assembly = ((object)value.Instance).GetType().Assembly; return true; } private static bool TrySha256(string filePath, out string hashHex, out string reason) { hashHex = null; reason = null; try { using (FileStream inputStream = File.OpenRead(filePath)) { using SHA256 sHA = SHA256.Create(); hashHex = BitConverter.ToString(sHA.ComputeHash(inputStream)).Replace("-", string.Empty); } return true; } catch (Exception ex) { reason = "SHA-256 read failed for loaded M4A1 plugin bytes: " + ex.GetType().Name + ": " + ex.Message; return false; } } private static bool TryResolveSingleDeclaredMethod(Type type, string methodName, out MethodInfo method, out string reason) { method = null; reason = null; 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 != methodName)) { method = methodInfo; num++; } } if (num == 1) { return true; } method = null; reason = ((num == 0) ? ("exact method is missing: " + type.FullName + "." + methodName) : ("exact method resolution is ambiguous: " + type.FullName + "." + methodName + " (count=" + num + ")")); return false; } private static bool HasOwnedTranspiler(MethodBase target, string owner) { Patches patchInfo = Harmony.GetPatchInfo(target); if (patchInfo == null || patchInfo.Transpilers == null) { return false; } foreach (Patch transpiler in patchInfo.Transpilers) { if (transpiler != null && transpiler.owner == owner) { return true; } } return false; } private static bool HasOwnedPostfix(MethodBase target, string owner) { Patches patchInfo = Harmony.GetPatchInfo(target); if (patchInfo == null || patchInfo.Postfixes == null) { return false; } foreach (Patch postfix in patchInfo.Postfixes) { if (postfix != null && postfix.owner == owner) { return true; } } return false; } private static void RespawnExtraLifeShrinePostfix(CharacterMaster __instance) { //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0047: 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) //IL_004a: Invalid comparison between Unknown and I4 //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) if (!NetworkServer.active || (Object)(object)__instance == (Object)null) { return; } CharacterBody body = __instance.GetBody(); if ((Object)(object)body == (Object)null || (Object)(object)body.healthComponent == (Object)null || !body.healthComponent.alive) { return; } BodyIndex val = BodyCatalog.FindBodyIndex("M4A1Body"); if ((int)val == -1 || body.bodyIndex != val) { return; } try { M4A1.ReassignDummyLinks(body); if (_log != null) { _log.LogInfo((object)"ShrineAIRecovery: dummy leaders reassigned after Shrine of Shaping revive."); } } catch (Exception ex) { if (_log != null) { _log.LogError((object)("ShrineAIRecovery repair failed: " + ex.GetType().Name + ": " + ex.Message)); } } } } } namespace M4A1Fix.Compatibility { internal static class M4A1Compatibility { private sealed class DecodedInstruction { internal int Offset; internal OpCode OpCode; internal object Operand; } internal const string ExpectedM4A1AssemblySha256 = "600188D306782F6C1B26EA1BDA4A615BFF9AC4437318873D5728CD58B26A7154"; private const string ExpectedM4A1OnBodyDeathIlSha256 = "79150F880D5A6361BEF0E94A78AABB57B7A56881565C843E8A3D2614D65FE5E4"; private const int ExpectedTryReviveCodeSize = 632; private const int ExpectedRespawnExtraLifeShrineCodeSize = 264; private const int ExpectedPendingStateCodeSize = 75; private const string ExpectedTryReviveCanonicalSha256 = "43D299DDC88201E148A1D914092175CB58285EDB1683822D825FDCC8E4163395"; private const string ExpectedRespawnExtraLifeShrineCanonicalSha256 = "28D947645DE9C1E53D7C25591434D8B75ECB2E2E42492DF482D9A57296428E1F"; private const string ExpectedPendingStateCanonicalSha256 = "7DDA7E015613A20CBD68394435B95ECE885557F7875832D063F853B764157F74"; internal const string M4A1DummyLinkHooksTypeName = "M4A1Mod.Survivors.M4A1.Hooks.DummyLink"; internal const string M4A1UtilsTypeName = "M4A1Mod.Survivors.M4A1.Utils.M4A1"; private static readonly Dictionary<ushort, OpCode> OpCodeMap = BuildOpCodeMap(); internal static bool TryValidateRevivePriorityTarget(out MethodInfo target, out string reason) { target = null; reason = null; Assembly assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault((Assembly a) => string.Equals(a.GetName().Name, "M4A1Mod", StringComparison.Ordinal)); if (assembly == null) { reason = "M4A1Mod assembly is not loaded."; return false; } if (!ValidateAssemblyHash(assembly, "600188D306782F6C1B26EA1BDA4A615BFF9AC4437318873D5728CD58B26A7154", "M4A1Mod.dll", out reason)) { return false; } if (typeof(CharacterMaster).Assembly != typeof(CharacterBody).Assembly) { reason = "RoR2 CharacterMaster and CharacterBody are not from the same assembly."; return false; } Type type = assembly.GetType("M4A1Mod.Survivors.M4A1.Hooks.DummyLink", throwOnError: false); if (type == null) { reason = "M4A1 DummyLink hook type is missing."; return false; } target = type.GetMethod("CharacterMaster_OnBodyDeath", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (!ValidateM4A1TargetShape(target, out reason)) { target = null; return false; } if (!ValidateMethodIlHash(target, "79150F880D5A6361BEF0E94A78AABB57B7A56881565C843E8A3D2614D65FE5E4", "M4A1 CharacterMaster_OnBodyDeath", out reason)) { target = null; return false; } MethodInfo method = typeof(CharacterMaster).GetMethod("TryReviveOnBodyDeath", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(CharacterBody) }, null); if (method == null || method.ReturnType != typeof(bool) || !ValidateTryReviveStructure(method, out reason)) { target = null; if (reason == null) { reason = "RoR2 TryReviveOnBodyDeath signature mismatch."; } return false; } MethodInfo method2 = typeof(CharacterMaster).GetMethod("RespawnExtraLifeShrine", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null); if (method2 == null || method2.ReturnType != typeof(void) || !ValidateShrineRespawnStructure(method2, out reason)) { target = null; if (reason == null) { reason = "RoR2 RespawnExtraLifeShrine signature mismatch."; } return false; } MethodInfo method3 = typeof(CharacterMaster).GetMethod("IsExtraLifePendingServer", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null); if (method3 == null || method3.ReturnType != typeof(bool) || !ValidatePendingStateStructure(method3, out reason)) { target = null; if (reason == null) { reason = "RoR2 IsExtraLifePendingServer signature mismatch."; } return false; } return true; } private static bool ValidateM4A1TargetShape(MethodInfo method, out string reason) { reason = null; if (method == null || !method.IsStatic || method.ReturnType != typeof(void)) { reason = "M4A1 CharacterMaster_OnBodyDeath method shape is missing or incompatible."; return false; } ParameterInfo[] parameters = method.GetParameters(); if (parameters.Length != 3 || parameters[0].ParameterType.Name != "orig_OnBodyDeath" || parameters[1].ParameterType != typeof(CharacterMaster) || parameters[2].ParameterType != typeof(CharacterBody)) { reason = "M4A1 CharacterMaster_OnBodyDeath parameter shape is incompatible."; return false; } return true; } private static bool ValidateAssemblyHash(Assembly assembly, string expected, string displayName, out string reason) { reason = null; string location = assembly.Location; if (string.IsNullOrEmpty(location) || !File.Exists(location)) { reason = displayName + " location is unavailable for compatibility hashing."; return false; } string text; try { using FileStream inputStream = File.OpenRead(location); using SHA256 sHA = SHA256.Create(); text = ToHex(sHA.ComputeHash(inputStream)); } catch (IOException ex) { reason = displayName + " hash read failed: " + ex.Message; return false; } catch (UnauthorizedAccessException ex2) { reason = displayName + " hash read denied: " + ex2.Message; return false; } if (!string.Equals(text, expected, StringComparison.OrdinalIgnoreCase)) { reason = displayName + " SHA-256 mismatch. Expected " + expected + ", got " + text + "."; return false; } return true; } private static bool ValidateTryReviveStructure(MethodInfo method, out string reason) { if (!ValidateCanonicalFingerprint(method, 632, "43D299DDC88201E148A1D914092175CB58285EDB1683822D825FDCC8E4163395", "RoR2 TryReviveOnBodyDeath", out reason)) { return false; } if (!TryDecode(method, 632, "RoR2 TryReviveOnBodyDeath", out var instructions, out reason)) { return false; } int num = FindMember(instructions, "DLC2Content+Buffs::ExtraLifeBuff", 0); int num2 = FindMember(instructions, "CharacterBody::HasBuff", num + 1); int num3 = FindMember(instructions, "CharacterBody::RemoveBuff", num2 + 1); int num4 = FindString(instructions, "RespawnExtraLifeShrine", num3 + 1); int num5 = FindMember(instructions, "UnityEngine.MonoBehaviour::Invoke(System.String,System.Single)", num4 + 1); int num6 = FindString(instructions, "PlayExtraLifeSFX", num5 + 1); int num7 = FindMember(instructions, "UnityEngine.MonoBehaviour::Invoke(System.String,System.Single)", num6 + 1); int num8 = FindMember(instructions, "ItemTransformation::TryTake", 0); if (num < 0 || num2 <= num || num3 <= num2 || num4 <= num3 || num5 <= num4 || num6 <= num5 || num7 <= num6 || (num8 >= 0 && num8 <= num7)) { reason = "RoR2 TryReviveOnBodyDeath normalized structural fingerprint mismatch in Shrine-before-Dio decision landmarks."; return false; } return true; } private static bool ValidateShrineRespawnStructure(MethodInfo method, out string reason) { if (!ValidateCanonicalFingerprint(method, 264, "28D947645DE9C1E53D7C25591434D8B75ECB2E2E42492DF482D9A57296428E1F", "RoR2 RespawnExtraLifeShrine", out reason)) { return false; } if (!TryDecode(method, 264, "RoR2 RespawnExtraLifeShrine", out var instructions, out reason)) { return false; } int num = FindMember(instructions, "CharacterMaster::Respawn", 0); int num2 = FindMember(instructions, "RoR2Content+Buffs::Immune", 0); int num3 = FindMember(instructions, "CharacterBody::AddTimedBuff", (num2 >= 0) ? num2 : 0); if (num < 0 || num2 < 0 || num3 < num2) { reason = "RoR2 RespawnExtraLifeShrine normalized structural fingerprint mismatch in respawn/immunity landmarks."; return false; } return true; } private static bool ValidatePendingStateStructure(MethodInfo method, out string reason) { if (!ValidateCanonicalFingerprint(method, 75, "7DDA7E015613A20CBD68394435B95ECE885557F7875832D063F853B764157F74", "RoR2 IsExtraLifePendingServer", out reason)) { return false; } if (!TryDecode(method, 75, "RoR2 IsExtraLifePendingServer", out var instructions, out reason)) { return false; } int num = FindMember(instructions, "ExtraLifeServerBehavior", 0); int num2 = FindString(instructions, "RespawnSeekerRevive", 0); int num3 = FindString(instructions, "RespawnExtraLifeShrine", num2 + 1); int num4 = FindString(instructions, "RespawnExtraLifeHealAndRevive", num3 + 1); if (num < 0 || num2 < 0 || num3 <= num2 || num4 <= num3) { reason = "RoR2 IsExtraLifePendingServer normalized structural fingerprint mismatch in pending-revive landmarks."; return false; } return true; } private static bool ValidateCanonicalFingerprint(MethodInfo method, int expectedCodeSize, string expectedHash, string displayName, out string reason) { reason = null; if (!TryBuildCanonicalMethod(method, expectedCodeSize, displayName, out var canonical, out reason)) { return false; } string text; using (SHA256 sHA = SHA256.Create()) { text = ToHex(sHA.ComputeHash(Encoding.UTF8.GetBytes(canonical))); } if (!string.Equals(text, expectedHash, StringComparison.OrdinalIgnoreCase)) { reason = displayName + " canonical IL SHA-256 mismatch. Expected " + expectedHash + ", got " + text + "."; return false; } return true; } private static bool TryBuildCanonicalMethod(MethodInfo method, int expectedCodeSize, string displayName, out string canonical, out string reason) { canonical = null; reason = null; if (!TryDecode(method, expectedCodeSize, displayName, out var instructions, out reason)) { return false; } try { MethodBody methodBody = method.GetMethodBody(); if (methodBody == null) { reason = displayName + " has no method body."; return false; } Dictionary<int, int> dictionary = new Dictionary<int, int>(); for (int i = 0; i < instructions.Count; i++) { dictionary.Add(instructions[i].Offset, i); } dictionary.Add(expectedCodeSize, instructions.Count); List<string> list = new List<string>(); list.Add("CANONICAL_IL_V1"); list.Add("METHOD=" + CanonicalMethodIdentity(method)); list.Add("INIT_LOCALS=" + (methodBody.InitLocals ? "1" : "0")); list.Add("MAX_STACK=" + methodBody.MaxStackSize.ToString(CultureInfo.InvariantCulture)); IList<LocalVariableInfo> localVariables = methodBody.LocalVariables; list.Add("LOCALS=" + localVariables.Count.ToString(CultureInfo.InvariantCulture)); for (int j = 0; j < localVariables.Count; j++) { LocalVariableInfo localVariableInfo = localVariables[j]; list.Add("LOCAL[" + j.ToString(CultureInfo.InvariantCulture) + "]=T=" + CanonicalTypeIdentity(localVariableInfo.LocalType) + ";PINNED=" + (localVariableInfo.IsPinned ? "1" : "0")); } list.Add("INSTRUCTIONS=" + instructions.Count.ToString(CultureInfo.InvariantCulture)); for (int k = 0; k < instructions.Count; k++) { DecodedInstruction decodedInstruction = instructions[k]; if (!TryCanonicalOperand(decodedInstruction, dictionary, out var canonical2, out reason)) { reason = displayName + " canonical operand failed at instruction " + k.ToString(CultureInfo.InvariantCulture) + ": " + reason; return false; } list.Add("I[" + k.ToString(CultureInfo.InvariantCulture) + "]=OP=" + decodedInstruction.OpCode.Name + ";O=" + canonical2); } IList<ExceptionHandlingClause> exceptionHandlingClauses = methodBody.ExceptionHandlingClauses; list.Add("EH=" + exceptionHandlingClauses.Count.ToString(CultureInfo.InvariantCulture)); for (int l = 0; l < exceptionHandlingClauses.Count; l++) { ExceptionHandlingClause exceptionHandlingClause = exceptionHandlingClauses[l]; if (!dictionary.TryGetValue(exceptionHandlingClause.TryOffset, out var value) || !dictionary.TryGetValue(exceptionHandlingClause.TryOffset + exceptionHandlingClause.TryLength, out var value2) || !dictionary.TryGetValue(exceptionHandlingClause.HandlerOffset, out var value3) || !dictionary.TryGetValue(exceptionHandlingClause.HandlerOffset + exceptionHandlingClause.HandlerLength, out var value4)) { reason = displayName + " has an EH boundary outside canonical instruction boundaries."; return false; } string text = "NONE"; string text2 = "NONE"; string text3; if (exceptionHandlingClause.Flags == ExceptionHandlingClauseOptions.Clause) { text3 = "CATCH"; if (exceptionHandlingClause.CatchType == null) { reason = displayName + " has a catch clause without a catch type."; return false; } text2 = CanonicalTypeIdentity(exceptionHandlingClause.CatchType); } else if (exceptionHandlingClause.Flags == ExceptionHandlingClauseOptions.Filter) { text3 = "FILTER"; if (!dictionary.TryGetValue(exceptionHandlingClause.FilterOffset, out var value5)) { reason = displayName + " has a filter boundary outside canonical instruction boundaries."; return false; } text = value5.ToString(CultureInfo.InvariantCulture); } else if (exceptionHandlingClause.Flags == ExceptionHandlingClauseOptions.Finally) { text3 = "FINALLY"; } else { if (exceptionHandlingClause.Flags != ExceptionHandlingClauseOptions.Fault) { reason = displayName + " has an unsupported EH clause kind: " + exceptionHandlingClause.Flags.ToString() + "."; return false; } text3 = "FAULT"; } list.Add("EH[" + l.ToString(CultureInfo.InvariantCulture) + "]=K=" + text3 + ";TRY=" + value.ToString(CultureInfo.InvariantCulture) + ".." + value2.ToString(CultureInfo.InvariantCulture) + ";HANDLER=" + value3.ToString(CultureInfo.InvariantCulture) + ".." + value4.ToString(CultureInfo.InvariantCulture) + ";FILTER=" + text + ";CATCH=" + text2); } canonical = string.Join("\n", list.ToArray()) + "\n"; return true; } catch (Exception ex) { reason = displayName + " canonicalization failed closed: " + ex.GetType().Name + ": " + ex.Message; return false; } } private static bool TryCanonicalOperand(DecodedInstruction instruction, Dictionary<int, int> offsetToIndex, out string canonical, out string reason) { canonical = null; reason = null; object operand = instruction.Operand; switch (instruction.OpCode.OperandType) { case OperandType.InlineNone: canonical = "NONE"; return true; case OperandType.ShortInlineI: if (!(operand is sbyte b)) { reason = "ShortInlineI operand type mismatch."; return false; } canonical = "I=" + b.ToString(CultureInfo.InvariantCulture); return true; case OperandType.InlineI: if (!(operand is int num3)) { reason = "InlineI operand type mismatch."; return false; } canonical = "I=" + num3.ToString(CultureInfo.InvariantCulture); return true; case OperandType.InlineI8: if (!(operand is long num4)) { reason = "InlineI8 operand type mismatch."; return false; } canonical = "I=" + num4.ToString(CultureInfo.InvariantCulture); return true; case OperandType.ShortInlineR: if (!(operand is float value3)) { reason = "ShortInlineR operand type mismatch."; return false; } canonical = "R4=" + BitConverter.ToUInt32(BitConverter.GetBytes(value3), 0).ToString("X8", CultureInfo.InvariantCulture); return true; case OperandType.InlineR: if (!(operand is double value2)) { reason = "InlineR operand type mismatch."; return false; } canonical = "R8=" + BitConverter.ToUInt64(BitConverter.GetBytes(value2), 0).ToString("X16", CultureInfo.InvariantCulture); return true; case OperandType.InlineVar: case OperandType.ShortInlineVar: { int num; if (operand is byte b2) { num = b2; } else { if (!(operand is ushort num2)) { reason = "variable operand type mismatch."; return false; } num = num2; } string text = instruction.OpCode.Name ?? string.Empty; string text2 = ((text.IndexOf("arg", StringComparison.Ordinal) >= 0) ? "ARG" : ((text.IndexOf("loc", StringComparison.Ordinal) >= 0) ? "LOC" : "VAR")); canonical = text2 + "=" + num.ToString(CultureInfo.InvariantCulture); return true; } case OperandType.InlineBrTarget: case OperandType.ShortInlineBrTarget: { if (!(operand is int key)) { reason = "branch operand type mismatch."; return false; } if (!offsetToIndex.TryGetValue(key, out var value5)) { reason = "branch target is not an instruction boundary."; return false; } canonical = "BR=" + value5.ToString(CultureInfo.InvariantCulture); return true; } case OperandType.InlineSwitch: { if (!(operand is int[] array)) { reason = "switch operand type mismatch."; return false; } string[] array2 = new string[array.Length]; for (int i = 0; i < array.Length; i++) { if (!offsetToIndex.TryGetValue(array[i], out var value4)) { reason = "switch target is not an instruction boundary."; return false; } array2[i] = value4.ToString(CultureInfo.InvariantCulture); } canonical = "SW=[" + string.Join(",", array2) + "]"; return true; } case OperandType.InlineString: if (!(operand is string value)) { reason = "string token did not resolve to a string."; return false; } canonical = "STR=" + Utf8Hex(value); return true; case OperandType.InlineField: case OperandType.InlineMethod: case OperandType.InlineTok: case OperandType.InlineType: canonical = "META=" + CanonicalMemberIdentity(operand); return true; case OperandType.InlineSig: reason = "InlineSig is forbidden by CANONICAL_IL_V1."; return false; default: reason = "unsupported operand type " + instruction.OpCode.OperandType.ToString() + "."; return false; } } private static string CanonicalMemberIdentity(object member) { if (member is Type type) { return CanonicalTypeIdentity(type); } if (member is FieldInfo field) { return CanonicalFieldIdentity(field); } if (member is MethodBase method) { return CanonicalMethodIdentity(method); } if (member is MemberInfo memberInfo) { throw new InvalidDataException("Unsupported resolved member kind: " + memberInfo.MemberType); } throw new InvalidDataException("Metadata operand did not resolve to a supported semantic member."); } private static string CanonicalFieldIdentity(FieldInfo field) { Type declaringType = field.DeclaringType; if (declaringType == null) { throw new InvalidDataException("Field has no declaring type."); } FieldInfo fieldInfo = field; if (declaringType.IsGenericType && !declaringType.IsGenericTypeDefinition) { fieldInfo = declaringType.GetGenericTypeDefinition().GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((FieldInfo candidate) => candidate.MetadataToken == field.MetadataToken); if (fieldInfo == null) { throw new InvalidDataException("Could not recover generic field definition by metadata token."); } } return "F(D=" + CanonicalTypeIdentity(declaringType) + ";N=" + Utf8Hex(field.Name) + ";T=" + CanonicalTypeIdentity(fieldInfo.FieldType) + ")"; } private static string CanonicalMethodIdentity(MethodBase method) { if (method is MethodInfo { IsGenericMethod: not false, IsGenericMethodDefinition: false } methodInfo) { return "MS(B=" + CanonicalMethodIdentity(methodInfo.GetGenericMethodDefinition()) + ";A=[" + string.Join(",", methodInfo.GetGenericArguments().Select(CanonicalTypeIdentity).ToArray()) + "])"; } Type declaringType = method.DeclaringType; if (declaringType == null) { throw new InvalidDataException("Method has no declaring type."); } MethodBase methodBase = method; if (declaringType.IsGenericType && !declaringType.IsGenericTypeDefinition) { Type genericTypeDefinition = declaringType.GetGenericTypeDefinition(); methodBase = genericTypeDefinition.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Cast<MethodBase>().Concat(genericTypeDefinition.GetConstructors(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Cast<MethodBase>()) .FirstOrDefault((MethodBase candidate) => candidate.MetadataToken == method.MetadataToken); if (methodBase == null) { throw new InvalidDataException("Could not recover generic method definition by metadata token."); } } MethodInfo methodInfo2 = methodBase as MethodInfo; Type type = ((methodInfo2 != null) ? methodInfo2.ReturnType : typeof(void)); int num = (methodBase.IsGenericMethod ? methodBase.GetGenericArguments().Length : 0); ParameterInfo[] parameters = methodBase.GetParameters(); return "M(D=" + CanonicalTypeIdentity(declaringType) + ";N=" + Utf8Hex(method.Name) + ";S=" + (method.IsStatic ? "1" : "0") + ";GA=" + num.ToString(CultureInfo.InvariantCulture) + ";R=" + CanonicalTypeIdentity(type) + ";P=[" + string.Join(",", parameters.Select((ParameterInfo p) => CanonicalTypeIdentity(p.ParameterType)).ToArray()) + "])"; } private static string CanonicalTypeIdentity(Type type) { if (type == null) { throw new InvalidDataException("Null type cannot be canonicalized."); } if (type.IsByRef) { return "BR(" + CanonicalTypeIdentity(type.GetElementType()) + ")"; } if (type.IsPointer) { return "PTR(" + CanonicalTypeIdentity(type.GetElementType()) + ")"; } if (type.IsArray) { Type elementType = type.GetElementType(); if (type.GetArrayRank() == 1 && type == elementType.MakeArrayType()) { return "SZ(" + CanonicalTypeIdentity(elementType) + ")"; } return "AR(R=" + type.GetArrayRank().ToString(CultureInfo.InvariantCulture) + ";T=" + CanonicalTypeIdentity(elementType) + ";S=;L=)"; } if (type.IsGenericParameter) { string text = ((type.DeclaringMethod != null) ? "M" : "T"); return "GP(" + text + ";P=" + type.GenericParameterPosition.ToString(CultureInfo.InvariantCulture) + ")"; } if (type.IsGenericType && !type.IsGenericTypeDefinition) { Type genericTypeDefinition = type.GetGenericTypeDefinition(); return "GI(D=" + CanonicalNamedType(genericTypeDefinition) + ";A=[" + string.Join(",", type.GetGenericArguments().Select(CanonicalTypeIdentity).ToArray()) + "])"; } return CanonicalNamedType(type); } private static string CanonicalNamedType(Type type) { string fullName = type.FullName; if (string.IsNullOrEmpty(fullName)) { throw new InvalidDataException("Named type has no FullName: " + type); } string text = type.Assembly.GetName().Name; if (string.IsNullOrEmpty(text)) { throw new InvalidDataException("Named type has no assembly simple name: " + fullName); } switch (text) { case "mscorlib": case "System": case "System.Core": case "netstandard": text = "CORE"; break; } return "N(A=" + Utf8Hex(text) + ";F=" + Utf8Hex(fullName) + ")"; } private static string Utf8Hex(string value) { return ToHex(Encoding.UTF8.GetBytes(value)); } private static bool ValidateMethodIlHash(MethodInfo method, string expected, string displayName, out string reason) { reason = null; byte[] array = method.GetMethodBody()?.GetILAsByteArray(); if (array == null) { reason = displayName + " has no inspectable IL body."; return false; } string text; using (SHA256 sHA = SHA256.Create()) { text = ToHex(sHA.ComputeHash(array)); } if (!string.Equals(text, expected, StringComparison.OrdinalIgnoreCase)) { reason = displayName + " IL SHA-256 mismatch. Expected " + expected + ", got " + text + "."; return false; } return true; } private static Dictionary<ushort, OpCode> BuildOpCodeMap() { Dictionary<ushort, OpCode> dictionary = new Dictionary<ushort, OpCode>(); FieldInfo[] fields = typeof(OpCodes).GetFields(BindingFlags.Static | BindingFlags.Public); foreach (FieldInfo fieldInfo in fields) { if (!(fieldInfo.FieldType != typeof(OpCode))) { OpCode value = (OpCode)fieldInfo.GetValue(null); dictionary[(ushort)value.Value] = value; } } return dictionary; } private static bool TryDecode(MethodInfo method, int expectedCodeSize, string displayName, out List<DecodedInstruction> instructions, out string reason) { instructions = null; reason = null; byte[] array = method.GetMethodBody()?.GetILAsByteArray(); if (array == null) { reason = displayName + " has no inspectable IL body."; return false; } if (array.Length != expectedCodeSize) { reason = displayName + " IL code-size mismatch. Expected " + expectedCodeSize.ToString(CultureInfo.InvariantCulture) + ", got " + array.Length.ToString(CultureInfo.InvariantCulture) + "."; return false; } List<DecodedInstruction> list = new List<DecodedInstruction>(); int num = 0; try { while (num < array.Length) { int offset = num; ushort num2 = array[num++]; if (num2 == 254) { if (num >= array.Length) { throw new InvalidDataException("truncated two-byte opcode"); } num2 = (ushort)(0xFE00 | array[num++]); } if (!OpCodeMap.TryGetValue(num2, out var value)) { throw new InvalidDataException("unknown opcode 0x" + num2.ToString("X4")); } object obj = null; switch (value.OperandType) { case OperandType.ShortInlineI: obj = (sbyte)array[num++]; break; case OperandType.InlineI: obj = BitConverter.ToInt32(array, num); num += 4; break; case OperandType.InlineI8: obj = BitConverter.ToInt64(array, num); num += 8; break; case OperandType.ShortInlineR: obj = BitConverter.ToSingle(array, num); num += 4; break; case OperandType.InlineR: obj = BitConverter.ToDouble(array, num); num += 8; break; case OperandType.ShortInlineVar: obj = array[num++]; break; case OperandType.InlineVar: obj = BitConverter.ToUInt16(array, num); num += 2; break; case OperandType.ShortInlineBrTarget: obj = num + 1 + (sbyte)array[num]; num++; break; case OperandType.InlineBrTarget: { int num5 = BitConverter.ToInt32(array, num); num += 4; obj = num + num5; break; } case OperandType.InlineSwitch: { int num3 = BitConverter.ToInt32(array, num); num += 4; int num4 = num + 4 * num3; int[] array2 = new int[num3]; for (int i = 0; i < num3; i++) { array2[i] = num4 + BitConverter.ToInt32(array, num); num += 4; } obj = array2; break; } case OperandType.InlineString: { int metadataToken5 = BitConverter.ToInt32(array, num); num += 4; obj = method.Module.ResolveString(metadataToken5); if (obj == null) { throw new InvalidDataException("unresolved string token"); } break; } case OperandType.InlineField: { int metadataToken4 = BitConverter.ToInt32(array, num); num += 4; obj = method.Module.ResolveField(metadataToken4, method.DeclaringType.GetGenericArguments(), method.GetGenericArguments()); if (obj == null) { throw new InvalidDataException("unresolved field token"); } break; } case OperandType.InlineMethod: { int metadataToken3 = BitConverter.ToInt32(array, num); num += 4; obj = method.Module.ResolveMethod(metadataToken3, method.DeclaringType.GetGenericArguments(), method.GetGenericArguments()); if (obj == null) { throw new InvalidDataException("unresolved method token"); } break; } case OperandType.InlineType: { int metadataToken2 = BitConverter.ToInt32(array, num); num += 4; obj = method.Module.ResolveType(metadataToken2, method.DeclaringType.GetGenericArguments(), method.GetGenericArguments()); if (obj == null) { throw new InvalidDataException("unresolved type token"); } break; } case OperandType.InlineTok: { int metadataToken = BitConverter.ToInt32(array, num); num += 4; obj = method.Module.ResolveMember(metadataToken, method.DeclaringType.GetGenericArguments(), method.GetGenericArguments()); if (obj == null) { throw new InvalidDataException("unresolved member token"); } break; } case OperandType.InlineSig: throw new InvalidDataException("InlineSig is not accepted by this fail-closed contract"); default: throw new InvalidDataException("unsupported operand type " + value.OperandType); case OperandType.InlineNone: break; } list.Add(new DecodedInstruction { Offset = offset, OpCode = value, Operand = obj }); } } catch (Exception ex) { reason = displayName + " normalized IL inspection failed closed: " + ex.GetType().Name + ": " + ex.Message; return false; } HashSet<int> offsets = new HashSet<int>(list.Select((DecodedInstruction instruction) => instruction.Offset)); offsets.Add(array.Length); foreach (DecodedInstruction item2 in list) { if (item2.Operand is int item && (item2.OpCode.OperandType == OperandType.InlineBrTarget || item2.OpCode.OperandType == OperandType.ShortInlineBrTarget) && !offsets.Contains(item)) { reason = displayName + " has a branch target outside instruction boundaries."; return false; } if (item2.Operand is int[] source && source.Any((int target) => !offsets.Contains(target))) { reason = displayName + " has an invalid switch target."; return false; } } instructions = list; return true; } private static string MemberId(object operand) { MemberInfo memberInfo = operand as MemberInfo; if (memberInfo == null) { return null; } if (memberInfo is MethodBase methodBase) { string text = string.Join(",", (from parameter in methodBase.GetParameters() select parameter.ParameterType.FullName ?? parameter.ParameterType.Name).ToArray()); MethodInfo methodInfo = methodBase as MethodInfo; string text2 = ((methodInfo != null) ? (methodInfo.ReturnType.FullName ?? methodInfo.ReturnType.Name) : "void"); return ((memberInfo.DeclaringType != null) ? memberInfo.DeclaringType.FullName : string.Empty) + "::" + memberInfo.Name + "(" + text + ")->" + text2; } if (memberInfo is FieldInfo fieldInfo) { return ((fieldInfo.DeclaringType != null) ? fieldInfo.DeclaringType.FullName : string.Empty) + "::" + fieldInfo.Name + ":" + (fieldInfo.FieldType.FullName ?? fieldInfo.FieldType.Name); } if (memberInfo is Type type) { return type.FullName; } return ((memberInfo.DeclaringType != null) ? memberInfo.DeclaringType.FullName : string.Empty) + "::" + memberInfo.Name; } private static int FindMember(List<DecodedInstruction> instructions, string contains, int start) { for (int i = Math.Max(start, 0); i < instructions.Count; i++) { string text = MemberId(instructions[i].Operand); if (text != null && text.Contains(contains)) { return i; } } return -1; } private static int FindString(List<DecodedInstruction> instructions, string expected, int start) { for (int i = Math.Max(start, 0); i < instructions.Count; i++) { if (string.Equals(instructions[i].Operand as string, expected, StringComparison.Ordinal)) { return i; } } return -1; } private static string ToHex(byte[] bytes) { return BitConverter.ToString(bytes).Replace("-", string.Empty); } } }