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]