Skip to content

Commit

Permalink
adds UnpatchCategory, fixes HarmonyPriority bug with inherited values
Browse files Browse the repository at this point in the history
  • Loading branch information
pardeike committed Mar 29, 2024
1 parent f0a4f51 commit 395749f
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 25 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>

<PropertyGroup>
<HarmonyVersion>2.3.2.0</HarmonyVersion>
<HarmonyVersion>2.3.3.0</HarmonyVersion>
<HarmonyPrerelease></HarmonyPrerelease>
<MonoModCoreVersion>1.1.0</MonoModCoreVersion>
</PropertyGroup>
Expand Down
28 changes: 27 additions & 1 deletion Harmony/Public/Harmony.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ public void PatchAllUncategorized(Assembly assembly)
patchClasses.DoIf((patchClass => string.IsNullOrEmpty(patchClass.Category)), (patchClass => patchClass.Patch()));
}

/// <summary>Searches an assembly for Harmony annotations with a specific category and uses them to create patches</summary>
/// <summary>Searches the current assembly for Harmony annotations with a specific category and uses them to create patches</summary>
/// <param name="category">Name of patch category</param>
///
public void PatchCategory(string category)
Expand Down Expand Up @@ -219,6 +219,32 @@ public void Unpatch(MethodBase original, MethodInfo patch)
_ = processor.Unpatch(patch);
}

/// <summary>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</summary>
/// <param name="category">Name of patch category</param>
///
public void UnpatchCategory(string category)
{
var method = new StackTrace().GetFrame(1).GetMethod();
var assembly = method.ReflectedType.Assembly;
UnpatchCategory(assembly, category);
}

/// <summary>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</summary>
/// <param name="assembly">The assembly</param>
/// <param name="category">Name of patch category</param>
///
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());
}

/// <summary>Test for patches from a specific Harmony ID</summary>
/// <param name="harmonyID">The Harmony ID</param>
/// <returns>True if patches for this ID exist</returns>
Expand Down
20 changes: 16 additions & 4 deletions Harmony/Public/HarmonyMethod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
4 changes: 2 additions & 2 deletions Harmony/Public/Patch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>();
var theirIndex = trv.Field("index").GetValue<int>();
var theirPriority = trv.Field(nameof(Patch.priority)).GetValue<int>();
var theirIndex = trv.Field(nameof(Patch.index)).GetValue<int>();

if (priority != theirPriority)
return -(priority.CompareTo(theirPriority));
Expand Down
51 changes: 45 additions & 6 deletions Harmony/Public/PatchClassProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public PatchClassProcessor(Harmony instance, Type type)

containerAttributes = HarmonyMethod.Merge(harmonyAttributes);
containerAttributes.methodType ??= MethodType.Normal;

Category = containerAttributes.category;

auxilaryMethods = [];
Expand Down Expand Up @@ -96,7 +96,7 @@ public List<MethodInfo> 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)
{
Expand All @@ -108,6 +108,21 @@ public List<MethodInfo> Patch()
return replacements;
}

/// <summary>REmoves the patches</summary>
///
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++)
Expand All @@ -125,7 +140,7 @@ void ReversePatch(ref MethodBase lastOriginal)
}
}

List<MethodInfo> BulkPatch(List<MethodBase> originals, ref MethodBase lastOriginal)
List<MethodInfo> BulkPatch(List<MethodBase> originals, ref MethodBase lastOriginal, bool unpatch)
{
var jobs = new PatchJobs<MethodInfo>();
for (var i = 0; i < originals.Count; i++)
Expand All @@ -149,12 +164,15 @@ List<MethodInfo> BulkPatch(List<MethodBase> 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<MethodInfo> PatchWithAttributes(ref MethodBase lastOriginal)
List<MethodInfo> PatchWithAttributes(ref MethodBase lastOriginal, bool unpatch)
{
var jobs = new PatchJobs<MethodInfo>();
foreach (var patchMethod in patchMethods)
Expand All @@ -169,7 +187,10 @@ List<MethodInfo> 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();
}
Expand Down Expand Up @@ -207,6 +228,24 @@ void ProcessPatchJob(PatchJobs<MethodInfo>.Job job)
job.replacement = replacement;
}

void ProcessUnpatchJob(PatchJobs<MethodInfo>.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<MethodBase> GetBulkMethods()
{
var isPatchAll = containerType.GetCustomAttributes(true).Any(a => a.GetType().FullName == PatchTools.harmonyPatchAllFullName);
Expand Down
64 changes: 62 additions & 2 deletions HarmonyTests/Patching/FinalizerPatches.cs
Original file line number Diff line number Diff line change
@@ -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<string, object> info;

Expand Down
61 changes: 54 additions & 7 deletions HarmonyTests/Tools/Assets/AttributesClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 31 additions & 2 deletions HarmonyTests/Tools/TestAttributes.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using HarmonyLib;
using HarmonyLib;
using HarmonyLibTests.Assets;
using NUnit.Framework;

Expand All @@ -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]
Expand Down

0 comments on commit 395749f

Please sign in to comment.