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.1.1
plugins/M4A1Fix/M4A1Fix.dll
Decompiled 3 weeks agousing System; using System.Collections; 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.Compatibility.IL; using M4A1Fix.Patches; using M4A1Fix.Patches.IL; using M4A1Fix.Runtime; using M4A1Mod.Survivors.M4A1.Utils; using Microsoft.CodeAnalysis; using RoR2; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.Networking; using UnityEngine.ResourceManagement.AsyncOperations; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: InternalsVisibleTo("M4A1Fix.Tests")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("M4A1Fix")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.1.0.0")] [assembly: AssemblyInformationalVersion("1.1.0")] [assembly: AssemblyProduct("M4A1Fix")] [assembly: AssemblyTitle("M4A1Fix")] [assembly: AssemblyVersion("1.1.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.1.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.1.0"; internal const string M4A1PluginGuid = "com.Snoresville.M4A1"; private Harmony _revivePriorityHarmony; private Harmony _shrineAiRecoveryHarmony; private RunStartWarmup _runStartWarmup; 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.1.0 loaded."); _revivePriorityHarmony = new Harmony("com.m4a1fix.revivepriority"); if (!RevivePriorityPatch.TryInstall(_revivePriorityHarmony, 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)); return; } ((BaseUnityPlugin)this).Logger.LogInfo((object)"ShrineAIRecovery enabled."); if (RunStartWarmup.TryCreate(this, ((BaseUnityPlugin)this).Logger, out _runStartWarmup, out reason)) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"RunStartWarmup enabled; waiting for game content."); } else { ((BaseUnityPlugin)this).Logger.LogWarning((object)("RunStartWarmup disabled: " + reason)); } } private void OnDestroy() { try { _runStartWarmup?.Dispose(); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("RunStartWarmup cleanup failed: " + ex)); } if (_shrineAiRecoveryHarmony != null && !HarmonyPatchLifecycle.TryUnpatch(_shrineAiRecoveryHarmony, out var reason)) { ((BaseUnityPlugin)this).Logger.LogError((object)("ShrineAIRecovery cleanup failed: " + reason)); } if (_revivePriorityHarmony != null && !HarmonyPatchLifecycle.TryUnpatch(_revivePriorityHarmony, out var reason2)) { ((BaseUnityPlugin)this).Logger.LogError((object)("RevivePriority cleanup failed: " + reason2)); } } } } namespace M4A1Fix.Runtime { internal interface IAssetWarmupOperation : IDisposable { bool IsDone { get; } bool Succeeded { get; } } internal enum AssetWarmupStatus { Loading, Completed, Skipped, Failed } internal sealed class AssetWarmupJob : IEnumerator, IDisposable { private readonly List<object> _keys = new List<object>(); private readonly List<IAssetWarmupOperation> _operations = new List<IAssetWarmupOperation>(); private readonly Func<object, IAssetWarmupOperation> _load; private readonly Func<bool> _canContinue; private readonly Action _finalize; private readonly int _batchSize; private int _next; private bool _stopped; internal AssetWarmupStatus Status { get; private set; } internal string Error { get; private set; } internal int Count => _keys.Count; public object Current => null; internal AssetWarmupJob(IEnumerable<object> keys, Func<object, IAssetWarmupOperation> load, Func<bool> canContinue, Action finalize, int batchSize = 8) { if (keys == null) { throw new ArgumentNullException("keys"); } _load = load ?? throw new ArgumentNullException("load"); _canContinue = canContinue ?? throw new ArgumentNullException("canContinue"); _finalize = finalize ?? throw new ArgumentNullException("finalize"); if (batchSize <= 0) { throw new ArgumentOutOfRangeException("batchSize"); } _batchSize = batchSize; HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); foreach (object key in keys) { if (key == null || string.IsNullOrEmpty(key.ToString())) { throw new ArgumentException("Asset keys must be nonempty.", "keys"); } if (hashSet.Add(key.ToString())) { _keys.Add(key); } } } public bool MoveNext() { if (_stopped) { return false; } try { if (!_canContinue()) { return Stop(AssetWarmupStatus.Skipped); } if (_next < _keys.Count) { int num = Math.Min(_next + _batchSize, _keys.Count); while (_next < num) { IAssetWarmupOperation assetWarmupOperation = _load(_keys[_next++]); if (assetWarmupOperation == null) { throw new InvalidOperationException("Asset loader returned no operation."); } _operations.Add(assetWarmupOperation); } return true; } foreach (IAssetWarmupOperation operation in _operations) { if (!operation.IsDone) { return true; } if (!operation.Succeeded) { throw new InvalidOperationException("An item-display asset failed to load."); } } if (!_canContinue()) { return Stop(AssetWarmupStatus.Skipped); } _finalize(); return Stop(AssetWarmupStatus.Completed); } catch (Exception ex) { Error = ex.GetType().Name + ": " + ex.Message; return Stop(AssetWarmupStatus.Failed); } } private bool Stop(AssetWarmupStatus status) { Status = status; Dispose(); return false; } public void Dispose() { if (_stopped) { return; } _stopped = true; if (Status == AssetWarmupStatus.Loading) { Status = AssetWarmupStatus.Skipped; } foreach (IAssetWarmupOperation operation in _operations) { try { operation.Dispose(); } catch (Exception ex) { Status = AssetWarmupStatus.Failed; Error = ((Error == null) ? "" : (Error + " ")) + "Asset release failed: " + ex.Message; } } _operations.Clear(); } public void Reset() { throw new NotSupportedException(); } } internal static class HarmonyPatchLifecycle { internal static bool TryUnpatch(Harmony harmony, out string reason) { reason = null; try { harmony.UnpatchSelf(); return true; } catch (Exception ex) { reason = ex.ToString(); return false; } } internal static bool HasTranspiler(MethodBase target, string owner, MethodInfo method) { 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 && transpiler.PatchMethod == method) { return true; } } return false; } internal static bool HasPostfix(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; } } } namespace M4A1Fix.Patches { internal static class RevivePriorityPatch { private static bool _transpilerMatched; private static readonly MethodInfo TranspilerMethod = AccessTools.Method(typeof(RevivePriorityPatch), "CharacterMasterOnBodyDeathTranspiler", (Type[])null, (Type[])null); internal static bool TryInstall(Harmony harmony, out MethodInfo target, out string reason) { //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Expected O, but got Unknown target = null; reason = null; _transpilerMatched = false; if (harmony == null) { reason = "Harmony owner was not created."; return false; } bool flag = false; try { if (!M4A1Compatibility.TryValidateRevivePriorityTarget(out target, out reason)) { return false; } MethodInfo transpilerMethod = TranspilerMethod; if (transpilerMethod == null) { throw new InvalidOperationException("revive-priority transpiler method could not be resolved."); } flag = true; harmony.Patch((MethodBase)target, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(transpilerMethod), (HarmonyMethod)null, (HarmonyMethod)null); if (!IsHealthy(target)) { throw new InvalidOperationException("revive-priority transpiler was not confirmed on the exact target."); } return true; } catch (Exception ex) { _transpilerMatched = false; reason = "installation failed: " + ex; if (flag && !HarmonyPatchLifecycle.TryUnpatch(harmony, out var reason2)) { reason = reason + " Cleanup failed: " + reason2; } target = null; return false; } } internal static bool IsHealthy(MethodInfo exactTarget) { if (_transpilerMatched && exactTarget != null) { return HarmonyPatchLifecycle.HasTranspiler(exactTarget, "com.m4a1fix.revivepriority", TranspilerMethod); } return false; } private static IEnumerable<CodeInstruction> CharacterMasterOnBodyDeathTranspiler(IEnumerable<CodeInstruction> instructions, ILGenerator generator) { _transpilerMatched = false; IEnumerable<CodeInstruction> result = RevivePriorityTranspiler.Rewrite(instructions, generator); _transpilerMatched = true; return result; } } internal static class RevivePriorityPolicy { internal static bool ShouldDelegateShrineToVanilla(CharacterBody body) { if (NetworkServer.active && (Object)(object)body != (Object)null) { return RoR2ReviveContract.HasShrineBuff(body); } return false; } } internal sealed class RunStartWarmup : IDisposable { private sealed class AddressableOperation : IAssetWarmupOperation, IDisposable { private AsyncOperationHandle<Object> _handle; public bool IsDone => _handle.IsDone; public bool Succeeded { get { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Invalid comparison between Unknown and I4 if ((int)_handle.Status == 1) { return _handle.Result is GameObject; } return false; } } internal AddressableOperation(object key) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) _handle = Addressables.LoadAssetAsync<Object>(key); } public void Dispose() { //IL_000e: Unknown result type (might be due to invalid IL or missing references) if (_handle.IsValid()) { Addressables.Release<Object>(_handle); } } } private readonly Plugin _owner; private readonly ManualLogSource _log; private readonly ItemDisplayWarmupContract _contract; private AssetWarmupJob _job; private Coroutine _coroutine; private bool _disposed; private RunStartWarmup(Plugin owner, ManualLogSource log, ItemDisplayWarmupContract contract) { _owner = owner; _log = log; _contract = contract; RoR2Application.onLoadFinished = (Action)Delegate.Combine(RoR2Application.onLoadFinished, new Action(Begin)); } internal static bool TryCreate(Plugin owner, ManualLogSource log, out RunStartWarmup warmup, out string reason) { warmup = null; try { if (!ItemDisplayWarmupContract.TryResolve(out var contract, out reason)) { return false; } warmup = new RunStartWarmup(owner, log, contract); return true; } catch (Exception ex) { reason = ex.ToString(); return false; } } private void Begin() { RoR2Application.onLoadFinished = (Action)Delegate.Remove(RoR2Application.onLoadFinished, new Action(Begin)); if (_disposed) { return; } try { _coroutine = ((MonoBehaviour)_owner).StartCoroutine(Prepare()); } catch (Exception ex) { _log.LogWarning((object)("RunStartWarmup fallback: " + ex)); } } private IEnumerator Prepare() { yield return null; _log.LogInfo((object)"RunStartWarmup: waiting for BodyCatalog."); IEnumerator readiness = WaitForCatalog(() => ((ResourceAvailability)(ref BodyCatalog.availability)).available, () => !_disposed && (Object)(object)Run.instance == (Object)null); while (readiness.MoveNext()) { yield return null; } if (_disposed || (Object)(object)Run.instance != (Object)null) { _log.LogWarning((object)"RunStartWarmup: Skipped while waiting for catalog; original first-run path retained."); yield break; } Stopwatch timer = Stopwatch.StartNew(); try { if ((Object)(object)Run.instance != (Object)null || _contract.IsInitialized) { yield break; } List<object> keys = CollectKeys(); _job = new AssetWarmupJob(keys, (object key) => new AddressableOperation(key), () => !_disposed && (Object)(object)Run.instance == (Object)null, delegate { Stopwatch stopwatch = Stopwatch.StartNew(); _contract.InitializeCache(); _log.LogInfo((object)("RunStartWarmup: original cache initialized in " + stopwatch.ElapsedMilliseconds + " ms.")); }); _log.LogInfo((object)("RunStartWarmup: asynchronously preparing " + _job.Count + " display assets.")); } catch (Exception ex) { _log.LogWarning((object)("RunStartWarmup fallback: " + ex)); } if (_job != null) { while (_job.MoveNext()) { yield return null; } if (_job.Status == AssetWarmupStatus.Completed) { _log.LogInfo((object)("RunStartWarmup: ready before first run; background elapsed " + timer.ElapsedMilliseconds + " ms.")); } else { _log.LogWarning((object)("RunStartWarmup: " + _job.Status.ToString() + "; original first-run path retained. " + _job.Error)); } _job.Dispose(); _job = null; _coroutine = null; } } internal static IEnumerator WaitForCatalog(Func<bool> ready, Func<bool> canContinue) { while (canContinue() && !ready()) { yield return null; } } private static List<object> CollectKeys() { //IL_00e1: Unknown result type (might be due to invalid IL or missing references) //IL_00e6: Unknown result type (might be due to invalid IL or missing references) //IL_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_00f6: Unknown result type (might be due to invalid IL or missing references) //IL_00f8: Unknown result type (might be due to invalid IL or missing references) //IL_010d: Unknown result type (might be due to invalid IL or missing references) //IL_0112: Unknown result type (might be due to invalid IL or missing references) GameObject val = BodyCatalog.FindBodyPrefab("MercBody"); ModelLocator val2 = (((Object)(object)val != (Object)null) ? val.GetComponent<ModelLocator>() : null); CharacterModel val3 = (((Object)(object)val2 != (Object)null && (Object)(object)val2.modelTransform != (Object)null) ? ((Component)val2.modelTransform).GetComponent<CharacterModel>() : null); if ((Object)(object)val3 == (Object)null || (Object)(object)val3.itemDisplayRuleSet == (Object)null) { throw new InvalidOperationException("MercBody item displays missing after BodyCatalog readiness: body=" + ((Object)(object)val != (Object)null) + ", locator=" + ((Object)(object)val2 != (Object)null) + ", model=" + ((Object)(object)val3 != (Object)null) + "."); } List<object> list = new List<object>(); KeyAssetRuleGroup[] keyAssetRuleGroups = val3.itemDisplayRuleSet.keyAssetRuleGroups; foreach (KeyAssetRuleGroup val4 in keyAssetRuleGroups) { if (val4.displayRuleGroup.rules == null) { continue; } ItemDisplayRule[] rules = val4.displayRuleGroup.rules; foreach (ItemDisplayRule val5 in rules) { AssetReferenceGameObject followerPrefabAddress = val5.followerPrefabAddress; if ((Object)(object)val5.followerPrefab == (Object)null && followerPrefabAddress != null && ((AssetReference)followerPrefabAddress).RuntimeKeyIsValid()) { list.Add(((AssetReference)followerPrefabAddress).RuntimeKey); } } } return list; } public void Dispose() { _disposed = true; RoR2Application.onLoadFinished = (Action)Delegate.Remove(RoR2Application.onLoadFinished, new Action(Begin)); try { if (_coroutine != null) { ((MonoBehaviour)_owner).StopCoroutine(_coroutine); } } finally { _job?.Dispose(); if (_job?.Error != null) { _log.LogWarning((object)("RunStartWarmup cleanup: " + _job.Error)); } _job = null; _coroutine = null; } } } 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 static ManualLogSource _log; private static MethodInfo _revivePriorityTarget; internal static bool TryInstall(ManualLogSource log, MethodInfo revivePriorityTarget, out Harmony harmony, out string reason) { //IL_00f0: Unknown result type (might be due to invalid IL or missing references) //IL_00f6: Expected O, but got Unknown //IL_0102: Unknown result type (might be due to invalid IL or missing references) //IL_010f: Expected O, but got Unknown _log = log; harmony = null; reason = null; _revivePriorityTarget = null; try { if (!RevivePriorityPatch.IsHealthy(revivePriorityTarget)) { reason = "RevivePriority is not healthy on the exact M4A1 death target."; return false; } if (!M4A1AssemblyContract.TryValidateLoadedPlugin(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 (!MethodLookup.TryResolveSingleDeclaredMethod(type, "CharacterMaster_OnBodyDeath", out var method, out reason)) { return false; } if (!method.Equals(revivePriorityTarget) || !RevivePriorityPatch.IsHealthy(method)) { reason = "RevivePriority is not installed on the exact M4A1 CharacterMaster_OnBodyDeath target."; return false; } if (!MethodLookup.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; } harmony = new Harmony("com.m4a1fix.revivepriority/shrine-ai-recovery"); _revivePriorityTarget = revivePriorityTarget; harmony.Patch((MethodBase)method2, (HarmonyMethod)null, new HarmonyMethod(method3), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); if (!HarmonyPatchLifecycle.HasPostfix(method2, "com.m4a1fix.revivepriority/shrine-ai-recovery")) { throw new InvalidOperationException("Harmony did not report the ShrineAIRecovery postfix after installation."); } return true; } catch (Exception ex) { _revivePriorityTarget = null; reason = "installation exception: " + ex; if (harmony == null || HarmonyPatchLifecycle.TryUnpatch(harmony, out var reason2)) { harmony = null; } else { reason = reason + " Cleanup failed: " + reason2; } 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 { if (!RevivePriorityPatch.IsHealthy(_revivePriorityTarget)) { if (_log != null) { _log.LogError((object)"ShrineAIRecovery skipped: RevivePriority is no longer healthy on the exact death target."); } return; } 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.Patches.IL { internal static class RevivePriorityTranspiler { private readonly struct DeathHookDecision { internal int OriginalCallStart { get; } internal int DecisionLoadIndex { get; } internal DeathHookDecision(int originalCallStart, int decisionLoadIndex) { OriginalCallStart = originalCallStart; DecisionLoadIndex = decisionLoadIndex; } } private const int OriginalCallbackArgumentCount = 3; private const int BodyArgumentIndex = 2; internal static IEnumerable<CodeInstruction> Rewrite(IEnumerable<CodeInstruction> instructions, ILGenerator generator) { List<CodeInstruction> list = new List<CodeInstruction>(instructions); DeathHookDecision decision = FindDecision(list); InjectShrineDecision(list, generator, decision); return list; } private static DeathHookDecision FindDecision(List<CodeInstruction> codes) { int num = FindOrigOnBodyDeathInvoke(codes); int num2 = FindGetSomeChildDummyLink(codes); if (num < 3 || num2 < 1) { throw PatternMismatch("original callback or Dummy Link call is missing"); } int num3 = num - 3; int num4 = num2 - 1; if (num3 >= num4) { throw PatternMismatch("original callback does not precede Dummy Link selection"); } if (!IsLdarg(codes[num3], 0) || !IsLdarg(codes[num3 + 1], 1) || !IsLdarg(codes[num3 + 2], 2) || !IsLdarg(codes[num4], 2)) { throw PatternMismatch("callback or Dummy Link argument loads changed"); } int num5 = FindConditionalBranchTo(codes, num4, num3); if (num5 <= 0 || !IsLocalLoad(codes[num5 - 1]) || (codes[num5].opcode != OpCodes.Brfalse && codes[num5].opcode != OpCodes.Brfalse_S)) { throw PatternMismatch("expected local Boolean/brfalse decision changed"); } int decisionLoadIndex = num5 - 1; return new DeathHookDecision(num3, decisionLoadIndex); } private static void InjectShrineDecision(List<CodeInstruction> codes, ILGenerator generator, DeathHookDecision decision) { //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Expected O, but got Unknown //IL_00a5: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Expected O, but got Unknown //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Expected O, but got Unknown //IL_00cc: Unknown result type (might be due to invalid IL or missing references) //IL_00d6: Expected O, but got Unknown Label label = generator.DefineLabel(); Label label2 = generator.DefineLabel(); codes[decision.OriginalCallStart].labels.Add(label); codes[decision.DecisionLoadIndex].labels.Add(label2); CodeInstruction item = CloneWithoutLabelsOrBlocks(codes[decision.DecisionLoadIndex]); MethodInfo methodInfo = AccessTools.Method(typeof(RevivePriorityPolicy), "ShouldDelegateShrineToVanilla", (Type[])null, (Type[])null); if (methodInfo == null) { throw PatternMismatch("Shrine decision helper is missing"); } List<CodeInstruction> list = new List<CodeInstruction> { item, new CodeInstruction(OpCodes.Brtrue, (object)label2), new CodeInstruction(OpCodes.Ldarg_2, (object)null), new CodeInstruction(OpCodes.Call, (object)methodInfo), new CodeInstruction(OpCodes.Brtrue, (object)label) }; MoveEntryMetadata(codes[decision.DecisionLoadIndex], list[0], label2); codes.InsertRange(decision.DecisionLoadIndex, list); } private static void MoveEntryMetadata(CodeInstruction originalDecisionLoad, CodeInstruction newEntry, Label continueOriginalDecisionLabel) { newEntry.labels.AddRange(originalDecisionLoad.labels.Where((Label label) => !label.Equals(continueOriginalDecisionLabel))); originalDecisionLoad.labels.RemoveAll((Label label) => !label.Equals(continueOriginalDecisionLabel)); newEntry.blocks.AddRange(originalDecisionLoad.blocks); originalDecisionLoad.blocks.Clear(); } private static InvalidOperationException PatternMismatch(string detail) { return new InvalidOperationException("M4A1 revive-priority IL pattern mismatch: " + detail + "."); } 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) { int num = -1; 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") { if (num >= 0) { throw PatternMismatch("Dummy Link selection is ambiguous"); } num = i; } } return num; } 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); } } } namespace M4A1Fix.Compatibility { internal static class CurrentRoR2ReviveContract { internal const string AssemblySha256 = "A5F7D83C7E6872FDD6099FDC299ADB7D4FA03CBF465E526D6F5EFCC35C2C281D"; internal static bool TryValidate(out Func<CharacterBody, BuffDef, bool> hasShrineBuff, out string reason) { hasShrineBuff = null; if (!TryValidateGameFile(typeof(CharacterMaster).Assembly.Location, Paths.ManagedPath, out reason)) { return false; } if (!Validate("OnBodyDeath", typeof(void), new Type[1] { typeof(CharacterBody) }, 624, "1050368A38FE8F544DDC254F0F4BC2B02F4FA42C2689E2DF06A5032D25C6D169", out var instructions, out reason) || !Validate("TryReviveOnBodyDeath", typeof(bool), new Type[1] { typeof(CharacterBody) }, 763, "2F1B2850A74F9FFD70F21A5487E65FFB05872A4616BD6530940C3B6090BDCEAA", out var instructions2, out reason) || !Validate("RespawnExtraLifeShrine", typeof(void), Type.EmptyTypes, 256, "B3D08729347A0036302C1F6D2BD05EC0038CB7210F527E4990B1905AA7BDB8AA", out var instructions3, out reason) || !Validate("IsExtraLifePendingServer", typeof(bool), Type.EmptyTypes, 75, "7DDA7E015613A20CBD68394435B95ECE885557F7875832D063F853B764157F74", out var instructions4, out reason)) { return false; } int num = IlMethodReader.FindMember(instructions2, "DLC2Content+Buffs::ExtraLifeBuff", 0); int num2 = IlMethodReader.FindMember(instructions2, "CharacterBody::HasUnmanagedBuff", num + 1); int num3 = IlMethodReader.FindMember(instructions2, "CharacterBody::RemoveUnmanagedBuff", num2 + 1); int num4 = IlMethodReader.FindString(instructions2, "RespawnExtraLifeShrine", num3 + 1); int num5 = IlMethodReader.FindMember(instructions2, "ItemTransformation::TryTake", num4 + 1); if (num < 0 || num2 <= num || num3 <= num2 || num4 <= num3 || num5 <= num4 || IlMethodReader.FindMember(instructions, "CharacterMaster::TryReviveOnBodyDeath", 0) < 0 || IlMethodReader.FindMember(instructions3, "CharacterMaster::Respawn", 0) < 0 || IlMethodReader.FindString(instructions4, "RespawnExtraLifeShrine", 0) < 0) { reason = "Current RoR2 Shrine-before-Dio/respawn/pending landmarks mismatch."; return false; } MethodInfo method = typeof(CharacterBody).GetMethod("HasUnmanagedBuff", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(BuffDef) }, null); if (method == null || method.ReturnType != typeof(bool)) { reason = "RoR2 HasUnmanagedBuff(BuffDef) signature mismatch."; return false; } hasShrineBuff = (Func<CharacterBody, BuffDef, bool>)Delegate.CreateDelegate(typeof(Func<CharacterBody, BuffDef, bool>), method); reason = null; return true; } internal static bool IsSupportedHash(string hash) { return string.Equals(hash, "A5F7D83C7E6872FDD6099FDC299ADB7D4FA03CBF465E526D6F5EFCC35C2C281D", StringComparison.OrdinalIgnoreCase); } private static bool TryValidateGameFile(string loadedLocation, string managedDirectory, out string reason) { reason = null; string text = loadedLocation; if (string.IsNullOrEmpty(text)) { if (string.IsNullOrWhiteSpace(managedDirectory)) { reason = "RoR2 assembly is memory-loaded and BepInEx ManagedPath is unavailable."; return false; } text = Path.Combine(managedDirectory, "RoR2.dll"); } if (!File.Exists(text)) { reason = "RoR2.dll for compatibility hashing was not found: " + text; return false; } try { string text2 = Sha256Hash.ComputeFile(text); if (IsSupportedHash(text2)) { return true; } reason = "M4A1 1.1.5 requires the inspected RoR2.dll SHA-256 A5F7D83C7E6872FDD6099FDC299ADB7D4FA03CBF465E526D6F5EFCC35C2C281D, got " + text2 + "."; } catch (IOException ex) { reason = "RoR2.dll compatibility hash read failed: " + ex.Message; } catch (UnauthorizedAccessException ex2) { reason = "RoR2.dll compatibility hash read denied: " + ex2.Message; } return false; } private static bool Validate(string name, Type result, Type[] parameters, int size, string canonicalHash, out List<DecodedInstruction> instructions, out string reason) { instructions = null; MethodInfo method = typeof(CharacterMaster).GetMethod(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, parameters, null); if (method == null || method.ReturnType != result) { reason = "Current RoR2 " + name + " signature mismatch."; return false; } return MethodFingerprint.TryValidate(method, size, canonicalHash, "Current RoR2 " + name, out instructions, out reason); } } internal sealed class ItemDisplayWarmupContract { private readonly MethodInfo _lazyInit; private readonly FieldInfo[] _cacheFields; internal bool IsInitialized => (bool)_cacheFields[0].GetValue(null); private ItemDisplayWarmupContract(MethodInfo lazyInit, FieldInfo[] cacheFields) { _lazyInit = lazyInit; _cacheFields = cacheFields; } internal static bool TryResolve(out ItemDisplayWarmupContract contract, out string reason) { contract = null; if (!M4A1AssemblyContract.TryGetSupportedAssembly(out var assembly, out reason)) { return false; } if (!M4A1AssemblyContract.IsCurrentAssembly(assembly)) { reason = "Item-display warmup requires the inspected M4A1 1.1.5."; return false; } Type type = assembly.GetType("M4A1Mod.Modules.ItemDisplays", throwOnError: false); if (type == null) { reason = "Original item-display type is missing."; return false; } string[] array = new string[3] { "LazyInit", "PopulateDisplays", "PopulateFromBody" }; string[] array2 = new string[3] { "24E9E0D92BED91043AE8B96C4032D02378E33B75ABA5A51482E21EC097C3A948", "435C8CD38B347B07F1BA808C0E9198DDDAB4382E1D136E8D98AFC1E21089517B", "5FA7B92F2D2AC90B56BEB4D85A623082A74FB6567BBE03CB07EC7B7A23C50276" }; MethodInfo lazyInit = null; for (int i = 0; i < array.Length; i++) { if (!MethodLookup.TryResolveSingleDeclaredMethod(type, array[i], out var method, out reason)) { return false; } if (!method.IsStatic || method.ReturnType != typeof(void) || method.GetParameters().Length != ((i == 2) ? 1 : 0) || (i == 2 && method.GetParameters()[0].ParameterType != typeof(string)) || !MethodFingerprint.TryValidateRaw(method, array2[i], array[i], out reason)) { reason = reason ?? (array[i] + " signature mismatch."); return false; } if (i == 0) { lazyInit = method; } } string[] array3 = new string[4] { "initialized", "itemDisplayPrefabs", "KeyAssetDisplayPrefabs", "KeyAssets" }; FieldInfo[] array4 = new FieldInfo[array3.Length]; for (int j = 0; j < array3.Length; j++) { array4[j] = type.GetField(array3[j], BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (array4[j] == null) { reason = "Item-display cache field missing: " + array3[j]; return false; } if (j > 0 && !typeof(IDictionary).IsAssignableFrom(array4[j].FieldType)) { reason = "Item-display dictionary field changed: " + array3[j]; return false; } } if (array4[0].FieldType != typeof(bool)) { reason = "Item-display initialized flag changed."; return false; } contract = new ItemDisplayWarmupContract(lazyInit, array4); return true; } internal void InitializeCache() { if (IsInitialized) { return; } object[] array = new object[_cacheFields.Length]; List<DictionaryEntry>[] array2 = new List<DictionaryEntry>[array.Length]; for (int i = 0; i < array.Length; i++) { array[i] = _cacheFields[i].GetValue(null); if (!(array[i] is IDictionary dictionary)) { continue; } array2[i] = new List<DictionaryEntry>(); foreach (DictionaryEntry item in dictionary) { array2[i].Add(item); } } try { _lazyInit.Invoke(null, null); } catch { for (int j = 0; j < array.Length; j++) { if (array[j] is IDictionary dictionary2) { dictionary2.Clear(); foreach (DictionaryEntry item2 in array2[j]) { dictionary2.Add(item2.Key, item2.Value); } } _cacheFields[j].SetValue(null, array[j]); } throw; } } } internal static class M4A1AssemblyContract { internal const string ExpectedAssemblySha256 = "600188D306782F6C1B26EA1BDA4A615BFF9AC4437318873D5728CD58B26A7154"; internal const string CurrentAssemblySha256 = "023B3FE8F86F9DCE8FD9F203BA3580D565A9940D2E0E82AB3FA129EDC33CC6F0"; private const string ExpectedDeathHookIlSha256 = "79150F880D5A6361BEF0E94A78AABB57B7A56881565C843E8A3D2614D65FE5E4"; private const string CurrentDeathHookIlSha256 = "702CCE7C58854F4AB9123B01E59E87B363536F46CCB2EE958C78AB5261ECB0C9"; internal const string DummyLinkHooksTypeName = "M4A1Mod.Survivors.M4A1.Hooks.DummyLink"; internal const string UtilsTypeName = "M4A1Mod.Survivors.M4A1.Utils.M4A1"; internal static bool TryGetSupportedAssembly(out Assembly assembly, out string reason) { assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault((Assembly a) => string.Equals(a.GetName().Name, "M4A1Mod", StringComparison.Ordinal)); reason = null; if (assembly == null) { reason = "M4A1Mod assembly is not loaded."; return false; } return ValidateAssemblyHash(assembly, out reason); } internal static bool IsCurrentAssembly(Assembly assembly) { return string.Equals(Sha256Hash.ComputeFile(assembly.Location), "023B3FE8F86F9DCE8FD9F203BA3580D565A9940D2E0E82AB3FA129EDC33CC6F0", StringComparison.OrdinalIgnoreCase); } internal static bool TryGetDeathHook(Assembly assembly, out MethodInfo target, out string reason) { target = null; reason = null; if (!ValidateAssemblyHash(assembly, out reason)) { 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; } MethodInfo method = type.GetMethod("CharacterMaster_OnBodyDeath", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (!ValidateM4A1TargetShape(method, out reason) || !MethodFingerprint.TryValidateRaw(method, IsCurrentAssembly(assembly) ? "702CCE7C58854F4AB9123B01E59E87B363536F46CCB2EE958C78AB5261ECB0C9" : "79150F880D5A6361BEF0E94A78AABB57B7A56881565C843E8A3D2614D65FE5E4", "M4A1 CharacterMaster_OnBodyDeath", out reason)) { return false; } target = method; 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, out string reason) { reason = null; string location = assembly.Location; if (string.IsNullOrEmpty(location) || !File.Exists(location)) { reason = "M4A1Mod.dll location is unavailable for compatibility hashing."; return false; } string text; try { text = Sha256Hash.ComputeFile(location); } catch (IOException ex) { reason = "M4A1Mod.dll hash read failed: " + ex.Message; return false; } catch (UnauthorizedAccessException ex2) { reason = "M4A1Mod.dll hash read denied: " + ex2.Message; return false; } if (!IsSupportedHash(text)) { reason = "M4A1Mod.dll SHA-256 mismatch. Expected verified M4A1 1.1.4 or 1.1.5, got " + text + "."; return false; } return true; } internal static bool IsSupportedHash(string hash) { if (!string.Equals(hash, "600188D306782F6C1B26EA1BDA4A615BFF9AC4437318873D5728CD58B26A7154", StringComparison.OrdinalIgnoreCase)) { return string.Equals(hash, "023B3FE8F86F9DCE8FD9F203BA3580D565A9940D2E0E82AB3FA129EDC33CC6F0", StringComparison.OrdinalIgnoreCase); } return true; } internal static bool TryValidateLoadedPlugin(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 (!IsSupportedHash(hashHex)) { 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; if (!ValidateAssemblyHash(assembly, out reason)) { return false; } if (!string.Equals(hashHex, Sha256Hash.ComputeFile(assembly.Location), StringComparison.OrdinalIgnoreCase)) { reason = "M4A1 PluginInfo file and live instance assembly have different SHA-256 identities."; return false; } return true; } private static bool TrySha256(string filePath, out string hashHex, out string reason) { hashHex = null; reason = null; try { hashHex = Sha256Hash.ComputeFile(filePath); return true; } catch (Exception ex) { reason = "SHA-256 read failed for loaded M4A1 plugin bytes: " + ex.GetType().Name + ": " + ex.Message; return false; } } } internal static class M4A1Compatibility { internal static bool TryValidateRevivePriorityTarget(out MethodInfo target, out string reason) { target = null; if (!M4A1AssemblyContract.TryGetSupportedAssembly(out var assembly, out reason) || !RoR2ReviveContract.TryValidateTypeIdentity(out reason) || !M4A1AssemblyContract.TryGetDeathHook(assembly, out var target2, out reason) || !RoR2ReviveContract.TryValidate(M4A1AssemblyContract.IsCurrentAssembly(assembly), out reason)) { return false; } target = target2; return true; } } internal static class MethodLookup { private const BindingFlags DeclaredMethodFlags = BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; internal 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; } } internal static class RoR2ReviveContract { private delegate bool MethodValidator(MethodInfo method, out string reason); 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"; private static Func<CharacterBody, BuffDef, bool> _hasShrineBuff; internal static bool HasShrineBuff(CharacterBody body) { if (_hasShrineBuff != null) { return _hasShrineBuff(body, Buffs.ExtraLifeBuff); } return false; } internal static bool TryValidateTypeIdentity(out string reason) { reason = null; if (typeof(CharacterMaster).Assembly == typeof(CharacterBody).Assembly) { return true; } reason = "RoR2 CharacterMaster and CharacterBody are not from the same assembly."; return false; } internal static bool TryValidate(bool currentUpstream, out string reason) { _hasShrineBuff = null; if (currentUpstream) { return CurrentRoR2ReviveContract.TryValidate(out _hasShrineBuff, out reason); } int num; if (TryValidateMethod("TryReviveOnBodyDeath", typeof(bool), new Type[1] { typeof(CharacterBody) }, ValidateTryReviveStructure, out reason) && TryValidateMethod("RespawnExtraLifeShrine", typeof(void), Type.EmptyTypes, ValidateShrineRespawnStructure, out reason)) { num = (TryValidateMethod("IsExtraLifePendingServer", typeof(bool), Type.EmptyTypes, ValidatePendingStateStructure, out reason) ? 1 : 0); if (num != 0) { _hasShrineBuff = (CharacterBody body, BuffDef buff) => body.HasBuff(buff); } } else { num = 0; } return (byte)num != 0; } private static bool TryValidateMethod(string name, Type returnType, Type[] arguments, MethodValidator validateBody, out string reason) { MethodInfo method = typeof(CharacterMaster).GetMethod(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, arguments, null); reason = null; if (method == null || method.ReturnType != returnType) { reason = "RoR2 " + name + " signature mismatch."; return false; } return validateBody(method, out reason); } private static bool ValidateTryReviveStructure(MethodInfo method, out string reason) { if (!MethodFingerprint.TryValidate(method, 632, "43D299DDC88201E148A1D914092175CB58285EDB1683822D825FDCC8E4163395", "RoR2 TryReviveOnBodyDeath", out var instructions, out reason)) { return false; } int num = IlMethodReader.FindMember(instructions, "DLC2Content+Buffs::ExtraLifeBuff", 0); int num2 = IlMethodReader.FindMember(instructions, "CharacterBody::HasBuff", num + 1); int num3 = IlMethodReader.FindMember(instructions, "CharacterBody::RemoveBuff", num2 + 1); int num4 = IlMethodReader.FindString(instructions, "RespawnExtraLifeShrine", num3 + 1); int num5 = IlMethodReader.FindMember(instructions, "UnityEngine.MonoBehaviour::Invoke(System.String,System.Single)", num4 + 1); int num6 = IlMethodReader.FindString(instructions, "PlayExtraLifeSFX", num5 + 1); int num7 = IlMethodReader.FindMember(instructions, "UnityEngine.MonoBehaviour::Invoke(System.String,System.Single)", num6 + 1); int num8 = IlMethodReader.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 (!MethodFingerprint.TryValidate(method, 264, "28D947645DE9C1E53D7C25591434D8B75ECB2E2E42492DF482D9A57296428E1F", "RoR2 RespawnExtraLifeShrine", out var instructions, out reason)) { return false; } int num = IlMethodReader.FindMember(instructions, "CharacterMaster::Respawn", 0); int num2 = IlMethodReader.FindMember(instructions, "RoR2Content+Buffs::Immune", 0); int num3 = IlMethodReader.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 (!MethodFingerprint.TryValidate(method, 75, "7DDA7E015613A20CBD68394435B95ECE885557F7875832D063F853B764157F74", "RoR2 IsExtraLifePendingServer", out var instructions, out reason)) { return false; } int num = IlMethodReader.FindMember(instructions, "ExtraLifeServerBehavior", 0); int num2 = IlMethodReader.FindString(instructions, "RespawnSeekerRevive", 0); int num3 = IlMethodReader.FindString(instructions, "RespawnExtraLifeShrine", num2 + 1); int num4 = IlMethodReader.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; } } internal static class Sha256Hash { internal static string Compute(byte[] bytes) { using SHA256 sHA = SHA256.Create(); return Hex(sHA.ComputeHash(bytes)); } internal static string ComputeFile(string path) { using FileStream inputStream = File.OpenRead(path); using SHA256 sHA = SHA256.Create(); return Hex(sHA.ComputeHash(inputStream)); } private static string Hex(byte[] bytes) { return BitConverter.ToString(bytes).Replace("-", string.Empty); } } } namespace M4A1Fix.Compatibility.IL { internal static class CanonicalMethodFormatter { internal static bool TryBuildCanonicalMethod(MethodInfo method, int expectedCodeSize, string displayName, List<DecodedInstruction> instructions, out string canonical, out string reason) { canonical = null; reason = null; 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)); AppendLocals(methodBody, list); if (!TryAppendInstructions(instructions, dictionary, displayName, list, out reason) || !TryAppendExceptionRegions(methodBody, dictionary, displayName, list, out reason)) { return false; } 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 void AppendLocals(MethodBody body, List<string> lines) { IList<LocalVariableInfo> localVariables = body.LocalVariables; lines.Add("LOCALS=" + localVariables.Count.ToString(CultureInfo.InvariantCulture)); for (int i = 0; i < localVariables.Count; i++) { LocalVariableInfo localVariableInfo = localVariables[i]; lines.Add("LOCAL[" + i.ToString(CultureInfo.InvariantCulture) + "]=T=" + CanonicalTypeIdentity(localVariableInfo.LocalType) + ";PINNED=" + (localVariableInfo.IsPinned ? "1" : "0")); } } private static bool TryAppendInstructions(List<DecodedInstruction> instructions, Dictionary<int, int> offsetToIndex, string displayName, List<string> lines, out string reason) { reason = null; lines.Add("INSTRUCTIONS=" + instructions.Count.ToString(CultureInfo.InvariantCulture)); for (int i = 0; i < instructions.Count; i++) { DecodedInstruction decodedInstruction = instructions[i]; if (!TryCanonicalOperand(decodedInstruction, offsetToIndex, out var canonical, out reason)) { reason = displayName + " canonical operand failed at instruction " + i.ToString(CultureInfo.InvariantCulture) + ": " + reason; return false; } lines.Add("I[" + i.ToString(CultureInfo.InvariantCulture) + "]=OP=" + decodedInstruction.OpCode.Name + ";O=" + canonical); } return true; } private static bool TryAppendExceptionRegions(MethodBody body, Dictionary<int, int> offsetToIndex, string displayName, List<string> lines, out string reason) { reason = null; IList<ExceptionHandlingClause> exceptionHandlingClauses = body.ExceptionHandlingClauses; lines.Add("EH=" + exceptionHandlingClauses.Count.ToString(CultureInfo.InvariantCulture)); for (int i = 0; i < exceptionHandlingClauses.Count; i++) { ExceptionHandlingClause exceptionHandlingClause = exceptionHandlingClauses[i]; if (!offsetToIndex.TryGetValue(exceptionHandlingClause.TryOffset, out var value) || !offsetToIndex.TryGetValue(exceptionHandlingClause.TryOffset + exceptionHandlingClause.TryLength, out var value2) || !offsetToIndex.TryGetValue(exceptionHandlingClause.HandlerOffset, out var value3) || !offsetToIndex.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 (!offsetToIndex.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"; } lines.Add("EH[" + i.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); } return true; } 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 string ToHex(byte[] bytes) { return BitConverter.ToString(bytes).Replace("-", string.Empty); } } internal sealed class DecodedInstruction { internal int Offset; internal OpCode OpCode; internal object Operand; } internal static class IlMethodReader { private static readonly Dictionary<ushort, OpCode> OpCodeMap = BuildOpCodeMap(); 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; } internal static bool TryRead(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 position = 0; try { while (position < array.Length) { int offset = position; ushort num = array[position++]; if (num == 254) { if (position >= array.Length) { throw new InvalidDataException("truncated two-byte opcode"); } num = (ushort)(0xFE00 | array[position++]); } if (!OpCodeMap.TryGetValue(num, out var value)) { throw new InvalidDataException("unknown opcode 0x" + num.ToString("X4")); } object operand = ReadOperand(method, array, value, ref position); list.Add(new DecodedInstruction { Offset = offset, OpCode = value, Operand = operand }); } } 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 object ReadOperand(MethodInfo method, byte[] il, OpCode opcode, ref int position) { object obj = null; switch (opcode.OperandType) { case OperandType.ShortInlineI: obj = (sbyte)il[position++]; break; case OperandType.InlineI: obj = BitConverter.ToInt32(il, position); position += 4; break; case OperandType.InlineI8: obj = BitConverter.ToInt64(il, position); position += 8; break; case OperandType.ShortInlineR: obj = BitConverter.ToSingle(il, position); position += 4; break; case OperandType.InlineR: obj = BitConverter.ToDouble(il, position); position += 8; break; case OperandType.ShortInlineVar: obj = il[position++]; break; case OperandType.InlineVar: obj = BitConverter.ToUInt16(il, position); position += 2; break; case OperandType.ShortInlineBrTarget: obj = position + 1 + (sbyte)il[position]; position++; break; case OperandType.InlineBrTarget: { int num3 = BitConverter.ToInt32(il, position); position += 4; obj = position + num3; break; } case OperandType.InlineSwitch: { int num = BitConverter.ToInt32(il, position); position += 4; int num2 = position + 4 * num; int[] array = new int[num]; for (int i = 0; i < num; i++) { array[i] = num2 + BitConverter.ToInt32(il, position); position += 4; } obj = array; break; } case OperandType.InlineString: { int metadataToken5 = BitConverter.ToInt32(il, position); position += 4; obj = method.Module.ResolveString(metadataToken5); if (obj == null) { throw new InvalidDataException("unresolved string token"); } break; } case OperandType.InlineField: { int metadataToken4 = BitConverter.ToInt32(il, position); position += 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(il, position); position += 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(il, position); position += 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(il, position); position += 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 " + opcode.OperandType); case OperandType.InlineNone: break; } return obj; } 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; } internal 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; } internal 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; } } internal static class MethodFingerprint { internal static bool TryValidate(MethodInfo method, int expectedCodeSize, string expectedHash, string displayName, out List<DecodedInstruction> instructions, out string reason) { reason = null; if (!IlMethodReader.TryRead(method, expectedCodeSize, displayName, out instructions, out reason)) { return false; } if (!CanonicalMethodFormatter.TryBuildCanonicalMethod(method, expectedCodeSize, displayName, instructions, out var canonical, out reason)) { return false; } string text = Sha256Hash.Compute(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; } internal static bool TryValidateRaw(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 = Sha256Hash.Compute(array); if (!string.Equals(text, expected, StringComparison.OrdinalIgnoreCase)) { reason = displayName + " IL SHA-256 mismatch. Expected " + expected + ", got " + text + "."; return false; } return true; } } }