From 395749ff507ab4e2cd4f84cf99085ec18f6870bc Mon Sep 17 00:00:00 2001 From: Andreas Pardeike Date: Fri, 29 Mar 2024 14:58:38 +0100 Subject: [PATCH] adds UnpatchCategory, fixes HarmonyPriority bug with inherited values --- Directory.Build.props | 2 +- Harmony/Public/Harmony.cs | 28 ++++++++- Harmony/Public/HarmonyMethod.cs | 20 ++++-- Harmony/Public/Patch.cs | 4 +- Harmony/Public/PatchClassProcessor.cs | 51 ++++++++++++++-- HarmonyTests/Patching/FinalizerPatches.cs | 64 +++++++++++++++++++- HarmonyTests/Tools/Assets/AttributesClass.cs | 61 ++++++++++++++++--- HarmonyTests/Tools/TestAttributes.cs | 33 +++++++++- 8 files changed, 238 insertions(+), 25 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 1b2611bf..c9b37181 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 2.3.2.0 + 2.3.3.0 1.1.0 diff --git a/Harmony/Public/Harmony.cs b/Harmony/Public/Harmony.cs index d87909d7..24c1f43c 100644 --- a/Harmony/Public/Harmony.cs +++ b/Harmony/Public/Harmony.cs @@ -122,7 +122,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) @@ -219,6 +219,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 diff --git a/Harmony/Public/HarmonyMethod.cs b/Harmony/Public/HarmonyMethod.cs index 379ae6db..533ff516 100644 --- a/Harmony/Public/HarmonyMethod.cs +++ b/Harmony/Public/HarmonyMethod.cs @@ -267,11 +267,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; } diff --git a/Harmony/Public/Patch.cs b/Harmony/Public/Patch.cs index cb13b923..5e57036b 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 2f92a6d9..fc2d1cfb 100644 --- a/Harmony/Public/PatchClassProcessor.cs +++ b/Harmony/Public/PatchClassProcessor.cs @@ -49,7 +49,7 @@ public PatchClassProcessor(Harmony instance, Type type) containerAttributes = HarmonyMethod.Merge(harmonyAttributes); containerAttributes.methodType ??= MethodType.Normal; - + Category = containerAttributes.category; auxilaryMethods = []; @@ -96,7 +96,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) { @@ -108,6 +108,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++) @@ -125,7 +140,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++) @@ -149,12 +164,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) @@ -169,7 +187,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(); } @@ -207,6 +228,24 @@ void ProcessPatchJob(PatchJobs.Job job) job.replacement = replacement; } + void ProcessUnpatchJob(PatchJobs.Job job) + { + var patchInfo = HarmonySharedState.GetPatchInfo(job.original) ?? 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); + HarmonySharedState.UpdatePatchInfo(job.original, replacement, patchInfo); + } + List GetBulkMethods() { var isPatchAll = containerType.GetCustomAttributes(true).Any(a => a.GetType().FullName == PatchTools.harmonyPatchAllFullName); diff --git a/HarmonyTests/Patching/FinalizerPatches.cs b/HarmonyTests/Patching/FinalizerPatches.cs index 41a9bd9c..d4276914 100644 --- a/HarmonyTests/Patching/FinalizerPatches.cs +++ b/HarmonyTests/Patching/FinalizerPatches.cs @@ -1,14 +1,74 @@ -using HarmonyLib; +using HarmonyLib; using HarmonyLibTests.Assets; using NUnit.Framework; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Reflection; +using System.Security.Cryptography.X509Certificates; +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/Tools/Assets/AttributesClass.cs b/HarmonyTests/Tools/Assets/AttributesClass.cs index 35eccf5c..af8ffc8b 100644 --- a/HarmonyTests/Tools/Assets/AttributesClass.cs +++ b/HarmonyTests/Tools/Assets/AttributesClass.cs @@ -14,24 +14,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]