diff options
author | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2021-07-30 00:54:15 -0400 |
---|---|---|
committer | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2021-07-30 00:54:15 -0400 |
commit | 948c800a98f00b1bdcfd05ee6228e3423f9eb465 (patch) | |
tree | b958b3cdb4428fa8788a5698a207a45912923de7 /src/SMAPI.Internal.Patching | |
parent | 4074f697d73f5cac6699836550b144fd0c4e2803 (diff) | |
download | SMAPI-948c800a98f00b1bdcfd05ee6228e3423f9eb465.tar.gz SMAPI-948c800a98f00b1bdcfd05ee6228e3423f9eb465.tar.bz2 SMAPI-948c800a98f00b1bdcfd05ee6228e3423f9eb465.zip |
migrate to the new Harmony patch pattern used in my mods
That improves validation and error-handling.
Diffstat (limited to 'src/SMAPI.Internal.Patching')
-rw-r--r-- | src/SMAPI.Internal.Patching/BasePatcher.cs | 54 | ||||
-rw-r--r-- | src/SMAPI.Internal.Patching/HarmonyPatcher.cs | 36 | ||||
-rw-r--r-- | src/SMAPI.Internal.Patching/IPatcher.cs | 16 | ||||
-rw-r--r-- | src/SMAPI.Internal.Patching/PatchHelper.cs | 77 | ||||
-rw-r--r-- | src/SMAPI.Internal.Patching/SMAPI.Internal.Patching.projitems | 17 | ||||
-rw-r--r-- | src/SMAPI.Internal.Patching/SMAPI.Internal.Patching.shproj | 13 |
6 files changed, 213 insertions, 0 deletions
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 +{ + /// <summary>Provides base implementation logic for <see cref="IPatcher"/> instances.</summary> + internal abstract class BasePatcher : IPatcher + { + /********* + ** Public methods + *********/ + /// <inheritdoc /> + public abstract void Apply(Harmony harmony, IMonitor monitor); + + + /********* + ** Protected methods + *********/ + /// <summary>Get a method and assert that it was found.</summary> + /// <typeparam name="TTarget">The type containing the method.</typeparam> + /// <param name="parameters">The method parameter types, or <c>null</c> if it's not overloaded.</param> + protected ConstructorInfo RequireConstructor<TTarget>(params Type[] parameters) + { + return PatchHelper.RequireConstructor<TTarget>(parameters); + } + + /// <summary>Get a method and assert that it was found.</summary> + /// <typeparam name="TTarget">The type containing the method.</typeparam> + /// <param name="name">The method name.</param> + /// <param name="parameters">The method parameter types, or <c>null</c> if it's not overloaded.</param> + /// <param name="generics">The method generic types, or <c>null</c> if it's not generic.</param> + protected MethodInfo RequireMethod<TTarget>(string name, Type[] parameters = null, Type[] generics = null) + { + return PatchHelper.RequireMethod<TTarget>(name, parameters, generics); + } + + /// <summary>Get a Harmony patch method on the current patcher instance.</summary> + /// <param name="name">The method name.</param> + /// <param name="priority">The patch priority to apply, usually specified using Harmony's <see cref="Priority"/> enum, or <c>null</c> to keep the default value.</param> + 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 +{ + /// <summary>Simplifies applying <see cref="IPatcher"/> instances to the game.</summary> + internal static class HarmonyPatcher + { + /********* + ** Public methods + *********/ + /// <summary>Apply the given Harmony patchers.</summary> + /// <param name="id">The mod ID applying the patchers.</param> + /// <param name="monitor">The monitor with which to log any errors.</param> + /// <param name="patchers">The patchers to apply.</param> + 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 +{ + /// <summary>A set of Harmony patches to apply.</summary> + internal interface IPatcher + { + /********* + ** Public methods + *********/ + /// <summary>Apply the Harmony patches for this instance.</summary> + /// <param name="harmony">The Harmony instance.</param> + /// <param name="monitor">The monitor with which to log any errors.</param> + 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 +{ + /// <summary>Provides utility methods for patching game code with Harmony.</summary> + internal static class PatchHelper + { + /********* + ** Public methods + *********/ + /// <summary>Get a constructor and assert that it was found.</summary> + /// <typeparam name="TTarget">The type containing the method.</typeparam> + /// <param name="parameters">The method parameter types, or <c>null</c> if it's not overloaded.</param> + /// <exception cref="InvalidOperationException">The type has no matching constructor.</exception> + public static ConstructorInfo RequireConstructor<TTarget>(Type[] parameters = null) + { + return + AccessTools.Constructor(typeof(TTarget), parameters) + ?? throw new InvalidOperationException($"Can't find constructor {PatchHelper.GetMethodString(typeof(TTarget), null, parameters)} to patch."); + } + + /// <summary>Get a method and assert that it was found.</summary> + /// <typeparam name="TTarget">The type containing the method.</typeparam> + /// <param name="name">The method name.</param> + /// <param name="parameters">The method parameter types, or <c>null</c> if it's not overloaded.</param> + /// <param name="generics">The method generic types, or <c>null</c> if it's not generic.</param> + /// <exception cref="InvalidOperationException">The type has no matching method.</exception> + public static MethodInfo RequireMethod<TTarget>(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."); + } + + /// <summary>Get a human-readable representation of a method target.</summary> + /// <param name="type">The type containing the method.</param> + /// <param name="name">The method name, or <c>null</c> for a constructor.</param> + /// <param name="parameters">The method parameter types, or <c>null</c> if it's not overloaded.</param> + /// <param name="generics">The method generic types, or <c>null</c> if it's not generic.</param> + 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <MSBuildAllProjects Condition="'$(MSBuildVersion)' == '' Or '$(MSBuildVersion)' < '16.0'">$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects> + <HasSharedItems>true</HasSharedItems> + <SharedGUID>6c16e948-3e5c-47a7-bf4b-07a7469a87a5</SharedGUID> + </PropertyGroup> + <PropertyGroup Label="Configuration"> + <Import_RootNamespace>SMAPI.Internal.Patching</Import_RootNamespace> + </PropertyGroup> + <ItemGroup> + <Compile Include="$(MSBuildThisFileDirectory)BasePatcher.cs" /> + <Compile Include="$(MSBuildThisFileDirectory)HarmonyPatcher.cs" /> + <Compile Include="$(MSBuildThisFileDirectory)IPatcher.cs" /> + <Compile Include="$(MSBuildThisFileDirectory)PatchHelper.cs" /> + </ItemGroup> +</Project>
\ 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup Label="Globals"> + <ProjectGuid>6c16e948-3e5c-47a7-bf4b-07a7469a87a5</ProjectGuid> + <MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion> + </PropertyGroup> + <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> + <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" /> + <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" /> + <PropertyGroup /> + <Import Project="SMAPI.Internal.Patching.projitems" Label="Shared" /> + <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" /> +</Project> |