diff options
Diffstat (limited to 'src/SMAPI.Mods.ErrorHandler/Patches')
16 files changed, 637 insertions, 861 deletions
diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/DialogueErrorPatch.cs b/src/SMAPI.Mods.ErrorHandler/Patches/DialogueErrorPatch.cs deleted file mode 100644 index cce13064..00000000 --- a/src/SMAPI.Mods.ErrorHandler/Patches/DialogueErrorPatch.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using StardewModdingAPI.Framework.Patching; -using StardewValley; -#if HARMONY_2 -using HarmonyLib; -using StardewModdingAPI.Framework; -#else -using System.Reflection; -using Harmony; -#endif - -namespace StardewModdingAPI.Mods.ErrorHandler.Patches -{ - /// <summary>A Harmony patch for the <see cref="Dialogue"/> constructor which intercepts invalid dialogue lines and logs an error instead of crashing.</summary> - /// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks> - [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 DialogueErrorPatch : IHarmonyPatch - { - /********* - ** Fields - *********/ - /// <summary>Writes messages to the console and log file on behalf of the game.</summary> - private static IMonitor MonitorForGame; - - /// <summary>Simplifies access to private code.</summary> - private static IReflectionHelper Reflection; - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="monitorForGame">Writes messages to the console and log file on behalf of the game.</param> - /// <param name="reflector">Simplifies access to private code.</param> - public DialogueErrorPatch(IMonitor monitorForGame, IReflectionHelper reflector) - { - DialogueErrorPatch.MonitorForGame = monitorForGame; - DialogueErrorPatch.Reflection = reflector; - } - - - /// <inheritdoc /> -#if HARMONY_2 - public void Apply(Harmony harmony) - { - harmony.Patch( - original: AccessTools.Constructor(typeof(Dialogue), new[] { typeof(string), typeof(NPC) }), - finalizer: new HarmonyMethod(this.GetType(), nameof(DialogueErrorPatch.Finalize_Dialogue_Constructor)) - ); - harmony.Patch( - original: AccessTools.Property(typeof(NPC), nameof(NPC.CurrentDialogue)).GetMethod, - finalizer: new HarmonyMethod(this.GetType(), nameof(DialogueErrorPatch.Finalize_NPC_CurrentDialogue)) - ); - } -#else - public void Apply(HarmonyInstance harmony) - { - harmony.Patch( - original: AccessTools.Constructor(typeof(Dialogue), new[] { typeof(string), typeof(NPC) }), - prefix: new HarmonyMethod(this.GetType(), nameof(DialogueErrorPatch.Before_Dialogue_Constructor)) - ); - harmony.Patch( - original: AccessTools.Property(typeof(NPC), nameof(NPC.CurrentDialogue)).GetMethod, - prefix: new HarmonyMethod(this.GetType(), nameof(DialogueErrorPatch.Before_NPC_CurrentDialogue)) - ); - } -#endif - - - /********* - ** Private methods - *********/ -#if HARMONY_2 - /// <summary>The method to call after the Dialogue constructor.</summary> - /// <param name="__instance">The instance being patched.</param> - /// <param name="masterDialogue">The dialogue being parsed.</param> - /// <param name="speaker">The NPC for which the dialogue is being parsed.</param> - /// <param name="__exception">The exception thrown by the wrapped method, if any.</param> - /// <returns>Returns the exception to throw, if any.</returns> - private static Exception Finalize_Dialogue_Constructor(Dialogue __instance, string masterDialogue, NPC speaker, Exception __exception) - { - if (__exception != null) - { - // log message - string name = !string.IsNullOrWhiteSpace(speaker?.Name) ? speaker.Name : null; - DialogueErrorPatch.MonitorForGame.Log($"Failed parsing dialogue string{(name != null ? $" for {name}" : "")}:\n{masterDialogue}\n{__exception.GetLogSummary()}", LogLevel.Error); - - // set default dialogue - IReflectedMethod parseDialogueString = DialogueErrorPatch.Reflection.GetMethod(__instance, "parseDialogueString"); - IReflectedMethod checkForSpecialDialogueAttributes = DialogueErrorPatch.Reflection.GetMethod(__instance, "checkForSpecialDialogueAttributes"); - parseDialogueString.Invoke("..."); - checkForSpecialDialogueAttributes.Invoke(); - } - - return null; - } - - /// <summary>The method to call after <see cref="NPC.CurrentDialogue"/>.</summary> - /// <param name="__instance">The instance being patched.</param> - /// <param name="__result">The return value of the original method.</param> - /// <param name="__exception">The exception thrown by the wrapped method, if any.</param> - /// <returns>Returns the exception to throw, if any.</returns> - private static Exception Finalize_NPC_CurrentDialogue(NPC __instance, ref Stack<Dialogue> __result, Exception __exception) - { - if (__exception == null) - return null; - - DialogueErrorPatch.MonitorForGame.Log($"Failed loading current dialogue for NPC {__instance.Name}:\n{__exception.GetLogSummary()}", LogLevel.Error); - __result = new Stack<Dialogue>(); - - return null; - } -#else - - /// <summary>The method to call instead of the Dialogue constructor.</summary> - /// <param name="__instance">The instance being patched.</param> - /// <param name="masterDialogue">The dialogue being parsed.</param> - /// <param name="speaker">The NPC for which the dialogue is being parsed.</param> - /// <returns>Returns whether to execute the original method.</returns> - private static bool Before_Dialogue_Constructor(Dialogue __instance, string masterDialogue, NPC speaker) - { - // get private members - bool nameArraysTranslated = DialogueErrorPatch.Reflection.GetField<bool>(typeof(Dialogue), "nameArraysTranslated").GetValue(); - IReflectedMethod translateArraysOfStrings = DialogueErrorPatch.Reflection.GetMethod(typeof(Dialogue), "TranslateArraysOfStrings"); - IReflectedMethod parseDialogueString = DialogueErrorPatch.Reflection.GetMethod(__instance, "parseDialogueString"); - IReflectedMethod checkForSpecialDialogueAttributes = DialogueErrorPatch.Reflection.GetMethod(__instance, "checkForSpecialDialogueAttributes"); - - // replicate base constructor - __instance.dialogues ??= new List<string>(); - - // duplicate code with try..catch - try - { - if (!nameArraysTranslated) - translateArraysOfStrings.Invoke(); - __instance.speaker = speaker; - parseDialogueString.Invoke(masterDialogue); - checkForSpecialDialogueAttributes.Invoke(); - } - catch (Exception baseEx) when (baseEx.InnerException is TargetInvocationException invocationEx && invocationEx.InnerException is Exception ex) - { - string name = !string.IsNullOrWhiteSpace(speaker?.Name) ? speaker.Name : null; - DialogueErrorPatch.MonitorForGame.Log($"Failed parsing dialogue string{(name != null ? $" for {name}" : "")}:\n{masterDialogue}\n{ex}", LogLevel.Error); - - parseDialogueString.Invoke("..."); - checkForSpecialDialogueAttributes.Invoke(); - } - - return false; - } - - /// <summary>The method to call instead of <see cref="NPC.CurrentDialogue"/>.</summary> - /// <param name="__instance">The instance being patched.</param> - /// <param name="__result">The return value of the original method.</param> - /// <param name="__originalMethod">The method being wrapped.</param> - /// <returns>Returns whether to execute the original method.</returns> - private static bool Before_NPC_CurrentDialogue(NPC __instance, ref Stack<Dialogue> __result, MethodInfo __originalMethod) - { - const string key = nameof(DialogueErrorPatch.Before_NPC_CurrentDialogue); - if (!PatchHelper.StartIntercept(key)) - return true; - - try - { - __result = (Stack<Dialogue>)__originalMethod.Invoke(__instance, new object[0]); - return false; - } - catch (TargetInvocationException ex) - { - DialogueErrorPatch.MonitorForGame.Log($"Failed loading current dialogue for NPC {__instance.Name}:\n{ex.InnerException ?? ex}", LogLevel.Error); - __result = new Stack<Dialogue>(); - return false; - } - finally - { - PatchHelper.StopIntercept(key); - } - } -#endif - } -} diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/DialoguePatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/DialoguePatcher.cs new file mode 100644 index 00000000..7a3af39c --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/Patches/DialoguePatcher.cs @@ -0,0 +1,75 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using HarmonyLib; +using StardewModdingAPI.Internal; +using StardewModdingAPI.Internal.Patching; +using StardewValley; + +namespace StardewModdingAPI.Mods.ErrorHandler.Patches +{ + /// <summary>Harmony patches for <see cref="Dialogue"/> which intercept invalid dialogue lines and logs an error instead of crashing.</summary> + /// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks> + [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 : BasePatcher + { + /********* + ** Fields + *********/ + /// <summary>Writes messages to the console and log file on behalf of the game.</summary> + private static IMonitor MonitorForGame; + + /// <summary>Simplifies access to private code.</summary> + private static IReflectionHelper Reflection; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="monitorForGame">Writes messages to the console and log file on behalf of the game.</param> + /// <param name="reflector">Simplifies access to private code.</param> + public DialoguePatcher(IMonitor monitorForGame, IReflectionHelper reflector) + { + DialoguePatcher.MonitorForGame = monitorForGame; + DialoguePatcher.Reflection = reflector; + } + + /// <inheritdoc /> + public override void Apply(Harmony harmony, IMonitor monitor) + { + harmony.Patch( + original: this.RequireConstructor<Dialogue>(typeof(string), typeof(NPC)), + finalizer: this.GetHarmonyMethod(nameof(DialoguePatcher.Finalize_Constructor)) + ); + } + + + /********* + ** Private methods + *********/ + /// <summary>The method to call when the Dialogue constructor throws an exception.</summary> + /// <param name="__instance">The instance being patched.</param> + /// <param name="masterDialogue">The dialogue being parsed.</param> + /// <param name="speaker">The NPC for which the dialogue is being parsed.</param> + /// <param name="__exception">The exception thrown by the wrapped method, if any.</param> + /// <returns>Returns the exception to throw, if any.</returns> + private static Exception Finalize_Constructor(Dialogue __instance, string masterDialogue, NPC speaker, Exception __exception) + { + if (__exception != null) + { + // log message + string name = !string.IsNullOrWhiteSpace(speaker?.Name) ? speaker.Name : null; + DialoguePatcher.MonitorForGame.Log($"Failed parsing dialogue string{(name != null ? $" for {name}" : "")}:\n{masterDialogue}\n{__exception.GetLogSummary()}", LogLevel.Error); + + // set default dialogue + IReflectedMethod parseDialogueString = DialoguePatcher.Reflection.GetMethod(__instance, "parseDialogueString"); + IReflectedMethod checkForSpecialDialogueAttributes = DialoguePatcher.Reflection.GetMethod(__instance, "checkForSpecialDialogueAttributes"); + parseDialogueString.Invoke("..."); + checkForSpecialDialogueAttributes.Invoke(); + } + + return null; + } + } +} diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/DictionaryPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/DictionaryPatcher.cs new file mode 100644 index 00000000..6ad64e16 --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/Patches/DictionaryPatcher.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using HarmonyLib; +using StardewModdingAPI.Internal.Patching; +using StardewValley.GameData; +using StardewValley.GameData.HomeRenovations; +using StardewValley.GameData.Movies; + +namespace StardewModdingAPI.Mods.ErrorHandler.Patches +{ + /// <summary>Harmony patches for <see cref="Dictionary{TKey,TValue}"/> which add the accessed key to <see cref="KeyNotFoundException"/> exceptions.</summary> + /// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks> + [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 : BasePatcher + { + /********* + ** Fields + *********/ + /// <summary>Simplifies access to private code.</summary> + private static IReflectionHelper Reflection; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="reflector">Simplifies access to private code.</param> + public DictionaryPatcher(IReflectionHelper reflector) + { + DictionaryPatcher.Reflection = reflector; + } + + /// <inheritdoc /> + 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) }; + + foreach (Type keyType in keyTypes) + { + foreach (Type valueType in valueTypes) + { + Type dictionaryType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType); + + harmony.Patch( + 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)) + ); + } + } + } + + + /********* + ** Private methods + *********/ + /// <summary>The method to call after the dictionary indexer throws an exception.</summary> + /// <param name="key">The dictionary key being fetched.</param> + /// <param name="__exception">The exception thrown by the wrapped method, if any.</param> + /// <returns>Returns the exception to throw, if any.</returns> + private static Exception Finalize_GetItem(object key, Exception __exception) + { + if (__exception is KeyNotFoundException) + { + DictionaryPatcher.Reflection + .GetField<string>(__exception, "_message") + .SetValue($"{__exception.Message}\nkey: '{key}'"); + } + + return __exception; + } + } +} diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/EventPatches.cs b/src/SMAPI.Mods.ErrorHandler/Patches/EventPatcher.cs index 72863d17..1b706147 100644 --- a/src/SMAPI.Mods.ErrorHandler/Patches/EventPatches.cs +++ b/src/SMAPI.Mods.ErrorHandler/Patches/EventPatcher.cs @@ -1,11 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; -#if HARMONY_2 using HarmonyLib; -#else -using Harmony; -#endif -using StardewModdingAPI.Framework.Patching; +using StardewModdingAPI.Internal.Patching; using StardewValley; namespace StardewModdingAPI.Mods.ErrorHandler.Patches @@ -14,7 +10,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches /// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks> [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 EventPatches : IHarmonyPatch + internal class EventPatcher : BasePatcher { /********* ** Fields @@ -28,21 +24,17 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches *********/ /// <summary>Construct an instance.</summary> /// <param name="monitorForGame">Writes messages to the console and log file on behalf of the game.</param> - public EventPatches(IMonitor monitorForGame) + public EventPatcher(IMonitor monitorForGame) { - EventPatches.MonitorForGame = monitorForGame; + EventPatcher.MonitorForGame = monitorForGame; } /// <inheritdoc /> -#if HARMONY_2 - public void Apply(Harmony harmony) -#else - public void Apply(HarmonyInstance harmony) -#endif + public override void Apply(Harmony harmony, IMonitor monitor) { harmony.Patch( - original: AccessTools.Method(typeof(Event), nameof(Event.LogErrorAndHalt)), - postfix: new HarmonyMethod(this.GetType(), nameof(EventPatches.After_Event_LogErrorAndHalt)) + original: this.RequireMethod<Event>(nameof(Event.LogErrorAndHalt)), + postfix: this.GetHarmonyMethod(nameof(EventPatcher.After_LogErrorAndHalt)) ); } @@ -52,9 +44,9 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches *********/ /// <summary>The method to call after <see cref="Event.LogErrorAndHalt"/>.</summary> /// <param name="e">The exception being logged.</param> - private static void After_Event_LogErrorAndHalt(Exception e) + private static void After_LogErrorAndHalt(Exception e) { - EventPatches.MonitorForGame.Log(e.ToString(), LogLevel.Error); + 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 new file mode 100644 index 00000000..7df6b0a2 --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatcher.cs @@ -0,0 +1,79 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using HarmonyLib; +using StardewModdingAPI.Internal.Patching; +using StardewValley; +using xTile; + +namespace StardewModdingAPI.Mods.ErrorHandler.Patches +{ + /// <summary>Harmony patches for <see cref="GameLocation"/> which intercept errors instead of crashing.</summary> + /// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks> + [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 : BasePatcher + { + /********* + ** Fields + *********/ + /// <summary>Writes messages to the console and log file on behalf of the game.</summary> + private static IMonitor MonitorForGame; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="monitorForGame">Writes messages to the console and log file on behalf of the game.</param> + public GameLocationPatcher(IMonitor monitorForGame) + { + GameLocationPatcher.MonitorForGame = monitorForGame; + } + + /// <inheritdoc /> + public override void Apply(Harmony harmony, IMonitor monitor) + { + harmony.Patch( + original: this.RequireMethod<GameLocation>(nameof(GameLocation.checkEventPrecondition)), + finalizer: this.GetHarmonyMethod(nameof(GameLocationPatcher.Finalize_CheckEventPrecondition)) + ); + harmony.Patch( + original: this.RequireMethod<GameLocation>(nameof(GameLocation.updateSeasonalTileSheets)), + finalizer: this.GetHarmonyMethod(nameof(GameLocationPatcher.Finalize_UpdateSeasonalTileSheets)) + ); + } + + + /********* + ** Private methods + *********/ + /// <summary>The method to call when <see cref="GameLocation.checkEventPrecondition"/> throws an exception.</summary> + /// <param name="__result">The return value of the original method.</param> + /// <param name="precondition">The precondition to be parsed.</param> + /// <param name="__exception">The exception thrown by the wrapped method, if any.</param> + /// <returns>Returns the exception to throw, if any.</returns> + private static Exception Finalize_CheckEventPrecondition(ref int __result, string precondition, Exception __exception) + { + if (__exception != null) + { + __result = -1; + GameLocationPatcher.MonitorForGame.Log($"Failed parsing event precondition ({precondition}):\n{__exception.InnerException}", LogLevel.Error); + } + + return null; + } + + /// <summary>The method to call when <see cref="GameLocation.updateSeasonalTileSheets"/> throws an exception.</summary> + /// <param name="__instance">The instance being patched.</param> + /// <param name="map">The map whose tilesheets to update.</param> + /// <param name="__exception">The exception thrown by the wrapped method, if any.</param> + /// <returns>Returns the exception to throw, if any.</returns> + 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); + + return null; + } + } +} diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatches.cs b/src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatches.cs deleted file mode 100644 index 7a48133e..00000000 --- a/src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatches.cs +++ /dev/null @@ -1,158 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -#if HARMONY_2 -using System; -using HarmonyLib; -#else -using System.Reflection; -using Harmony; -#endif -using StardewModdingAPI.Framework.Patching; -using StardewValley; -using xTile; - -namespace StardewModdingAPI.Mods.ErrorHandler.Patches -{ - /// <summary>Harmony patches for <see cref="GameLocation.checkEventPrecondition"/> and <see cref="GameLocation.updateSeasonalTileSheets"/> which intercept errors instead of crashing.</summary> - /// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks> - [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 GameLocationPatches : IHarmonyPatch - { - /********* - ** Fields - *********/ - /// <summary>Writes messages to the console and log file on behalf of the game.</summary> - private static IMonitor MonitorForGame; - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="monitorForGame">Writes messages to the console and log file on behalf of the game.</param> - public GameLocationPatches(IMonitor monitorForGame) - { - GameLocationPatches.MonitorForGame = monitorForGame; - } - - /// <inheritdoc /> -#if HARMONY_2 - public void Apply(Harmony harmony) - { - harmony.Patch( - original: AccessTools.Method(typeof(GameLocation), nameof(GameLocation.checkEventPrecondition)), - finalizer: new HarmonyMethod(this.GetType(), nameof(EventErrorPatch.Finalize_GameLocation_CheckEventPrecondition)) - ); -harmony.Patch( - original: AccessTools.Method(typeof(GameLocation), nameof(GameLocation.updateSeasonalTileSheets)), - finalizer: new HarmonyMethod(this.GetType(), nameof(EventErrorPatch.Before_GameLocation_UpdateSeasonalTileSheets)) - ); - } -#else - public void Apply(HarmonyInstance harmony) - { - harmony.Patch( - original: AccessTools.Method(typeof(GameLocation), nameof(GameLocation.checkEventPrecondition)), - prefix: new HarmonyMethod(this.GetType(), nameof(GameLocationPatches.Before_GameLocation_CheckEventPrecondition)) - ); - harmony.Patch( - original: AccessTools.Method(typeof(GameLocation), nameof(GameLocation.updateSeasonalTileSheets)), - prefix: new HarmonyMethod(this.GetType(), nameof(GameLocationPatches.Before_GameLocation_UpdateSeasonalTileSheets)) - ); - } -#endif - - - /********* - ** Private methods - *********/ -#if HARMONY_2 - /// <summary>The method to call instead of GameLocation.checkEventPrecondition.</summary> - /// <param name="__result">The return value of the original method.</param> - /// <param name="precondition">The precondition to be parsed.</param> - /// <param name="__exception">The exception thrown by the wrapped method, if any.</param> - /// <returns>Returns the exception to throw, if any.</returns> - private static Exception Finalize_GameLocation_CheckEventPrecondition(ref int __result, string precondition, Exception __exception) - { - if (__exception != null) - { - __result = -1; - EventErrorPatch.MonitorForGame.Log($"Failed parsing event precondition ({precondition}):\n{__exception.InnerException}", LogLevel.Error); - } - - return null; - } -#else - /// <summary>The method to call instead of <see cref="GameLocation.checkEventPrecondition"/>.</summary> - /// <param name="__instance">The instance being patched.</param> - /// <param name="__result">The return value of the original method.</param> - /// <param name="precondition">The precondition to be parsed.</param> - /// <param name="__originalMethod">The method being wrapped.</param> - /// <returns>Returns whether to execute the original method.</returns> - private static bool Before_GameLocation_CheckEventPrecondition(GameLocation __instance, ref int __result, string precondition, MethodInfo __originalMethod) - { - const string key = nameof(GameLocationPatches.Before_GameLocation_CheckEventPrecondition); - if (!PatchHelper.StartIntercept(key)) - return true; - - try - { - __result = (int)__originalMethod.Invoke(__instance, new object[] { precondition }); - return false; - } - catch (TargetInvocationException ex) - { - __result = -1; - GameLocationPatches.MonitorForGame.Log($"Failed parsing event precondition ({precondition}):\n{ex.InnerException}", LogLevel.Error); - return false; - } - finally - { - PatchHelper.StopIntercept(key); - } - } -#endif - -#if HARMONY_2 - /// <summary>The method to call instead of <see cref="GameLocation.updateSeasonalTileSheets"/>.</summary> - /// <param name="__instance">The instance being patched.</param> - /// <param name="map">The map whose tilesheets to update.</param> - /// <param name="__exception">The exception thrown by the wrapped method, if any.</param> - /// <returns>Returns the exception to throw, if any.</returns> - private static Exception Before_GameLocation_UpdateSeasonalTileSheets(GameLocation __instance, Map map, Exception __exception) - { - if (__exception != null) - GameLocationPatches.MonitorForGame.Log($"Failed updating seasonal tilesheets for location '{__instance.NameOrUniqueName}': \n{__exception}", LogLevel.Error); - - return null; - } -#else - /// <summary>The method to call instead of <see cref="GameLocation.updateSeasonalTileSheets"/>.</summary> - /// <param name="__instance">The instance being patched.</param> - /// <param name="map">The map whose tilesheets to update.</param> - /// <param name="__originalMethod">The method being wrapped.</param> - /// <returns>Returns whether to execute the original method.</returns> - private static bool Before_GameLocation_UpdateSeasonalTileSheets(GameLocation __instance, Map map, MethodInfo __originalMethod) - { - const string key = nameof(GameLocationPatches.Before_GameLocation_UpdateSeasonalTileSheets); - if (!PatchHelper.StartIntercept(key)) - return true; - - try - { - __originalMethod.Invoke(__instance, new object[] { map }); - return false; - } - catch (TargetInvocationException ex) - { - GameLocationPatches.MonitorForGame.Log($"Failed updating seasonal tilesheets for location '{__instance.NameOrUniqueName}'. Technical details:\n{ex.InnerException}", LogLevel.Error); - return false; - } - finally - { - PatchHelper.StopIntercept(key); - } - } -#endif - } -} diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/IClickableMenuPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/IClickableMenuPatcher.cs new file mode 100644 index 00000000..b65a695a --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/Patches/IClickableMenuPatcher.cs @@ -0,0 +1,44 @@ +using System.Diagnostics.CodeAnalysis; +using HarmonyLib; +using StardewModdingAPI.Internal.Patching; +using StardewValley; +using StardewValley.Menus; +using SObject = StardewValley.Object; + +namespace StardewModdingAPI.Mods.ErrorHandler.Patches +{ + /// <summary>Harmony patches for <see cref="IClickableMenu"/> which intercept crashes due to invalid items.</summary> + /// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks> + [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 : BasePatcher + { + /********* + ** Public methods + *********/ + /// <inheritdoc /> + public override void Apply(Harmony harmony, IMonitor monitor) + { + harmony.Patch( + original: this.RequireMethod<IClickableMenu>(nameof(IClickableMenu.drawToolTip)), + prefix: this.GetHarmonyMethod(nameof(IClickableMenuPatcher.Before_DrawTooltip)) + ); + } + + + /********* + ** Private methods + *********/ + /// <summary>The method to call instead of <see cref="IClickableMenu.drawToolTip"/>.</summary> + /// <param name="hoveredItem">The item for which to draw a tooltip.</param> + /// <returns>Returns whether to execute the original method.</returns> + 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)) + return false; + + return true; + } + } +} diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/LoadErrorPatch.cs b/src/SMAPI.Mods.ErrorHandler/Patches/LoadErrorPatch.cs deleted file mode 100644 index 52d5f5a1..00000000 --- a/src/SMAPI.Mods.ErrorHandler/Patches/LoadErrorPatch.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.Co |
