diff --git a/Harmony/Harmony.csproj b/Harmony/Harmony.csproj index 00d4f1b9..0afca2bb 100644 --- a/Harmony/Harmony.csproj +++ b/Harmony/Harmony.csproj @@ -23,25 +23,25 @@ $(DefaultItemExcludes);Documentation/** true false - $(HarmonyXVersion) - $(HarmonyXVersionFull) - $(HarmonyXVersionFull) - $(HarmonyXVersion)$(HarmonyXVersionSuffix) - $(HarmonyXVersion)$(HarmonyXVersionSuffix) + $(HarmonyXVersion) + $(HarmonyXVersionFull) + $(HarmonyXVersionFull) + $(HarmonyXVersion)$(HarmonyXVersionSuffix) + $(HarmonyXVersion)$(HarmonyXVersionSuffix) $(NoWarn);SYSLIB0011;NU5131 - Debug;Release - true - true - $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb - True + Debug;Release + true + true + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + True HarmonyLib - + - + false @@ -55,22 +55,22 @@ portable true - + - + - - + + - - + + - + @@ -85,23 +85,23 @@ - - - - - - + + + + + + - - - - - - - - + + + + + + + + - - - + + + diff --git a/Harmony/Internal/PatchModels.cs b/Harmony/Internal/PatchModels.cs index b24042e4..c9bbc0db 100644 --- a/Harmony/Internal/PatchModels.cs +++ b/Harmony/Internal/PatchModels.cs @@ -86,7 +86,6 @@ internal class AttributePatch internal HarmonyMethod info; internal HarmonyPatchType? type; - static readonly string harmonyAttributeName = typeof(HarmonyAttribute).FullName; internal static IEnumerable Create(MethodInfo patch, bool collectIncomplete = false) { if (patch is null) @@ -108,7 +107,7 @@ internal static IEnumerable Create(MethodInfo patch, bool collec var f_info = AccessTools.Field(attr.GetType(), nameof(HarmonyAttribute.info)); return f_info.GetValue(attr); }) - .Select(harmonyInfo => AccessTools.MakeDeepCopy(harmonyInfo)) + .Select(AccessTools.MakeDeepCopy) .ToList(); var completeMethods = new List(); diff --git a/Harmony/Internal/Util/StackTraceFixes.cs b/Harmony/Internal/Util/StackTraceFixes.cs index 68b1d560..e83608f9 100644 --- a/Harmony/Internal/Util/StackTraceFixes.cs +++ b/Harmony/Internal/Util/StackTraceFixes.cs @@ -6,9 +6,7 @@ using System; using System.Diagnostics; using System.Reflection; -using MonoMod.Core.Platforms; using MonoMod.RuntimeDetour; -using MonoMod.Utils; using System.Linq; namespace HarmonyLib.Internal.RuntimeFixes @@ -32,28 +30,19 @@ public static void Install() } // Helper to save the detour info after patch is complete - private static void OnILChainRefresh(ILHookInfo self) - { - PatchManager.AddReplacementOriginal( - PlatformTriple.Current.GetIdentifiable(self.Method.Method), - PlatformTriple.Current.GetIdentifiable(self.Method.GetEndOfChain()) - ); - } + private static void OnILChainRefresh(ILHookInfo self) => + PatchManager.AddReplacementOriginal(self.Method.Method, self.Method.GetEndOfChain()); static Assembly GetExecutingAssemblyReplacement() { var frames = new StackTrace().GetFrames(); - if (frames?.Skip(1).FirstOrDefault() is { } frame && Harmony.GetOriginalMethodFromStackframe(frame) is { } original) + if (frames?.Skip(1).FirstOrDefault() is { } frame && Harmony.GetMethodFromStackframe(frame) is { } original) return original.Module.Assembly; return Assembly.GetExecutingAssembly(); } - private static MethodBase GetMethodReplacement(StackFrame self) - { - var method = self.GetMethod(); - var original = PatchManager.GetOriginal(PlatformTriple.Current.GetIdentifiable(method) as MethodInfo); - return original ?? method; - } + private static MethodBase GetMethodReplacement(StackFrame self) => + Harmony.GetMethodFromStackframe(self) ?? self.GetMethod(); // ReSharper disable InconsistentNaming private static readonly MethodInfo GetExecutingAssembly_MethodInfo = diff --git a/Harmony/Public/Harmony.cs b/Harmony/Public/Harmony.cs index ac757be1..72166ab2 100644 --- a/Harmony/Public/Harmony.cs +++ b/Harmony/Public/Harmony.cs @@ -156,7 +156,7 @@ public void PatchAllUncategorized(Assembly assembly) patchClasses.DoIf((patchClass => string.IsNullOrEmpty(patchClass.Category)), (patchClass => patchClass.Patch())); } - /// Searches an assembly for Harmony annotations with a specific category and uses them to create patches + /// Searches the current assembly for Harmony annotations with a specific category and uses them to create patches /// Name of patch category /// public void PatchCategory(string category) @@ -328,6 +328,32 @@ public void Unpatch(MethodBase original, MethodInfo patch) _ = processor.Unpatch(patch); } + /// Searches the current assembly for types with a specific category annotation and uses them to unpatch existing patches. Fully unpatching is not supported. Be careful, unpatching is global + /// Name of patch category + /// + public void UnpatchCategory(string category) + { + var method = new StackTrace().GetFrame(1).GetMethod(); + var assembly = method.ReflectedType.Assembly; + UnpatchCategory(assembly, category); + } + + /// Searches an assembly for types with a specific category annotation and uses them to unpatch existing patches. Fully unpatching is not supported. Be careful, unpatching is global + /// The assembly + /// Name of patch category + /// + public void UnpatchCategory(Assembly assembly, string category) + { + AccessTools.GetTypesFromAssembly(assembly) + .Where(type => + { + var harmonyAttributes = HarmonyMethodExtensions.GetFromType(type); + var containerAttributes = HarmonyMethod.Merge(harmonyAttributes); + return containerAttributes.category == category; + }) + .Do(type => CreateClassProcessor(type).Unpatch()); + } + /// Test for patches from a specific Harmony ID /// The Harmony ID /// True if patches for this ID exist @@ -335,7 +361,7 @@ public void Unpatch(MethodBase original, MethodInfo patch) public static bool HasAnyPatches(string harmonyID) { return GetAllPatchedMethods() - .Select(original => GetPatchInfo(original)) + .Select(GetPatchInfo) .Any(info => info.Owners.Contains(harmonyID)); } @@ -360,13 +386,13 @@ public IEnumerable GetPatchedMethods() public static IEnumerable GetAllPatchedMethods() => PatchProcessor.GetAllPatchedMethods(); /// Gets the original method from a given replacement method - /// A replacement method, for example from a stacktrace + /// A replacement method (patched original method) /// The original method/constructor or null if not found /// public static MethodBase GetOriginalMethod(MethodInfo replacement) { if (replacement == null) throw new ArgumentNullException(nameof(replacement)); - return PatchManager.GetOriginal(replacement); + return PatchManager.GetRealMethod(replacement, useReplacement: false); } /// Tries to get the method from a stackframe including dynamic replacement methods @@ -376,7 +402,7 @@ public static MethodBase GetOriginalMethod(MethodInfo replacement) public static MethodBase GetMethodFromStackframe(StackFrame frame) { if (frame == null) throw new ArgumentNullException(nameof(frame)); - return PatchManager.FindReplacement(frame) ?? frame.GetMethod(); + return PatchManager.GetStackFrameMethod(frame, useReplacement: true); } /// Gets the original method from the stackframe and uses original if method is a dynamic replacement @@ -384,17 +410,16 @@ public static MethodBase GetMethodFromStackframe(StackFrame frame) /// The original method from that stackframe public static MethodBase GetOriginalMethodFromStackframe(StackFrame frame) { - var member = GetMethodFromStackframe(frame); - if (member is MethodInfo methodInfo) - member = GetOriginalMethod(methodInfo) ?? member; - return member; + if (frame == null) throw new ArgumentNullException(nameof(frame)); + return PatchManager.GetStackFrameMethod(frame, useReplacement: false); } /// Gets Harmony version for all active Harmony instances /// [out] The current Harmony version /// A dictionary containing assembly versions keyed by Harmony IDs /// - public static Dictionary VersionInfo(out Version currentVersion) => PatchProcessor.VersionInfo(out currentVersion); + public static Dictionary VersionInfo(out Version currentVersion) + => PatchProcessor.VersionInfo(out currentVersion); private static int _autoGuidCounter = 100; diff --git a/Harmony/Public/HarmonyMethod.cs b/Harmony/Public/HarmonyMethod.cs index 1e0d5d84..f2422625 100644 --- a/Harmony/Public/HarmonyMethod.cs +++ b/Harmony/Public/HarmonyMethod.cs @@ -288,11 +288,23 @@ public static HarmonyMethod Merge(this HarmonyMethod master, HarmonyMethod detai { var baseValue = masterTrv.Field(f).GetValue(); var detailValue = detailTrv.Field(f).GetValue(); - // This if is needed because priority defaults to -1 - // This causes the value of a HarmonyPriority attribute to be overriden by the next attribute if it is not merged last - // should be removed by making priority nullable and default to null at some point - if (f != nameof(HarmonyMethod.priority) || (int)detailValue != -1) + if (f != nameof(HarmonyMethod.priority)) SetValue(resultTrv, f, detailValue ?? baseValue); + else + { + // This if is needed because priority defaults to -1 + // This causes the value of a HarmonyPriority attribute to be overriden by the next attribute if it is not merged last + // should be removed by making priority nullable and default to null at some point + + var baseInt = (int)baseValue; + var detailInt = (int)detailValue; + var priority = Math.Max(baseInt, detailInt); + if (baseInt == -1 && detailInt != -1) + priority = detailInt; + if (baseInt != -1 && detailInt == -1) + priority = baseInt; + SetValue(resultTrv, f, priority); + } }); return result; } @@ -313,7 +325,7 @@ static HarmonyMethod GetHarmonyMethodInfo(object attribute) public static List GetFromType(Type type) { return type.GetCustomAttributes(true) - .Select(attr => GetHarmonyMethodInfo(attr)) + .Select(GetHarmonyMethodInfo) .Where(info => info is not null) .ToList(); } @@ -331,7 +343,7 @@ public static List GetFromType(Type type) public static List GetFromMethod(MethodBase method) { return method.GetCustomAttributes(true) - .Select(attr => GetHarmonyMethodInfo(attr)) + .Select(GetHarmonyMethodInfo) .Where(info => info is not null) .ToList(); } diff --git a/Harmony/Public/Patch.cs b/Harmony/Public/Patch.cs index 4a10133f..415619e3 100644 --- a/Harmony/Public/Patch.cs +++ b/Harmony/Public/Patch.cs @@ -113,8 +113,8 @@ internal static PatchInfo Deserialize(byte[] bytes) internal static int PriorityComparer(object obj, int index, int priority) { var trv = Traverse.Create(obj); - var theirPriority = trv.Field("priority").GetValue(); - var theirIndex = trv.Field("index").GetValue(); + var theirPriority = trv.Field(nameof(Patch.priority)).GetValue(); + var theirIndex = trv.Field(nameof(Patch.index)).GetValue(); if (priority != theirPriority) return -(priority.CompareTo(theirPriority)); diff --git a/Harmony/Public/PatchClassProcessor.cs b/Harmony/Public/PatchClassProcessor.cs index 96a481b0..43c90d96 100644 --- a/Harmony/Public/PatchClassProcessor.cs +++ b/Harmony/Public/PatchClassProcessor.cs @@ -107,7 +107,7 @@ public List Patch() lastOriginal = originals[0]; ReversePatch(ref lastOriginal); - replacements = originals.Count > 0 ? BulkPatch(originals, ref lastOriginal) : PatchWithAttributes(ref lastOriginal); + replacements = originals.Count > 0 ? BulkPatch(originals, ref lastOriginal, false) : PatchWithAttributes(ref lastOriginal, false); } catch (Exception ex) { @@ -119,6 +119,21 @@ public List Patch() return replacements; } + /// REmoves the patches + /// + public void Unpatch() + { + if (containerAttributes is null) + return; + + var originals = GetBulkMethods(); + MethodBase lastOriginal = null; + if (originals.Count > 0) + _ = BulkPatch(originals, ref lastOriginal, true); + else + _ = PatchWithAttributes(ref lastOriginal, true); + } + void ReversePatch(ref MethodBase lastOriginal) { for (var i = 0; i < patchMethods.Count; i++) @@ -148,7 +163,7 @@ void ReversePatch(ref MethodBase lastOriginal) } } - List BulkPatch(List originals, ref MethodBase lastOriginal) + List BulkPatch(List originals, ref MethodBase lastOriginal, bool unpatch) { var jobs = new PatchJobs(); for (var i = 0; i < originals.Count; i++) @@ -172,12 +187,15 @@ List BulkPatch(List originals, ref MethodBase lastOrigin foreach (var job in jobs.GetJobs()) { lastOriginal = job.original; - ProcessPatchJob(job); + if (unpatch) + ProcessUnpatchJob(job); + else + ProcessPatchJob(job); } return jobs.GetReplacements(); } - List PatchWithAttributes(ref MethodBase lastOriginal) + List PatchWithAttributes(ref MethodBase lastOriginal, bool unpatch) { var jobs = new PatchJobs(); foreach (var patchMethod in patchMethods) @@ -200,7 +218,10 @@ List PatchWithAttributes(ref MethodBase lastOriginal) foreach (var job in jobs.GetJobs()) { lastOriginal = job.original; - ProcessPatchJob(job); + if (unpatch) + ProcessUnpatchJob(job); + else + ProcessPatchJob(job); } return jobs.GetReplacements(); } @@ -239,6 +260,24 @@ void ProcessPatchJob(PatchJobs.Job job) job.replacement = replacement; } + void ProcessUnpatchJob(PatchJobs.Job job) + { + var patchInfo = job.original.GetPatchInfo() ?? new PatchInfo(); + + var hasBody = job.original.HasMethodBody(); + if (hasBody) + { + job.postfixes.Do(patch => patchInfo.RemovePatch(patch.method)); + job.prefixes.Do(patch => patchInfo.RemovePatch(patch.method)); + } + job.transpilers.Do(patch => patchInfo.RemovePatch(patch.method)); + if (hasBody) + job.finalizers.Do(patch => patchInfo.RemovePatch(patch.method)); + + var replacement = PatchFunctions.UpdateWrapper(job.original, patchInfo); + PatchManager.AddReplacementOriginal(job.original, replacement); + } + List GetBulkMethods() { var isPatchAll = containerType.GetCustomAttributes(true).Any(a => a.GetType().FullName == PatchTools.harmonyPatchAllFullName); diff --git a/Harmony/Public/Patching/PatchManager.cs b/Harmony/Public/Patching/PatchManager.cs index afaf7cb4..4d0dae6e 100644 --- a/Harmony/Public/Patching/PatchManager.cs +++ b/Harmony/Public/Patching/PatchManager.cs @@ -1,9 +1,9 @@ -using MonoMod.Core.Platforms; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; namespace HarmonyLib.Public.Patching { @@ -14,16 +14,20 @@ namespace HarmonyLib.Public.Patching /// public static class PatchManager { - private static readonly Dictionary PatchInfos = new Dictionary(); - private static readonly Dictionary MethodPatchers = new Dictionary(); - // Keep replacements as weak references to allow GC to collect them (e.g. if replacement is DynamicMethod) - private static readonly List> ReplacementToOriginals = new List>(); + private static readonly Dictionary PatchInfos = new(); + private static readonly Dictionary MethodPatchers = new(); - // typeof(StackFrame).methodAddress - private static FieldInfo methodAddress; + private static readonly ConditionalWeakTable ReplacementToOriginals = new(); + private static readonly Dictionary ReplacementToOriginalsMono = new(); + + private static readonly AccessTools.FieldRef MethodAddressRef; static PatchManager() { + // this field is used to find methods from stackframes in Mono + if (AccessTools.IsMonoRuntime && AccessTools.Field(typeof(StackFrame), "methodAddress") is FieldInfo field) + MethodAddressRef = AccessTools.FieldRefAccess(field); + ResolvePatcher += ManagedMethodPatcher.TryResolve; ResolvePatcher += NativeDetourMethodPatcher.TryResolve; } @@ -98,45 +102,66 @@ public static PatchInfo ToPatchInfo(this MethodBase methodBase) public static IEnumerable GetPatchedMethods() { lock (PatchInfos) - return PatchInfos.Keys.ToList(); + return PatchInfos.Keys.ToArray(); } - internal static MethodBase GetOriginal(MethodInfo replacement) + // With mono, useReplacement is used to either return the original or the replacement + // On .NET, useReplacement is ignored and the original is always returned + internal static MethodBase GetRealMethod(MethodInfo method, bool useReplacement) { - if (replacement is null) - return null; - - // The runtime can return several different MethodInfo's that point to the same method. Use the correct one - replacement = PlatformTriple.Current.GetIdentifiable(replacement) as MethodInfo; - if (replacement is null) - return null; - + var identifiableMethod = method.Identifiable(); lock (ReplacementToOriginals) + if (ReplacementToOriginals.TryGetValue(identifiableMethod, out var original)) + return original; + + if (AccessTools.IsMonoRuntime) { - ReplacementToOriginals.RemoveAll(kv => !kv.Key.IsAlive); - foreach (var replacementToOriginal in ReplacementToOriginals) - { - var method = replacementToOriginal.Key.Target as MethodInfo; - if (method == replacement) - return replacementToOriginal.Value; - } - return null; + var methodAddress = (long)method.MethodHandle.GetFunctionPointer(); + lock (ReplacementToOriginalsMono) + if (ReplacementToOriginalsMono.TryGetValue(methodAddress, out var info)) + return useReplacement ? info[1] : info[0]; } + + return method; } - internal static MethodBase FindReplacement(StackFrame frame) + internal static MethodBase GetStackFrameMethod(StackFrame frame, bool useReplacement) { - var method = frame.GetMethod() as MethodInfo; - if (method == null) return null; - return GetOriginal(method); + if (frame.GetMethod() is MethodInfo method) + return GetRealMethod(method, useReplacement); + + if (MethodAddressRef != null) + { + var methodAddress = MethodAddressRef(frame); + lock (ReplacementToOriginalsMono) + if (ReplacementToOriginalsMono.TryGetValue(methodAddress, out var info)) + return useReplacement ? info[1] : info[0]; + } + + return null; } internal static void AddReplacementOriginal(MethodBase original, MethodBase replacement) { + replacement = (replacement as MethodInfo)?.Identifiable() ?? replacement; if (replacement == null) return; + lock (ReplacementToOriginals) - ReplacementToOriginals.Add(new KeyValuePair(new WeakReference(replacement), original)); + { + if(!ReplacementToOriginals.TryAdd(replacement, original)) + { + ReplacementToOriginals.Remove(replacement); + ReplacementToOriginals.Add(replacement, original); + } + } + + if (AccessTools.IsMonoRuntime) + { + var methodAddress = (long)replacement.MethodHandle.GetFunctionPointer(); + lock (ReplacementToOriginalsMono) + ReplacementToOriginalsMono[methodAddress] = [original, replacement]; + } } /// diff --git a/Harmony/Tools/AccessTools.cs b/Harmony/Tools/AccessTools.cs index f8fdbf6d..f010ebd3 100644 --- a/Harmony/Tools/AccessTools.cs +++ b/Harmony/Tools/AccessTools.cs @@ -1,3 +1,4 @@ +using MonoMod.Core.Platforms; using HarmonyLib.Internal.Util; using MonoMod.Utils; using System; @@ -32,7 +33,7 @@ public static class AccessTools { /// Shortcut for to simplify the use of reflections and make it work for any access level /// - public static readonly BindingFlags all = BindingFlags.Public // This should a be const, but changing from static (readonly) to const breaks binary compatibility. + public static readonly BindingFlags all = BindingFlags.Public // This should be a const, but changing from static (readonly) to const breaks binary compatibility. | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static @@ -43,7 +44,7 @@ public static class AccessTools /// Shortcut for to simplify the use of reflections and make it work for any access level but only within the current type /// - public static readonly BindingFlags allDeclared = all | BindingFlags.DeclaredOnly; // This should a be const, but changing from static (readonly) to const breaks binary compatibility. + public static readonly BindingFlags allDeclared = all | BindingFlags.DeclaredOnly; // This should be a const, but changing from static (readonly) to const breaks binary compatibility. /// Enumerates all assemblies in the current app domain, excluding visual studio assemblies /// An enumeration of @@ -97,7 +98,7 @@ public static Type[] GetTypesFromAssembly(Assembly assembly) /// Enumerates all successfully loaded types in the current app domain, excluding visual studio assemblies /// An enumeration of all in all assemblies, excluding visual studio assemblies /// - public static IEnumerable AllTypes() => AllAssemblies().SelectMany(a => GetTypesFromAssembly(a)); + public static IEnumerable AllTypes() => AllAssemblies().SelectMany(GetTypesFromAssembly); /// Enumerates all inner types (non-recursive) of a given type /// The class/type to start with @@ -146,6 +147,11 @@ public static T FindIncludingInnerTypes(Type type, Func func) where return result; } + /// Creates an identifiable version of a method + /// The method + /// + public static MethodInfo Identifiable(this MethodInfo method) => PlatformTriple.Current.GetIdentifiable(method) as MethodInfo ?? method; + /// Gets the reflection information for a directly declared field /// The class/type where the field is defined /// The name of the field diff --git a/HarmonyTests/Extras/RetrieveOriginalMethod.cs b/HarmonyTests/Extras/RetrieveOriginalMethod.cs index 6206b5fe..5554b4a3 100644 --- a/HarmonyTests/Extras/RetrieveOriginalMethod.cs +++ b/HarmonyTests/Extras/RetrieveOriginalMethod.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; using System.Reflection; +using System.Runtime.CompilerServices; namespace HarmonyLibTests.Extras { @@ -14,20 +15,20 @@ private static void CheckStackTraceFor(MethodBase expectedMethod) Assert.NotNull(expectedMethod); var st = new StackTrace(1, false); - var method = Harmony.GetMethodFromStackframe(st.GetFrame(0)); + var frame = st.GetFrame(0); + Assert.NotNull(frame); - Assert.NotNull(method); + var methodFromStackframe = Harmony.GetMethodFromStackframe(frame); + Assert.NotNull(methodFromStackframe); + Assert.AreEqual(expectedMethod, methodFromStackframe); - if (method is MethodInfo replacement) - { - var original = Harmony.GetOriginalMethod(replacement); - Assert.NotNull(original); - Assert.AreEqual(original, expectedMethod); - } + var replacement = frame.GetMethod() as MethodInfo; + Assert.NotNull(replacement); + var original = Harmony.GetOriginalMethod(replacement); + Assert.NotNull(original); + Assert.AreEqual(expectedMethod, original); } - /* TODO - * [Test] public void TestRegularMethod() { @@ -48,7 +49,6 @@ public void TestConstructor() var inst = new NestedClass(5); _ = inst.index; } - */ internal static void PatchTarget() { @@ -60,7 +60,7 @@ internal static void PatchTarget() } } - // [MethodImpl(MethodImplOptions.NoInlining)] + [MethodImpl(MethodImplOptions.NoInlining)] internal static void DummyPrefix() { } diff --git a/HarmonyTests/Patching/FinalizerPatches.cs b/HarmonyTests/Patching/FinalizerPatches.cs index 16f9c12a..4c57b155 100644 --- a/HarmonyTests/Patching/FinalizerPatches.cs +++ b/HarmonyTests/Patching/FinalizerPatches.cs @@ -1,14 +1,72 @@ -using HarmonyLib; +using HarmonyLib; using HarmonyLibTests.Assets; using NUnit.Framework; using System; using System.Collections.Generic; using System.Reflection; +using System.Text; namespace HarmonyLibTests.Patching { [TestFixture, NonParallelizable] - public class FinalizerPatches : TestLogger + public class FinalizerPatches1 : TestLogger + { + static StringBuilder progress = new(); + + [Test] + public void Test_FinalizerPatchOrder() + { + var harmony = new Harmony("test"); + harmony.PatchCategory("finalizer-test"); + try + { + _ = progress.Clear(); + Class.Test(); + Assert.Fail("Should throw an exception"); + } + catch (Exception ex) + { + var result = progress.Append($"-> {ex?.Message ?? "-"}").ToString(); + Assert.AreEqual("Finalizer 2 E0 -> E-2\nFinalizer 1 E-2 -> E-1\n-> E-1", result); + } + } + + [HarmonyPatch(typeof(Class), nameof(Class.Test))] + [HarmonyPatchCategory("finalizer-test")] + [HarmonyPriority(Priority.Low)] + static class Patch1 + { + static Exception Finalizer(Exception __exception) + { + _ = progress.Append($"Finalizer 1 {__exception?.Message ?? "-"} -> E-1\n"); + return new Exception("E-1"); + } + } + + [HarmonyPatch(typeof(Class), nameof(Class.Test))] + [HarmonyPatchCategory("finalizer-test")] + [HarmonyPriority(Priority.High)] + static class Patch2 + { + static Exception Finalizer(Exception __exception) + { + _ = progress.Append($"Finalizer 2 {__exception?.Message ?? "-"} -> E-2\n"); + return new Exception("E-2"); + } + } + } + + static class Class + { + public static void Test() + { + Console.WriteLine("Test"); + throw new Exception("E0"); + } + } + + [TestFixture, NonParallelizable] + public class FinalizerPatches2 : TestLogger { static Dictionary info; diff --git a/HarmonyTests/TestTools.cs b/HarmonyTests/TestTools.cs index 289a1d1d..a780e843 100644 --- a/HarmonyTests/TestTools.cs +++ b/HarmonyTests/TestTools.cs @@ -185,7 +185,6 @@ public static void RunInIsolationContext(Action action) = TestDomainProxy.RunInIsolationContext(action); #endif - #if NETCOREAPP // .NET Core does not support multiple AppDomains, but it does support unloading assemblies via AssemblyLoadContext. // Based off sample code in https://docs.microsoft.com/en-us/dotnet/standard/assembly/unloadability diff --git a/HarmonyTests/Tools/Assets/AttributesClass.cs b/HarmonyTests/Tools/Assets/AttributesClass.cs index 00c6dd46..d85457a7 100644 --- a/HarmonyTests/Tools/Assets/AttributesClass.cs +++ b/HarmonyTests/Tools/Assets/AttributesClass.cs @@ -13,24 +13,71 @@ public void Method1() { } [HarmonyPatch(typeof(string))] [HarmonyPatch("foobar")] - [HarmonyPriority(Priority.High)] + [HarmonyPriority(Priority.HigherThanNormal)] [HarmonyPatch([typeof(float), typeof(string)])] public class AllAttributesClass { [HarmonyPrepare] - public void Method1() { } - - [HarmonyTargetMethod] - public void Method2() { } + public static bool Method1() => true; [HarmonyPrefix] [HarmonyPriority(Priority.High)] - public void Method3() { } + public static void Method2() { } [HarmonyPostfix] [HarmonyBefore("foo", "bar")] [HarmonyAfter("test")] - public void Method4() { } + public static void Method3() { } + + [HarmonyFinalizer] + [HarmonyPriority(Priority.Low)] + public static void Method4() { } + } + + public class AllAttributesClassMethodsInstance + { + public static void Test() + { + } + } + + [HarmonyPatch(typeof(AllAttributesClassMethodsInstance), "Test")] + [HarmonyPriority(Priority.HigherThanNormal)] + public class AllAttributesClassMethods + { + [HarmonyPrepare] + public static bool Method1() => true; + + [HarmonyCleanup] + public static void Method2() { } + + [HarmonyPrefix] + [HarmonyPriority(Priority.Low)] + public static void Method3Low() { } + + [HarmonyPrefix] + [HarmonyPriority(Priority.High)] + public static void Method3High() { } + + [HarmonyPostfix] + [HarmonyBefore("xfoo", "xbar")] + [HarmonyAfter("xtest")] + [HarmonyPriority(Priority.High)] + public static void Method4High() { } + + [HarmonyPostfix] + [HarmonyBefore("xfoo", "xbar")] + [HarmonyAfter("xtest")] + [HarmonyPriority(Priority.Low)] + public static void Method4Low() { } + + [HarmonyFinalizer] + [HarmonyPriority(Priority.Low)] + public static void Method5Low() { } + + [HarmonyFinalizer] + [HarmonyPriority(Priority.High)] + public static void Method5High() { } } public class NoAnnotationsClass diff --git a/HarmonyTests/Tools/TestAttributes.cs b/HarmonyTests/Tools/TestAttributes.cs index 5f4a276f..97786511 100644 --- a/HarmonyTests/Tools/TestAttributes.cs +++ b/HarmonyTests/Tools/TestAttributes.cs @@ -1,4 +1,4 @@ -using HarmonyLib; +using HarmonyLib; using HarmonyLibTests.Assets; using NUnit.Framework; @@ -20,7 +20,36 @@ public void Test_SimpleAttributes() Assert.AreEqual(2, info.argumentTypes.Length); Assert.AreEqual(typeof(float), info.argumentTypes[0]); Assert.AreEqual(typeof(string), info.argumentTypes[1]); - Assert.AreEqual(Priority.High, info.priority); + Assert.AreEqual(Priority.HigherThanNormal, info.priority); + } + + [Test] + public void Test_CombiningAttributesOnMultipleMethods() + { + var harmony = new Harmony("test"); + var processor = new PatchClassProcessor(harmony, typeof(AllAttributesClassMethods)); + var replacements = processor.Patch(); + Assert.NotNull(replacements, "patches"); + Assert.AreEqual(1, replacements.Count); + + var method = typeof(AllAttributesClassMethodsInstance).GetMethod("Test"); + var patches = Harmony.GetPatchInfo(method); + var prefixes = PatchFunctions.GetSortedPatchMethods(method, [.. patches.Prefixes], false); + var postfixes = PatchFunctions.GetSortedPatchMethods(method, [.. patches.Postfixes], false); + var finalizers = PatchFunctions.GetSortedPatchMethods(method, [.. patches.Finalizers], false); + + Assert.AreEqual(2, prefixes.Count); + Assert.AreEqual(2, postfixes.Count); + Assert.AreEqual(2, finalizers.Count); + + Assert.AreEqual(nameof(AllAttributesClassMethods.Method3High), prefixes[0].Name); + Assert.AreEqual(nameof(AllAttributesClassMethods.Method3Low), prefixes[1].Name); + + Assert.AreEqual(nameof(AllAttributesClassMethods.Method4High), postfixes[0].Name); + Assert.AreEqual(nameof(AllAttributesClassMethods.Method4Low), postfixes[1].Name); + + Assert.AreEqual(nameof(AllAttributesClassMethods.Method5High), finalizers[0].Name); + Assert.AreEqual(nameof(AllAttributesClassMethods.Method5Low), finalizers[1].Name); } [Test]