Skip to content

Commit

Permalink
Add HarmonyOptional attribute (#105)
Browse files Browse the repository at this point in the history
Add a patch attribute that marks the patch as optional - if the method is not found or patching fails, the patch process is not aborted and there's a warning given, not an error.

Resolves #47
  • Loading branch information
ManlyMarco authored Feb 8, 2024
1 parent 91d20d7 commit 0e333a7
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 6 deletions.
8 changes: 4 additions & 4 deletions Harmony/Internal/PatchTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,13 @@ internal static MethodBase GetOriginalMethod(this HarmonyMethod attr)

case MethodType.Getter:
if (attr.methodName is null)
return AccessTools.DeclaredIndexer(attr.declaringType, attr.argumentTypes).GetGetMethod(true);
return AccessTools.DeclaredProperty(attr.declaringType, attr.methodName).GetGetMethod(true);
return AccessTools.DeclaredIndexer(attr.declaringType, attr.argumentTypes)?.GetGetMethod(true);
return AccessTools.DeclaredProperty(attr.declaringType, attr.methodName)?.GetGetMethod(true);

case MethodType.Setter:
if (attr.methodName is null)
return AccessTools.DeclaredIndexer(attr.declaringType, attr.argumentTypes).GetSetMethod(true);
return AccessTools.DeclaredProperty(attr.declaringType, attr.methodName).GetSetMethod(true);
return AccessTools.DeclaredIndexer(attr.declaringType, attr.argumentTypes)?.GetSetMethod(true);
return AccessTools.DeclaredProperty(attr.declaringType, attr.methodName)?.GetSetMethod(true);

case MethodType.Constructor:
return AccessTools.DeclaredConstructor(attr.GetDeclaringType(), attr.argumentTypes);
Expand Down
14 changes: 14 additions & 0 deletions Harmony/Public/Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -777,4 +777,18 @@ public HarmonyArgument(int index, string name)
NewName = name;
}
}

/// <summary>Attribute used for optionally patching members that might not exist.
/// Harmony patches with this attribute will not throw an exception and abort the patching process if the target member is not found (a warning is logged instead).</summary>
///
[AttributeUsage(AttributeTargets.Method)]
public class HarmonyOptional : HarmonyAttribute
{
/// <summary>Default constructor</summary>
///
public HarmonyOptional()
{
info.optional = true;
}
}
}
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>Whether to not throw/abort when trying to patch members that do not exist (skip instead).</summary>
///
public bool? optional;

/// <summary>Default constructor</summary>
///
Expand Down
22 changes: 21 additions & 1 deletion Harmony/Public/PatchClassProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,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,6 +128,18 @@ void ReversePatch(ref MethodBase lastOriginal)
var annotatedOriginal = patchMethod.info.GetOriginalMethod();
if (annotatedOriginal is object)
lastOriginal = annotatedOriginal;

if (lastOriginal is null)
{
if (patchMethod.info.optional == true)
{
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 @@ -171,7 +183,15 @@ List<MethodInfo> PatchWithAttributes(ref MethodBase lastOriginal)
{
lastOriginal = patchMethod.info.GetOriginalMethod();
if (lastOriginal is null)
{
if (patchMethod.info.optional == true)
{
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 Down
42 changes: 42 additions & 0 deletions HarmonyTests/Patching/Assets/Specials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,48 @@ 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();

[HarmonyPrefix]
[HarmonyOptional]
[HarmonyPatch(typeof(OptionalPatch), "missing_method1")]
[HarmonyPatch(typeof(OptionalPatch), nameof(Thrower), MethodType.Normal)]
[HarmonyPatch(typeof(OptionalPatch), "missing_method2")]
public static bool Test6() => false;

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

public static class OptionalPatchNone
{
[HarmonyPrefix]
[HarmonyPatch(typeof(OptionalPatch), "missing_method1")]
[HarmonyPatch(typeof(OptionalPatchNone), nameof(Thrower), MethodType.Normal)]
[HarmonyPatch(typeof(OptionalPatch), "missing_method2")]
public static bool Test6() => false;

public static void Thrower() => throw new InvalidOperationException();
}

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

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

Assert.Throws<InvalidOperationException>(OptionalPatch.Thrower);
Assert.DoesNotThrow(() => instance.PatchAll(typeof(OptionalPatch)));
Assert.DoesNotThrow(OptionalPatch.Thrower);

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

[Test]
public void Test_Wrap_Patch()
{
Expand Down Expand Up @@ -185,7 +200,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 0e333a7

Please sign in to comment.