From 948c800a98f00b1bdcfd05ee6228e3423f9eb465 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 30 Jul 2021 00:54:15 -0400 Subject: [PATCH] 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 ++++ src/SMAPI.Internal/ExceptionExtensions.cs | 41 ++++++++++ src/SMAPI.Internal/SMAPI.Internal.projitems | 1 + src/SMAPI.Mods.ErrorHandler/ModEntry.cs | 4 +- .../Patches/DialoguePatcher.cs | 18 ++--- .../Patches/DictionaryPatcher.cs | 28 +++---- .../Patches/EventPatcher.cs | 12 +-- .../Patches/GameLocationPatcher.cs | 24 +++--- .../Patches/IClickableMenuPatcher.cs | 14 ++-- .../Patches/NpcPatcher.cs | 24 +++--- .../Patches/ObjectPatcher.cs | 16 ++-- .../Patches/SaveGamePatcher.cs | 15 ++-- .../Patches/SpriteBatchPatcher.cs | 20 ++--- .../Patches/UtilityPatcher.cs | 14 ++-- .../SMAPI.Mods.ErrorHandler.csproj | 1 + src/SMAPI.sln | 6 ++ .../Content/AssetInterceptorChange.cs | 1 + .../ContentManagers/GameContentManager.cs | 1 + .../ContentManagers/ModContentManager.cs | 1 + src/SMAPI/Framework/Events/ManagedEvent.cs | 1 + src/SMAPI/Framework/InternalExtensions.cs | 35 +-------- src/SMAPI/Framework/Logging/LogManager.cs | 1 + src/SMAPI/Framework/Patching/GamePatcher.cs | 45 ----------- src/SMAPI/Framework/Patching/IHarmonyPatch.cs | 15 ---- src/SMAPI/Framework/SCore.cs | 5 +- src/SMAPI/Framework/SGame.cs | 1 + src/SMAPI/Metadata/CoreAssetPropagator.cs | 1 + src/SMAPI/Patches/Game1Patcher.cs | 38 ++++----- src/SMAPI/Patches/TitleMenuPatcher.cs | 17 ++-- src/SMAPI/Properties/AssemblyInfo.cs | 1 - src/SMAPI/SMAPI.csproj | 1 + 36 files changed, 394 insertions(+), 221 deletions(-) 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 create mode 100644 src/SMAPI.Internal/ExceptionExtensions.cs delete mode 100644 src/SMAPI/Framework/Patching/GamePatcher.cs delete mode 100644 src/SMAPI/Framework/Patching/IHarmonyPatch.cs 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 + + + + + + + + diff --git a/src/SMAPI.Internal/ExceptionExtensions.cs b/src/SMAPI.Internal/ExceptionExtensions.cs new file mode 100644 index 00000000..d7a2252b --- /dev/null +++ b/src/SMAPI.Internal/ExceptionExtensions.cs @@ -0,0 +1,41 @@ +using System; +using System.Reflection; + +namespace StardewModdingAPI.Internal +{ + /// Provides extension methods for handling exceptions. + internal static class ExceptionExtensions + { + /********* + ** Public methods + *********/ + /// Get a string representation of an exception suitable for writing to the error log. + /// The error to summarize. + public static string GetLogSummary(this Exception exception) + { + switch (exception) + { + case TypeLoadException ex: + return $"Failed loading type '{ex.TypeName}': {exception}"; + + case ReflectionTypeLoadException ex: + string summary = exception.ToString(); + foreach (Exception childEx in ex.LoaderExceptions) + summary += $"\n\n{childEx.GetLogSummary()}"; + return summary; + + default: + return exception.ToString(); + } + } + + /// Get the lowest exception in an exception stack. + /// The exception from which to search. + public static Exception GetInnermostException(this Exception exception) + { + while (exception.InnerException != null) + exception = exception.InnerException; + return exception; + } + } +} diff --git a/src/SMAPI.Internal/SMAPI.Internal.projitems b/src/SMAPI.Internal/SMAPI.Internal.projitems index 0d583a6d..0ee94a5b 100644 --- a/src/SMAPI.Internal/SMAPI.Internal.projitems +++ b/src/SMAPI.Internal/SMAPI.Internal.projitems @@ -14,5 +14,6 @@ + \ No newline at end of file diff --git a/src/SMAPI.Mods.ErrorHandler/ModEntry.cs b/src/SMAPI.Mods.ErrorHandler/ModEntry.cs index ac9d1b94..067f6a8d 100644 --- a/src/SMAPI.Mods.ErrorHandler/ModEntry.cs +++ b/src/SMAPI.Mods.ErrorHandler/ModEntry.cs @@ -1,7 +1,7 @@ using System; using System.Reflection; using StardewModdingAPI.Events; -using StardewModdingAPI.Framework.Patching; +using StardewModdingAPI.Internal.Patching; using StardewModdingAPI.Mods.ErrorHandler.Patches; using StardewValley; @@ -28,7 +28,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler IMonitor monitorForGame = this.GetMonitorForGame(); // apply patches - new GamePatcher(this.Monitor).Apply( + HarmonyPatcher.Apply(this.ModManifest.UniqueID, this.Monitor, new DialoguePatcher(monitorForGame, this.Helper.Reflection), new DictionaryPatcher(this.Helper.Reflection), new EventPatcher(monitorForGame), diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/DialoguePatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/DialoguePatcher.cs index 7b730ee5..7a3af39c 100644 --- a/src/SMAPI.Mods.ErrorHandler/Patches/DialoguePatcher.cs +++ b/src/SMAPI.Mods.ErrorHandler/Patches/DialoguePatcher.cs @@ -1,17 +1,17 @@ using System; using System.Diagnostics.CodeAnalysis; using HarmonyLib; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.Patching; +using StardewModdingAPI.Internal; +using StardewModdingAPI.Internal.Patching; using StardewValley; namespace StardewModdingAPI.Mods.ErrorHandler.Patches { - /// A Harmony patch for the constructor which intercepts invalid dialogue lines and logs an error instead of crashing. + /// Harmony patches for which intercept invalid dialogue lines and logs an error instead of crashing. /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - internal class DialoguePatcher : IHarmonyPatch + internal class DialoguePatcher : BasePatcher { /********* ** Fields @@ -36,11 +36,11 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches } /// - public void Apply(Harmony harmony) + public override void Apply(Harmony harmony, IMonitor monitor) { harmony.Patch( - original: AccessTools.Constructor(typeof(Dialogue), new[] { typeof(string), typeof(NPC) }), - finalizer: new HarmonyMethod(this.GetType(), nameof(DialoguePatcher.Finalize_Dialogue_Constructor)) + original: this.RequireConstructor(typeof(string), typeof(NPC)), + finalizer: this.GetHarmonyMethod(nameof(DialoguePatcher.Finalize_Constructor)) ); } @@ -48,13 +48,13 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches /********* ** Private methods *********/ - /// The method to call after the Dialogue constructor. + /// The method to call when the Dialogue constructor throws an exception. /// The instance being patched. /// The dialogue being parsed. /// The NPC for which the dialogue is being parsed. /// The exception thrown by the wrapped method, if any. /// Returns the exception to throw, if any. - private static Exception Finalize_Dialogue_Constructor(Dialogue __instance, string masterDialogue, NPC speaker, Exception __exception) + private static Exception Finalize_Constructor(Dialogue __instance, string masterDialogue, NPC speaker, Exception __exception) { if (__exception != null) { diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/DictionaryPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/DictionaryPatcher.cs index 3c5240b6..6ad64e16 100644 --- a/src/SMAPI.Mods.ErrorHandler/Patches/DictionaryPatcher.cs +++ b/src/SMAPI.Mods.ErrorHandler/Patches/DictionaryPatcher.cs @@ -2,18 +2,18 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using HarmonyLib; -using StardewModdingAPI.Framework.Patching; +using StardewModdingAPI.Internal.Patching; using StardewValley.GameData; using StardewValley.GameData.HomeRenovations; using StardewValley.GameData.Movies; namespace StardewModdingAPI.Mods.ErrorHandler.Patches { - /// A Harmony patch for which adds the accessed key to exceptions. + /// Harmony patches for which add the accessed key to exceptions. /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - internal class DictionaryPatcher : IHarmonyPatch + internal class DictionaryPatcher : BasePatcher { /********* ** Fields @@ -33,7 +33,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches } /// - public void Apply(Harmony harmony) + public override void Apply(Harmony harmony, IMonitor monitor) { Type[] keyTypes = { typeof(int), typeof(string) }; Type[] valueTypes = { typeof(int), typeof(string), typeof(HomeRenovation), typeof(MovieData), typeof(SpecialOrderData) }; @@ -45,8 +45,8 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches Type dictionaryType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType); harmony.Patch( - original: AccessTools.Method(dictionaryType, "get_Item"), - finalizer: new HarmonyMethod(this.GetType(), nameof(DictionaryPatcher.Finalize_GetItem)) + original: AccessTools.Method(dictionaryType, "get_Item") ?? throw new InvalidOperationException($"Can't find method {PatchHelper.GetMethodString(dictionaryType, "get_Item")} to patch."), + finalizer: this.GetHarmonyMethod(nameof(DictionaryPatcher.Finalize_GetItem)) ); } } @@ -63,19 +63,13 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches private static Exception Finalize_GetItem(object key, Exception __exception) { if (__exception is KeyNotFoundException) - AddKeyTo(__exception, key?.ToString()); + { + DictionaryPatcher.Reflection + .GetField(__exception, "_message") + .SetValue($"{__exception.Message}\nkey: '{key}'"); + } return __exception; } - - /// Add the accessed key to an exception message. - /// The exception to modify. - /// The dictionary key. - private static void AddKeyTo(Exception exception, string key) - { - DictionaryPatcher.Reflection - .GetField(exception, "_message") - .SetValue($"{exception.Message}\nkey: '{key}'"); - } } } diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/EventPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/EventPatcher.cs index 9a7b34d8..1b706147 100644 --- a/src/SMAPI.Mods.ErrorHandler/Patches/EventPatcher.cs +++ b/src/SMAPI.Mods.ErrorHandler/Patches/EventPatcher.cs @@ -1,7 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; using HarmonyLib; -using StardewModdingAPI.Framework.Patching; +using StardewModdingAPI.Internal.Patching; using StardewValley; namespace StardewModdingAPI.Mods.ErrorHandler.Patches @@ -10,7 +10,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - internal class EventPatcher : IHarmonyPatch + internal class EventPatcher : BasePatcher { /********* ** Fields @@ -30,11 +30,11 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches } /// - public void Apply(Harmony harmony) + public override void Apply(Harmony harmony, IMonitor monitor) { harmony.Patch( - original: AccessTools.Method(typeof(Event), nameof(Event.LogErrorAndHalt)), - postfix: new HarmonyMethod(this.GetType(), nameof(EventPatcher.After_Event_LogErrorAndHalt)) + original: this.RequireMethod(nameof(Event.LogErrorAndHalt)), + postfix: this.GetHarmonyMethod(nameof(EventPatcher.After_LogErrorAndHalt)) ); } @@ -44,7 +44,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches *********/ /// The method to call after . /// The exception being logged. - private static void After_Event_LogErrorAndHalt(Exception e) + private static void After_LogErrorAndHalt(Exception e) { EventPatcher.MonitorForGame.Log(e.ToString(), LogLevel.Error); } diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatcher.cs index 7427fe48..7df6b0a2 100644 --- a/src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatcher.cs +++ b/src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatcher.cs @@ -1,17 +1,17 @@ using System; using System.Diagnostics.CodeAnalysis; using HarmonyLib; -using StardewModdingAPI.Framework.Patching; +using StardewModdingAPI.Internal.Patching; using StardewValley; using xTile; namespace StardewModdingAPI.Mods.ErrorHandler.Patches { - /// Harmony patches for and which intercept errors instead of crashing. + /// Harmony patches for which intercept errors instead of crashing. /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - internal class GameLocationPatcher : IHarmonyPatch + internal class GameLocationPatcher : BasePatcher { /********* ** Fields @@ -31,15 +31,15 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches } /// - public void Apply(Harmony harmony) + public override void Apply(Harmony harmony, IMonitor monitor) { harmony.Patch( - original: AccessTools.Method(typeof(GameLocation), nameof(GameLocation.checkEventPrecondition)), - finalizer: new HarmonyMethod(this.GetType(), nameof(GameLocationPatcher.Finalize_GameLocation_CheckEventPrecondition)) + original: this.RequireMethod(nameof(GameLocation.checkEventPrecondition)), + finalizer: this.GetHarmonyMethod(nameof(GameLocationPatcher.Finalize_CheckEventPrecondition)) ); harmony.Patch( - original: AccessTools.Method(typeof(GameLocation), nameof(GameLocation.updateSeasonalTileSheets)), - finalizer: new HarmonyMethod(this.GetType(), nameof(GameLocationPatcher.Before_GameLocation_UpdateSeasonalTileSheets)) + original: this.RequireMethod(nameof(GameLocation.updateSeasonalTileSheets)), + finalizer: this.GetHarmonyMethod(nameof(GameLocationPatcher.Finalize_UpdateSeasonalTileSheets)) ); } @@ -47,12 +47,12 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches /********* ** Private methods *********/ - /// The method to call instead of GameLocation.checkEventPrecondition. + /// The method to call when throws an exception. /// The return value of the original method. /// The precondition to be parsed. /// The exception thrown by the wrapped method, if any. /// Returns the exception to throw, if any. - private static Exception Finalize_GameLocation_CheckEventPrecondition(ref int __result, string precondition, Exception __exception) + private static Exception Finalize_CheckEventPrecondition(ref int __result, string precondition, Exception __exception) { if (__exception != null) { @@ -63,12 +63,12 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches return null; } - /// The method to call instead of . + /// The method to call when throws an exception. /// The instance being patched. /// The map whose tilesheets to update. /// The exception thrown by the wrapped method, if any. /// Returns the exception to throw, if any. - private static Exception Before_GameLocation_UpdateSeasonalTileSheets(GameLocation __instance, Map map, Exception __exception) + private static Exception Finalize_UpdateSeasonalTileSheets(GameLocation __instance, Map map, Exception __exception) { if (__exception != null) GameLocationPatcher.MonitorForGame.Log($"Failed updating seasonal tilesheets for location '{__instance.NameOrUniqueName}': \n{__exception}", LogLevel.Error); diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/IClickableMenuPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/IClickableMenuPatcher.cs index d6f9fbf4..b65a695a 100644 --- a/src/SMAPI.Mods.ErrorHandler/Patches/IClickableMenuPatcher.cs +++ b/src/SMAPI.Mods.ErrorHandler/Patches/IClickableMenuPatcher.cs @@ -1,27 +1,27 @@ using System.Diagnostics.CodeAnalysis; using HarmonyLib; -using StardewModdingAPI.Framework.Patching; +using StardewModdingAPI.Internal.Patching; using StardewValley; using StardewValley.Menus; using SObject = StardewValley.Object; namespace StardewModdingAPI.Mods.ErrorHandler.Patches { - /// A Harmony patch for which intercepts crashes due to invalid items. + /// Harmony patches for which intercept crashes due to invalid items. /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - internal class IClickableMenuPatcher : IHarmonyPatch + internal class IClickableMenuPatcher : BasePatcher { /********* ** Public methods *********/ /// - public void Apply(Harmony harmony) + public override void Apply(Harmony harmony, IMonitor monitor) { harmony.Patch( - original: AccessTools.Method(typeof(IClickableMenu), nameof(IClickableMenu.drawToolTip)), - prefix: new HarmonyMethod(this.GetType(), nameof(IClickableMenuPatcher.Before_IClickableMenu_DrawTooltip)) + original: this.RequireMethod(nameof(IClickableMenu.drawToolTip)), + prefix: this.GetHarmonyMethod(nameof(IClickableMenuPatcher.Before_DrawTooltip)) ); } @@ -32,7 +32,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches /// The method to call instead of . /// The item for which to draw a tooltip. /// Returns whether to execute the original method. - private static bool Before_IClickableMenu_DrawTooltip(Item hoveredItem) + private static bool Before_DrawTooltip(Item hoveredItem) { // invalid edible item cause crash when drawing tooltips if (hoveredItem is SObject obj && obj.Edibility != -300 && !Game1.objectInformation.ContainsKey(obj.ParentSheetIndex)) diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/NpcPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/NpcPatcher.cs index 4a07ea1d..275bb5bf 100644 --- a/src/SMAPI.Mods.ErrorHandler/Patches/NpcPatcher.cs +++ b/src/SMAPI.Mods.ErrorHandler/Patches/NpcPatcher.cs @@ -2,17 +2,17 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using HarmonyLib; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.Patching; +using StardewModdingAPI.Internal; +using StardewModdingAPI.Internal.Patching; using StardewValley; namespace StardewModdingAPI.Mods.ErrorHandler.Patches { - /// A Harmony patch for which intercepts crashes due to invalid schedule data. + /// Harmony patches for which intercept crashes due to invalid schedule data. /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - internal class NpcPatcher : IHarmonyPatch + internal class NpcPatcher : BasePatcher { /********* ** Fields @@ -32,16 +32,16 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches } /// - public void Apply(Harmony harmony) + public override void Apply(Harmony harmony, IMonitor monitor) { harmony.Patch( - original: AccessTools.Property(typeof(NPC), nameof(NPC.CurrentDialogue)).GetMethod, - finalizer: new HarmonyMethod(this.GetType(), nameof(NpcPatcher.Finalize_NPC_CurrentDialogue)) + original: this.RequireMethod($"get_{nameof(NPC.CurrentDialogue)}"), + finalizer: this.GetHarmonyMethod(nameof(NpcPatcher.Finalize_CurrentDialogue)) ); harmony.Patch( - original: AccessTools.Method(typeof(NPC), nameof(NPC.parseMasterSchedule)), - finalizer: new HarmonyMethod(this.GetType(), nameof(NpcPatcher.Finalize_NPC_parseMasterSchedule)) + original: this.RequireMethod(nameof(NPC.parseMasterSchedule)), + finalizer: this.GetHarmonyMethod(nameof(NpcPatcher.Finalize_ParseMasterSchedule)) ); } @@ -49,12 +49,12 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches /********* ** Private methods *********/ - /// The method to call after . + /// The method to call when throws an exception. /// The instance being patched. /// The return value of the original method. /// The exception thrown by the wrapped method, if any. /// Returns the exception to throw, if any. - private static Exception Finalize_NPC_CurrentDialogue(NPC __instance, ref Stack __result, Exception __exception) + private static Exception Finalize_CurrentDialogue(NPC __instance, ref Stack __result, Exception __exception) { if (__exception == null) return null; @@ -71,7 +71,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches /// The patched method's return value. /// The exception thrown by the wrapped method, if any. /// Returns the exception to throw, if any. - private static Exception Finalize_NPC_parseMasterSchedule(string rawData, NPC __instance, ref Dictionary __result, Exception __exception) + private static Exception Finalize_ParseMasterSchedule(string rawData, NPC __instance, ref Dictionary __result, Exception __exception) { if (__exception != null) { diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/ObjectPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/ObjectPatcher.cs index c4b25b96..fd4ea35c 100644 --- a/src/SMAPI.Mods.ErrorHandler/Patches/ObjectPatcher.cs +++ b/src/SMAPI.Mods.ErrorHandler/Patches/ObjectPatcher.cs @@ -2,34 +2,34 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using HarmonyLib; -using StardewModdingAPI.Framework.Patching; +using StardewModdingAPI.Internal.Patching; using StardewValley; using SObject = StardewValley.Object; namespace StardewModdingAPI.Mods.ErrorHandler.Patches { - /// A Harmony patch for which intercepts crashes due to the item no longer existing. + /// Harmony patches for which intercept crashes due to invalid items. /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - internal class ObjectPatcher : IHarmonyPatch + internal class ObjectPatcher : BasePatcher { /********* ** Public methods *********/ /// - public void Apply(Harmony harmony) + public override void Apply(Harmony harmony, IMonitor monitor) { // object.getDescription harmony.Patch( - original: AccessTools.Method(typeof(SObject), nameof(SObject.getDescription)), - prefix: new HarmonyMethod(this.GetType(), nameof(ObjectPatcher.Before_Object_GetDescription)) + original: this.RequireMethod(nameof(SObject.getDescription)), + prefix: this.GetHarmonyMethod(nameof(ObjectPatcher.Before_Object_GetDescription)) ); // object.getDisplayName harmony.Patch( - original: AccessTools.Method(typeof(SObject), "loadDisplayName"), - finalizer: new HarmonyMethod(this.GetType(), nameof(ObjectPatcher.Finalize_Object_loadDisplayName)) + original: this.RequireMethod("loadDisplayName"), + finalizer: this.GetHarmonyMethod(nameof(ObjectPatcher.Finalize_Object_loadDisplayName)) ); } diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs index ef165831..8945e4f3 100644 --- a/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs +++ b/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs @@ -4,18 +4,18 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using HarmonyLib; using Microsoft.Xna.Framework.Content; -using StardewModdingAPI.Framework.Patching; +using StardewModdingAPI.Internal.Patching; using StardewValley; using StardewValley.Buildings; using StardewValley.Locations; namespace StardewModdingAPI.Mods.ErrorHandler.Patches { - /// A Harmony patch for which prevents some errors due to broken save data. + /// Harmony patches for which prevent some errors due to broken save data. /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - internal class SaveGamePatcher : IHarmonyPatch + internal class SaveGamePatcher : BasePatcher { /********* ** Fields @@ -39,13 +39,12 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches SaveGamePatcher.OnContentRemoved = onContentRemoved; } - /// - public void Apply(Harmony harmony) + public override void Apply(Harmony harmony, IMonitor monitor) { harmony.Patch( - original: AccessTools.Method(typeof(SaveGame), nameof(SaveGame.loadDataToLocations)), - prefix: new HarmonyMethod(this.GetType(), nameof(SaveGamePatcher.Before_SaveGame_LoadDataToLocations)) + original: this.RequireMethod(nameof(SaveGame.loadDataToLocations)), + prefix: this.GetHarmonyMethod(nameof(SaveGamePatcher.Before_LoadDataToLocations)) ); } @@ -56,7 +55,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches /// The method to call instead of . /// The game locations being loaded. /// Returns whether to execute the original method. - private static bool Before_SaveGame_LoadDataToLocations(List gamelocations) + private static bool Before_LoadDataToLocations(List gamelocations) { bool removedAny = SaveGamePatcher.RemoveBrokenBuildings(gamelocations) diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchPatcher.cs index 1099afee..6860a4ec 100644 --- a/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchPatcher.cs +++ b/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchPatcher.cs @@ -2,27 +2,27 @@ using System; using System.Diagnostics.CodeAnalysis; using HarmonyLib; using Microsoft.Xna.Framework.Graphics; -using StardewModdingAPI.Framework.Patching; +using StardewModdingAPI.Internal.Patching; namespace StardewModdingAPI.Mods.ErrorHandler.Patches { - /// Harmony patch for to validate textures earlier. + /// Harmony patches for which validate textures earlier. /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - internal class SpriteBatchPatcher : IHarmonyPatch + internal class SpriteBatchPatcher : BasePatcher { /********* ** Public methods *********/ /// - public void Apply(Harmony harmony) + public override void Apply(Harmony harmony, IMonitor monitor) { harmony.Patch( original: Constants.GameFramework == GameFramework.Xna - ? AccessTools.Method(typeof(SpriteBatch), "InternalDraw") - : AccessTools.Method(typeof(SpriteBatch), "CheckValid", new[] { typeof(Texture2D) }), - postfix: new HarmonyMethod(this.GetType(), nameof(SpriteBatchPatcher.After_SpriteBatch_CheckValid)) + ? this.RequireMethod("InternalDraw") + : this.RequireMethod("CheckValid", new[] { typeof(Texture2D) }), + postfix: this.GetHarmonyMethod(nameof(SpriteBatchPatcher.After_CheckValid)) ); } @@ -31,13 +31,13 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches ** Private methods *********/ #if SMAPI_FOR_XNA - /// The method to call instead of . + /// The method to call after . /// The texture to validate. #else - /// The method to call instead of . + /// The method to call after . /// The texture to validate. #endif - private static void After_SpriteBatch_CheckValid(Texture2D texture) + private static void After_CheckValid(Texture2D texture) { if (texture?.IsDisposed == true) throw new ObjectDisposedException("Cannot draw this texture because it's disposed."); diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/UtilityPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/UtilityPatcher.cs index e29e3030..ce85d0c2 100644 --- a/src/SMAPI.Mods.ErrorHandler/Patches/UtilityPatcher.cs +++ b/src/SMAPI.Mods.ErrorHandler/Patches/UtilityPatcher.cs @@ -1,7 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; using HarmonyLib; -using StardewModdingAPI.Framework.Patching; +using StardewModdingAPI.Internal.Patching; using StardewValley; namespace StardewModdingAPI.Mods.ErrorHandler.Patches @@ -10,17 +10,17 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - internal class UtilityPatcher : IHarmonyPatch + internal class UtilityPatcher : BasePatcher { /********* ** Public methods *********/ /// - public void Apply(Harmony harmony) + public override void Apply(Harmony harmony, IMonitor monitor) { harmony.Patch( - original: AccessTools.Method(typeof(Utility), nameof(Utility.getItemFromStandardTextDescription)), - finalizer: new HarmonyMethod(this.GetType(), nameof(UtilityPatcher.Finalize_Utility_GetItemFromStandardTextDescription)) + original: this.RequireMethod(nameof(Utility.getItemFromStandardTextDescription)), + finalizer: this.GetHarmonyMethod(nameof(UtilityPatcher.Finalize_GetItemFromStandardTextDescription)) ); } @@ -28,12 +28,12 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches /********* ** Private methods *********/ - /// The method to call instead of . + /// The method to call when throws an exception. /// The item text description to parse. /// The delimiter by which to split the text description. /// The exception thrown by the wrapped method, if any. /// Returns the exception to throw, if any. - private static Exception Finalize_Utility_GetItemFromStandardTextDescription(string description, char delimiter, ref Exception __exception) + private static Exception Finalize_GetItemFromStandardTextDescription(string description, char delimiter, ref Exception __exception) { return __exception != null ? new FormatException($"Failed to parse item text description \"{description}\" with delimiter \"{delimiter}\".", __exception) diff --git a/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj b/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj index 531d3699..ffda5f89 100644 --- a/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj +++ b/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj @@ -47,4 +47,5 @@ + diff --git a/src/SMAPI.sln b/src/SMAPI.sln index 58228ce9..92c6cb24 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -52,6 +52,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mods", "Mods", "{AE9A4D46-E EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "SMAPI.Internal", "SMAPI.Internal\SMAPI.Internal.shproj", "{85208F8D-6FD1-4531-BE05-7142490F59FE}" EndProject +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "SMAPI.Internal.Patching", "SMAPI.Internal.Patching\SMAPI.Internal.Patching.shproj", "{6C16E948-3E5C-47A7-BF4B-07A7469A87A5}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.ModBuildConfig.Analyzer.Tests", "SMAPI.ModBuildConfig.Analyzer.Tests\SMAPI.ModBuildConfig.Analyzer.Tests.csproj", "{680B2641-81EA-467C-86A5-0E81CDC57ED0}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Tests", "SMAPI.Tests\SMAPI.Tests.csproj", "{AA95884B-7097-476E-92C8-D0500DE9D6D1}" @@ -86,10 +88,13 @@ Global GlobalSection(SharedMSBuildProjectFiles) = preSolution SMAPI.Internal\SMAPI.Internal.projitems*{0634ea4c-3b8f-42db-aea6-ca9e4ef6e92f}*SharedItemsImports = 5 SMAPI.Internal\SMAPI.Internal.projitems*{0a9bb24f-15ff-4c26-b1a2-81f7ae316518}*SharedItemsImports = 5 + SMAPI.Internal.Patching\SMAPI.Internal.Patching.projitems*{491e775b-ead0-44d4-b6ca-f1fc3e316d33}*SharedItemsImports = 5 SMAPI.Internal\SMAPI.Internal.projitems*{491e775b-ead0-44d4-b6ca-f1fc3e316d33}*SharedItemsImports = 5 + SMAPI.Internal.Patching\SMAPI.Internal.Patching.projitems*{6c16e948-3e5c-47a7-bf4b-07a7469a87a5}*SharedItemsImports = 13 SMAPI.Internal\SMAPI.Internal.projitems*{80efd92f-728f-41e0-8a5b-9f6f49a91899}*SharedItemsImports = 5 SMAPI.Internal\SMAPI.Internal.projitems*{85208f8d-6fd1-4531-be05-7142490f59fe}*SharedItemsImports = 13 SMAPI.Internal\SMAPI.Internal.projitems*{cd53ad6f-97f4-4872-a212-50c2a0fd3601}*SharedItemsImports = 5 + SMAPI.Internal.Patching\SMAPI.Internal.Patching.projitems*{e6da2198-7686-4f1d-b312-4a4dc70884c0}*SharedItemsImports = 5 SMAPI.Internal\SMAPI.Internal.projitems*{e6da2198-7686-4f1d-b312-4a4dc70884c0}*SharedItemsImports = 5 EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -156,6 +161,7 @@ Global {EB35A917-67B9-4EFA-8DFC-4FB49B3949BB} = {86C452BE-D2D8-45B4-B63F-E329EB06CEDA} {5947303D-3512-413A-9009-7AC43F5D3513} = {EB35A917-67B9-4EFA-8DFC-4FB49B3949BB} {85208F8D-6FD1-4531-BE05-7142490F59FE} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} + {6C16E948-3E5C-47A7-BF4B-07A7469A87A5} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} {680B2641-81EA-467C-86A5-0E81CDC57ED0} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} {AA95884B-7097-476E-92C8-D0500DE9D6D1} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F} = {AE9A4D46-E910-4293-8BC4-673F85FFF6A5} diff --git a/src/SMAPI/Framework/Content/AssetInterceptorChange.cs b/src/SMAPI/Framework/Content/AssetInterceptorChange.cs index 037d9f89..10488b84 100644 --- a/src/SMAPI/Framework/Content/AssetInterceptorChange.cs +++ b/src/SMAPI/Framework/Content/AssetInterceptorChange.cs @@ -1,5 +1,6 @@ using System; using System.Reflection; +using StardewModdingAPI.Internal; namespace StardewModdingAPI.Framework.Content { diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 80a9937a..63cd1759 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -9,6 +9,7 @@ using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Utilities; +using StardewModdingAPI.Internal; using StardewValley; using xTile; diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index bc5a8b74..d24ffb81 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -7,6 +7,7 @@ using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Internal; using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Utilities; using StardewValley; diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index 2204966c..fa20a079 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using StardewModdingAPI.Events; +using StardewModdingAPI.Internal; namespace StardewModdingAPI.Framework.Events { diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index ab7f1e6c..6c9a5f3b 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -14,6 +14,9 @@ namespace StardewModdingAPI.Framework /// Provides extension methods for SMAPI's internal use. internal static class InternalExtensions { + /********* + ** Public methods + *********/ /**** ** IMonitor ****/ @@ -54,38 +57,6 @@ namespace StardewModdingAPI.Framework @event.Raise(Singleton.Instance); } - /**** - ** Exceptions - ****/ - /// Get a string representation of an exception suitable for writing to the error log. - /// The error to summarize. - public static string GetLogSummary(this Exception exception) - { - switch (exception) - { - case TypeLoadException ex: - return $"Failed loading type '{ex.TypeName}': {exception}"; - - case ReflectionTypeLoadException ex: - string summary = exception.ToString(); - foreach (Exception childEx in ex.LoaderExceptions) - summary += $"\n\n{childEx.GetLogSummary()}"; - return summary; - - default: - return exception.ToString(); - } - } - - /// Get the lowest exception in an exception stack. - /// The exception from which to search. - public static Exception GetInnermostException(this Exception exception) - { - while (exception.InnerException != null) - exception = exception.InnerException; - return exception; - } - /**** ** ReaderWriterLockSlim ****/ diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs index e16b5c0d..a3d4f23d 100644 --- a/src/SMAPI/Framework/Logging/LogManager.cs +++ b/src/SMAPI/Framework/Logging/LogManager.cs @@ -9,6 +9,7 @@ using System.Threading; using StardewModdingAPI.Framework.Commands; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Internal; using StardewModdingAPI.Internal.ConsoleWriting; using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Utilities; diff --git a/src/SMAPI/Framework/Patching/GamePatcher.cs b/src/SMAPI/Framework/Patching/GamePatcher.cs deleted file mode 100644 index 3ce22ee9..00000000 --- a/src/SMAPI/Framework/Patching/GamePatcher.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using HarmonyLib; - -namespace StardewModdingAPI.Framework.Patching -{ - /// Encapsulates applying Harmony patches to the game. - internal class GamePatcher - { - /********* - ** Fields - *********/ - /// Encapsulates monitoring and logging. - private readonly IMonitor Monitor; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// Encapsulates monitoring and logging. - public GamePatcher(IMonitor monitor) - { - this.Monitor = monitor; - } - - /// Apply all loaded patches to the game. - /// The patches to apply. - public void Apply(params IHarmonyPatch[] patches) - { - Harmony harmony = new Harmony("SMAPI"); - foreach (IHarmonyPatch patch in patches) - { - try - { - patch.Apply(harmony); - } - catch (Exception ex) - { - this.Monitor.Log($"Couldn't apply runtime patch '{patch.GetType().Name}' to the game. Some SMAPI features may not work correctly. See log file for details.", LogLevel.Error); - this.Monitor.Log(ex.GetLogSummary(), LogLevel.Trace); - } - } - } - } -} diff --git a/src/SMAPI/Framework/Patching/IHarmonyPatch.cs b/src/SMAPI/Framework/Patching/IHarmonyPatch.cs deleted file mode 100644 index c1ff3040..00000000 --- a/src/SMAPI/Framework/Patching/IHarmonyPatch.cs +++ /dev/null @@ -1,15 +0,0 @@ -using HarmonyLib; - -namespace StardewModdingAPI.Framework.Patching -{ - /// A Harmony patch to apply. - internal interface IHarmonyPatch - { - /********* - ** Methods - *********/ - /// Apply the Harmony patch. - /// The Harmony instance. - void Apply(Harmony harmony); - } -} diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 35db2da2..a34b3eff 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -31,13 +31,14 @@ using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Networking; -using StardewModdingAPI.Framework.Patching; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Rendering; using StardewModdingAPI.Framework.Serialization; using StardewModdingAPI.Framework.StateTracking.Comparers; using StardewModdingAPI.Framework.StateTracking.Snapshots; using StardewModdingAPI.Framework.Utilities; +using StardewModdingAPI.Internal; +using StardewModdingAPI.Internal.Patching; using StardewModdingAPI.Patches; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; @@ -254,7 +255,7 @@ namespace StardewModdingAPI.Framework // apply game patches MiniMonoModHotfix.Apply(); - new GamePatcher(this.Monitor).Apply( + HarmonyPatcher.Apply("SMAPI", this.Monitor, new Game1Patcher(this.Reflection, this.OnLoadStageChanged), new TitleMenuPatcher(this.OnLoadStageChanged) ); diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index af7fa387..55ab8377 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -11,6 +11,7 @@ using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.StateTracking.Snapshots; using StardewModdingAPI.Framework.Utilities; +using StardewModdingAPI.Internal; using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.BellsAndWhistles; diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 5641f90f..9273864e 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -8,6 +8,7 @@ using Netcode; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Internal; using StardewModdingAPI.Toolkit.Utilities; using StardewValley; using StardewValley.BellsAndWhistles; diff --git a/src/SMAPI/Patches/Game1Patcher.cs b/src/SMAPI/Patches/Game1Patcher.cs index 82b13869..173a2055 100644 --- a/src/SMAPI/Patches/Game1Patcher.cs +++ b/src/SMAPI/Patches/Game1Patcher.cs @@ -2,19 +2,19 @@ using System; using System.Diagnostics.CodeAnalysis; using HarmonyLib; using StardewModdingAPI.Enums; -using StardewModdingAPI.Framework.Patching; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Internal.Patching; using StardewValley; using StardewValley.Menus; using StardewValley.Minigames; namespace StardewModdingAPI.Patches { - /// Harmony patches which notify SMAPI for save creation load stages. + /// Harmony patches for which notify SMAPI for save load stages. /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - internal class Game1Patcher : IHarmonyPatch + internal class Game1Patcher : BasePatcher { /********* ** Fields @@ -42,25 +42,25 @@ namespace StardewModdingAPI.Patches } /// - public void Apply(Harmony harmony) + public override void Apply(Harmony harmony, IMonitor monitor) { // detect CreatedInitialLocations and SaveAddedLocations harmony.Patch( - original: AccessTools.Method(typeof(Game1), nameof(Game1.AddModNPCs)), - prefix: new HarmonyMethod(this.GetType(), nameof(Game1Patcher.Before_Game1_AddModNPCs)) + original: this.RequireMethod(nameof(Game1.AddModNPCs)), + prefix: this.GetHarmonyMethod(nameof(Game1Patcher.Before_AddModNpcs)) ); // detect CreatedLocations, and track IsInLoadForNewGame harmony.Patch( - original: AccessTools.Method(typeof(Game1), nameof(Game1.loadForNewGame)), - prefix: new HarmonyMethod(this.GetType(), nameof(Game1Patcher.Before_Game1_LoadForNewGame)), - postfix: new HarmonyMethod(this.GetType(), nameof(Game1Patcher.After_Game1_LoadForNewGame)) + original: this.RequireMethod(nameof(Game1.loadForNewGame)), + prefix: this.GetHarmonyMethod(nameof(Game1Patcher.Before_LoadForNewGame)), + postfix: this.GetHarmonyMethod(nameof(Game1Patcher.After_LoadForNewGame)) ); // detect ReturningToTitle harmony.Patch( - original: AccessTools.Method(typeof(Game1), nameof(Game1.CleanupReturningToTitle)), - prefix: new HarmonyMethod(this.GetType(), nameof(Game1Patcher.Before_Game1_CleanupReturningToTitle)) + original: this.RequireMethod(nameof(Game1.CleanupReturningToTitle)), + prefix: this.GetHarmonyMethod(nameof(Game1Patcher.Before_CleanupReturningToTitle)) ); } @@ -68,10 +68,10 @@ namespace StardewModdingAPI.Patches /********* ** Private methods *********/ - /// Called before . + /// The method to call before . /// Returns whether to execute the original method. /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. - private static bool Before_Game1_AddModNPCs() + private static bool Before_AddModNpcs() { // When this method is called from Game1.loadForNewGame, it happens right after adding the vanilla // locations but before initializing them. @@ -86,27 +86,27 @@ namespace StardewModdingAPI.Patches return true; } - /// Called before . + /// The method to call before . /// Returns whether to execute the original method. /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. - private static bool Before_Game1_CleanupReturningToTitle() + private static bool Before_CleanupReturningToTitle() { Game1Patcher.OnStageChanged(LoadStage.ReturningToTitle); return true; } - /// Called before . + /// The method to call before . /// Returns whether to execute the original method. /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. - private static bool Before_Game1_LoadForNewGame() + private static bool Before_LoadForNewGame() { Game1Patcher.IsInLoadForNewGame = true; return true; } - /// Called after . + /// The method to call after . /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. - private static void After_Game1_LoadForNewGame() + private static void After_LoadForNewGame() { Game1Patcher.IsInLoadForNewGame = false; diff --git a/src/SMAPI/Patches/TitleMenuPatcher.cs b/src/SMAPI/Patches/TitleMenuPatcher.cs index a889adfc..b4320ce0 100644 --- a/src/SMAPI/Patches/TitleMenuPatcher.cs +++ b/src/SMAPI/Patches/TitleMenuPatcher.cs @@ -2,16 +2,16 @@ using System; using System.Diagnostics.CodeAnalysis; using HarmonyLib; using StardewModdingAPI.Enums; -using StardewModdingAPI.Framework.Patching; +using StardewModdingAPI.Internal.Patching; using StardewValley.Menus; namespace StardewModdingAPI.Patches { - /// Harmony patches which notify SMAPI for save creation load stages. + /// Harmony patches for which notify SMAPI when a new character was created. /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - internal class TitleMenuPatcher : IHarmonyPatch + internal class TitleMenuPatcher : BasePatcher { /********* ** Fields @@ -31,12 +31,11 @@ namespace StardewModdingAPI.Patches } /// - public void Apply(Harmony harmony) + public override void Apply(Harmony harmony, IMonitor monitor) { - // detect CreatedBasicInfo harmony.Patch( - original: AccessTools.Method(typeof(TitleMenu), nameof(TitleMenu.createdNewCharacter)), - prefix: new HarmonyMethod(this.GetType(), nameof(TitleMenuPatcher.Before_TitleMenu_CreatedNewCharacter)) + original: this.RequireMethod(nameof(TitleMenu.createdNewCharacter)), + prefix: this.GetHarmonyMethod(nameof(TitleMenuPatcher.Before_CreatedNewCharacter)) ); } @@ -44,10 +43,10 @@ namespace StardewModdingAPI.Patches /********* ** Private methods *********/ - /// Called before . + /// The method to call before . /// Returns whether to execute the original method. /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. - private static bool Before_TitleMenu_CreatedNewCharacter() + private static bool Before_CreatedNewCharacter() { TitleMenuPatcher.OnStageChanged(LoadStage.CreatedBasicInfo); return true; diff --git a/src/SMAPI/Properties/AssemblyInfo.cs b/src/SMAPI/Properties/AssemblyInfo.cs index ae758e9b..ee8a1674 100644 --- a/src/SMAPI/Properties/AssemblyInfo.cs +++ b/src/SMAPI/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("SMAPI.Tests")] -[assembly: InternalsVisibleTo("ErrorHandler")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Moq for unit testing diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 0c6cfdd3..7d5e7ef9 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -76,4 +76,5 @@ +