Skip to content

Commit

Permalink
Add HarmonyOptional attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
ManlyMarco committed Jan 23, 2024
1 parent a066adf commit baa51ff
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 2 deletions.
27 changes: 27 additions & 0 deletions Harmony/Public/Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -777,4 +777,31 @@ public HarmonyArgument(int index, string name)
NewName = name;
}
}

/// <summary> Flags used for optionally patching members that might not exist.</summary>
///
[Flags]
public enum OptionalFlags
{
/// <summary>Default behavior (throw and abort patching if there are no matches).</summary>
///
None = 0,

/// <summary> Do not throw an exception and abort the patching process if no matches are found (a warning is logged instead).</summary>
///
AllowNoMatches = 1 << 1,
}

/// <summary>Attribute used for optionally patching members that might not exist.</summary>
///
[AttributeUsage(AttributeTargets.Method)]
public class HarmonyOptional : HarmonyAttribute
{
/// <summary>Default constructor</summary>
///
public HarmonyOptional(OptionalFlags flags = OptionalFlags.AllowNoMatches)
{
info.optionalFlags = flags;
}
}
}
4 changes: 4 additions & 0 deletions Harmony/Public/HarmonyMethod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ public class HarmonyMethod
/// <summary>Whether to wrap the patch itself into a try/catch.</summary>
///
public bool? wrapTryCatch;

/// <summary>Flags used for optionally patching members that might not exist.</summary>
///
public OptionalFlags? optionalFlags;

/// <summary>Default constructor</summary>
///
Expand Down
28 changes: 27 additions & 1 deletion Harmony/Public/PatchClassProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Text;
using HarmonyLib.Public.Patching;
using HarmonyLib.Tools;
using MonoMod.Utils;

namespace HarmonyLib
{
Expand Down Expand Up @@ -59,7 +60,7 @@ public PatchClassProcessor(Harmony instance, Type type, bool allowUnannotatedTyp
containerAttributes = HarmonyMethod.Merge(harmonyAttributes);
if (containerAttributes.methodType is null) // MethodType default is Normal
containerAttributes.methodType = MethodType.Normal;

this.Category = containerAttributes.category;

auxilaryMethods = new Dictionary<Type, MethodInfo>();
Expand Down Expand Up @@ -128,8 +129,18 @@ void ReversePatch(ref MethodBase lastOriginal)
var annotatedOriginal = patchMethod.info.GetOriginalMethod();
if (annotatedOriginal is object)
lastOriginal = annotatedOriginal;

if (lastOriginal is null)
{
if (IsPatchOptional(patchMethod))
{
Logger.Log(Logger.LogChannel.Warn, () => $"Skipping optional reverse patch {patchMethod.info.method.FullDescription()} - target method not found");
continue;
}

throw new ArgumentException($"Undefined target method for reverse patch method {patchMethod.info.method.FullDescription()}");
}

var reversePatcher = instance.CreateReversePatcher(lastOriginal, patchMethod.info);
lock (PatchProcessor.locker)
_ = reversePatcher.Patch();
Expand Down Expand Up @@ -173,7 +184,15 @@ List<MethodInfo> PatchWithAttributes(ref MethodBase lastOriginal)
{
lastOriginal = patchMethod.info.GetOriginalMethod();
if (lastOriginal is null)
{
if (IsPatchOptional(patchMethod))
{
Logger.Log(Logger.LogChannel.Warn, () => $"Skipping optional patch {patchMethod.info.method.FullDescription()} - target method not found");
continue;
}

throw new ArgumentException($"Undefined target method for patch method {patchMethod.info.method.FullDescription()}");
}

var job = jobs.GetJob(lastOriginal);
job.AddPatch(patchMethod);
Expand All @@ -186,6 +205,13 @@ List<MethodInfo> PatchWithAttributes(ref MethodBase lastOriginal)
return jobs.GetReplacements();
}

static bool IsPatchOptional(AttributePatch patchMethod)
{
var optionalFlags = patchMethod.info.optionalFlags;
var isOptional = optionalFlags != null && optionalFlags.Value.Has(OptionalFlags.AllowNoMatches);
return isOptional;
}

void ProcessPatchJob(PatchJobs<MethodInfo>.Job job)
{
MethodInfo replacement = default;
Expand Down
34 changes: 34 additions & 0 deletions HarmonyTests/Patching/Assets/Specials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,40 @@ public static void ReplaceGetValue(ref bool __result)
}
}

public class OptionalPatch
{
[HarmonyPrefix, HarmonyOptional, HarmonyPatch(typeof(OptionalPatch), "missing_method")]
public static void Test0() => throw new InvalidOperationException();

[HarmonyReversePatch, HarmonyOptional, HarmonyPatch(typeof(OptionalPatch), "missing_method")]
public static void Test1() => throw new InvalidOperationException();

[HarmonyPostfix, HarmonyOptional, HarmonyPatch(typeof(OptionalPatch), MethodType.Constructor, typeof(string))]
public static void Test2() => throw new InvalidOperationException();

[HarmonyTranspiler, HarmonyOptional, HarmonyPatch(typeof(OptionalPatch), "missing_method", MethodType.Getter)]
public static void Test3() => throw new InvalidOperationException();

[HarmonyPostfix, HarmonyOptional, HarmonyPatch(typeof(OptionalPatch), nameof(NotEnumerator), MethodType.Enumerator)]
public static void Test4() => throw new InvalidOperationException();

[HarmonyPostfix, HarmonyOptional, HarmonyPatch(typeof(OptionalPatch), nameof(NotEnumerator), MethodType.Async)]
public static void Test5() => throw new InvalidOperationException();

private void NotEnumerator() => throw new InvalidOperationException();
}

public static class OptionalPatchNone
{
[HarmonyPrefix]
[HarmonyOptional(OptionalFlags.None)]
[HarmonyPatch(typeof(OptionalPatch), "missing_method")]
public static void Prefix()
{
throw new InvalidOperationException();
}
}

public static class SafeWrapPatch
{
public static bool called = false;
Expand Down
13 changes: 12 additions & 1 deletion HarmonyTests/Patching/Specials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ public void Test_HttpWebRequestGetResponse()
}
*/

[Test]
public void Test_Optional_Patch()
{
var instance = new Harmony("special-case-optional-patch");
Assert.NotNull(instance);

Assert.DoesNotThrow(() => instance.PatchAll(typeof(OptionalPatch)));

Assert.Throws<HarmonyException>(() => instance.PatchAll(typeof(OptionalPatchNone)));
}

[Test]
public void Test_Wrap_Patch()
{
Expand Down Expand Up @@ -185,7 +196,7 @@ public void Test_Enumerator_Patch()
Assert.AreEqual("MoveNext", EnumeratorPatch.patchTarget.Name);

var testObject = new EnumeratorCode();
Assert.AreEqual(new []{ 1, 2, 3, 4, 5 }, testObject.NumberEnumerator().ToArray());
Assert.AreEqual(new[] { 1, 2, 3, 4, 5 }, testObject.NumberEnumerator().ToArray());
Assert.AreEqual(6, EnumeratorPatch.runTimes);
}

Expand Down

0 comments on commit baa51ff

Please sign in to comment.