From 948c800a98f00b1bdcfd05ee6228e3423f9eb465 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 30 Jul 2021 00:54:15 -0400 Subject: migrate to the new Harmony patch pattern used in my mods That improves validation and error-handling. --- src/SMAPI.Internal.Patching/BasePatcher.cs | 54 +++++++++++++++ src/SMAPI.Internal.Patching/HarmonyPatcher.cs | 36 ++++++++++ src/SMAPI.Internal.Patching/IPatcher.cs | 16 +++++ src/SMAPI.Internal.Patching/PatchHelper.cs | 77 ++++++++++++++++++++++ .../SMAPI.Internal.Patching.projitems | 17 +++++ .../SMAPI.Internal.Patching.shproj | 13 ++++ 6 files changed, 213 insertions(+) create mode 100644 src/SMAPI.Internal.Patching/BasePatcher.cs create mode 100644 src/SMAPI.Internal.Patching/HarmonyPatcher.cs create mode 100644 src/SMAPI.Internal.Patching/IPatcher.cs create mode 100644 src/SMAPI.Internal.Patching/PatchHelper.cs create mode 100644 src/SMAPI.Internal.Patching/SMAPI.Internal.Patching.projitems create mode 100644 src/SMAPI.Internal.Patching/SMAPI.Internal.Patching.shproj (limited to 'src/SMAPI.Internal.Patching') diff --git a/src/SMAPI.Internal.Patching/BasePatcher.cs b/src/SMAPI.Internal.Patching/BasePatcher.cs new file mode 100644 index 00000000..87155d7f --- /dev/null +++ b/src/SMAPI.Internal.Patching/BasePatcher.cs @@ -0,0 +1,54 @@ +using System; +using System.Reflection; +using HarmonyLib; + +namespace StardewModdingAPI.Internal.Patching +{ + /// Provides base implementation logic for instances. + internal abstract class BasePatcher : IPatcher + { + /********* + ** Public methods + *********/ + /// + public abstract void Apply(Harmony harmony, IMonitor monitor); + + + /********* + ** Protected methods + *********/ + /// Get a method and assert that it was found. + /// The type containing the method. + /// The method parameter types, or null if it's not overloaded. + protected ConstructorInfo RequireConstructor(params Type[] parameters) + { + return PatchHelper.RequireConstructor(parameters); + } + + /// Get a method and assert that it was found. + /// The type containing the method. + /// The method name. + /// The method parameter types, or null if it's not overloaded. + /// The method generic types, or null if it's not generic. + protected MethodInfo RequireMethod(string name, Type[] parameters = null, Type[] generics = null) + { + return PatchHelper.RequireMethod(name, parameters, generics); + } + + /// Get a Harmony patch method on the current patcher instance. + /// The method name. + /// The patch priority to apply, usually specified using Harmony's enum, or null to keep the default value. + protected HarmonyMethod GetHarmonyMethod(string name, int? priority = null) + { + var method = new HarmonyMethod( + AccessTools.Method(this.GetType(), name) + ?? throw new InvalidOperationException($"Can't find patcher method {PatchHelper.GetMethodString(this.GetType(), name)}.") + ); + + if (priority.HasValue) + method.priority = priority.Value; + + return method; + } + } +} diff --git a/src/SMAPI.Internal.Patching/HarmonyPatcher.cs b/src/SMAPI.Internal.Patching/HarmonyPatcher.cs new file mode 100644 index 00000000..c07e3b41 --- /dev/null +++ b/src/SMAPI.Internal.Patching/HarmonyPatcher.cs @@ -0,0 +1,36 @@ +using System; +using HarmonyLib; + +namespace StardewModdingAPI.Internal.Patching +{ + /// Simplifies applying instances to the game. + internal static class HarmonyPatcher + { + /********* + ** Public methods + *********/ + /// Apply the given Harmony patchers. + /// The mod ID applying the patchers. + /// The monitor with which to log any errors. + /// The patchers to apply. + public static Harmony Apply(string id, IMonitor monitor, params IPatcher[] patchers) + { + Harmony harmony = new Harmony(id); + + foreach (IPatcher patcher in patchers) + { + try + { + patcher.Apply(harmony, monitor); + } + catch (Exception ex) + { + monitor.Log($"Couldn't apply runtime patch '{patcher.GetType().Name}' to the game. Some SMAPI features may not work correctly. See log file for details.", LogLevel.Error); + monitor.Log($"Technical details:\n{ex.GetLogSummary()}"); + } + } + + return harmony; + } + } +} diff --git a/src/SMAPI.Internal.Patching/IPatcher.cs b/src/SMAPI.Internal.Patching/IPatcher.cs new file mode 100644 index 00000000..a732d64f --- /dev/null +++ b/src/SMAPI.Internal.Patching/IPatcher.cs @@ -0,0 +1,16 @@ +using HarmonyLib; + +namespace StardewModdingAPI.Internal.Patching +{ + /// A set of Harmony patches to apply. + internal interface IPatcher + { + /********* + ** Public methods + *********/ + /// Apply the Harmony patches for this instance. + /// The Harmony instance. + /// The monitor with which to log any errors. + public void Apply(Harmony harmony, IMonitor monitor); + } +} diff --git a/src/SMAPI.Internal.Patching/PatchHelper.cs b/src/SMAPI.Internal.Patching/PatchHelper.cs new file mode 100644 index 00000000..c9758616 --- /dev/null +++ b/src/SMAPI.Internal.Patching/PatchHelper.cs @@ -0,0 +1,77 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Text; +using HarmonyLib; + +namespace StardewModdingAPI.Internal.Patching +{ + /// Provides utility methods for patching game code with Harmony. + internal static class PatchHelper + { + /********* + ** Public methods + *********/ + /// Get a constructor and assert that it was found. + /// The type containing the method. + /// The method parameter types, or null if it's not overloaded. + /// The type has no matching constructor. + public static ConstructorInfo RequireConstructor(Type[] parameters = null) + { + return + AccessTools.Constructor(typeof(TTarget), parameters) + ?? throw new InvalidOperationException($"Can't find constructor {PatchHelper.GetMethodString(typeof(TTarget), null, parameters)} to patch."); + } + + /// Get a method and assert that it was found. + /// The type containing the method. + /// The method name. + /// The method parameter types, or null if it's not overloaded. + /// The method generic types, or null if it's not generic. + /// The type has no matching method. + public static MethodInfo RequireMethod(string name, Type[] parameters = null, Type[] generics = null) + { + return + AccessTools.Method(typeof(TTarget), name, parameters, generics) + ?? throw new InvalidOperationException($"Can't find method {PatchHelper.GetMethodString(typeof(TTarget), name, parameters, generics)} to patch."); + } + + /// Get a human-readable representation of a method target. + /// The type containing the method. + /// The method name, or null for a constructor. + /// The method parameter types, or null if it's not overloaded. + /// The method generic types, or null if it's not generic. + public static string GetMethodString(Type type, string name, Type[] parameters = null, Type[] generics = null) + { + StringBuilder str = new(); + + // type + str.Append(type.FullName); + + // method name (if not constructor) + if (name != null) + { + str.Append('.'); + str.Append(name); + } + + // generics + if (generics?.Any() == true) + { + str.Append('<'); + str.Append(string.Join(", ", generics.Select(p => p.FullName))); + str.Append('>'); + } + + // parameters + if (parameters?.Any() == true) + { + str.Append('('); + str.Append(string.Join(", ", parameters.Select(p => p.FullName))); + str.Append(')'); + } + + return str.ToString(); + } + } +} diff --git a/src/SMAPI.Internal.Patching/SMAPI.Internal.Patching.projitems b/src/SMAPI.Internal.Patching/SMAPI.Internal.Patching.projitems new file mode 100644 index 00000000..4fa2a062 --- /dev/null +++ b/src/SMAPI.Internal.Patching/SMAPI.Internal.Patching.projitems @@ -0,0 +1,17 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 6c16e948-3e5c-47a7-bf4b-07a7469a87a5 + + + SMAPI.Internal.Patching + + + + + + + + \ No newline at end of file diff --git a/src/SMAPI.Internal.Patching/SMAPI.Internal.Patching.shproj b/src/SMAPI.Internal.Patching/SMAPI.Internal.Patching.shproj new file mode 100644 index 00000000..1a102c82 --- /dev/null +++ b/src/SMAPI.Internal.Patching/SMAPI.Internal.Patching.shproj @@ -0,0 +1,13 @@ + + + + 6c16e948-3e5c-47a7-bf4b-07a7469a87a5 + 14.0 + + + + + + + + -- cgit From f7f49aa8dfc4a608a5c5571815263cca195dd540 Mon Sep 17 00:00:00 2001 From: bladeoflight16 <1159076+bladeoflight16@users.noreply.github.com> Date: Sat, 31 Jul 2021 15:28:22 -0400 Subject: Target typed constructors are not yet supported by Mono. Adding explicit class name to resolve build error. --- src/SMAPI.Internal.Patching/PatchHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI.Internal.Patching') diff --git a/src/SMAPI.Internal.Patching/PatchHelper.cs b/src/SMAPI.Internal.Patching/PatchHelper.cs index c9758616..fc79ddf2 100644 --- a/src/SMAPI.Internal.Patching/PatchHelper.cs +++ b/src/SMAPI.Internal.Patching/PatchHelper.cs @@ -43,7 +43,7 @@ namespace StardewModdingAPI.Internal.Patching /// The method generic types, or null if it's not generic. public static string GetMethodString(Type type, string name, Type[] parameters = null, Type[] generics = null) { - StringBuilder str = new(); + StringBuilder str = new StringBuilder(); // type str.Append(type.FullName); -- cgit