Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add HarmonyOptional attribute #105

Merged
merged 7 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -22,7 +22,7 @@
/// <summary>This is an enumerator (<see cref="IEnumerable{T}"/>, <see cref="IEnumerator{T}"/> or UniTask coroutine)</summary>
/// <remarks>This path will target the <see cref="IEnumerator.MoveNext"/> method that contains the actual enumerator code</remarks>
Enumerator,
Async

Check warning on line 25 in Harmony/Public/Attributes.cs

View workflow job for this annotation

GitHub Actions / test

Missing XML comment for publicly visible type or member 'MethodType.Async'

Check warning on line 25 in Harmony/Public/Attributes.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'MethodType.Async'
}

/// <summary>Specifies the type of argument</summary>
Expand Down Expand Up @@ -777,4 +777,18 @@
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
Loading