summaryrefslogtreecommitdiff
path: root/src/SMAPI.Mods.ErrorHandler/Patches
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI.Mods.ErrorHandler/Patches')
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/DialogueErrorPatch.cs184
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/DialoguePatcher.cs75
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/DictionaryPatcher.cs75
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/EventPatcher.cs (renamed from src/SMAPI.Mods.ErrorHandler/Patches/EventPatches.cs)26
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatcher.cs79
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatches.cs158
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/IClickableMenuPatcher.cs44
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/LoadErrorPatch.cs150
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/NpcPatcher.cs85
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/ObjectErrorPatch.cs136
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/ObjectPatcher.cs71
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs145
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/ScheduleErrorPatch.cs108
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchPatcher.cs (renamed from src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchValidationPatches.cs)30
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/UtilityErrorPatches.cs89
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/UtilityPatcher.cs43
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.CodeAnalysis;
-using System.Linq;
-#if HARMONY_2
-using HarmonyLib;
-#else
-using Harmony;
-#endif
-using StardewModdingAPI.Framework.Exceptions;
-using StardewModdingAPI.Framework.Patching;
-using StardewValley;
-using StardewValley.Buildings;
-using StardewValley.Locations;
-
-namespace StardewModdingAPI.Mods.ErrorHandler.Patches
-{
- /// <summary>A Harmony patch for <see cref="SaveGame"/> which prevents some errors due to broken save data.</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 LoadErrorPatch : IHarmonyPatch
- {
- /*********
- ** Fields
- *********/
- /// <summary>Writes messages to the console and log file.</summary>
- private static IMonitor Monitor;
-
- /// <summary>A callback invoked when custom content is removed from the save data to avoid a crash.</summary>
- private static Action OnContentRemoved;
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Construct an instance.</summary>
- /// <param name="monitor">Writes messages to the console and log file.</param>
- /// <param name="onContentRemoved">A callback invoked when custom content is removed from the save data to avoid a crash.</param>
- public LoadErrorPatch(IMonitor monitor, Action onContentRemoved)
- {
- LoadErrorPatch.Monitor = monitor;
- LoadErrorPatch.OnContentRemoved = onContentRemoved;
- }
-
-
- /// <inheritdoc />
-#if HARMONY_2
- public void Apply(Harmony harmony)
-#else
- public void Apply(HarmonyInstance harmony)
-#endif
- {
- harmony.Patch(
- original: AccessTools.Method(typeof(SaveGame), nameof(SaveGame.loadDataToLocations)),
- prefix: new HarmonyMethod(this.GetType(), nameof(LoadErrorPatch.Before_SaveGame_LoadDataToLocations))
- );
- }
-
-
- /*********
- ** Private methods
- *********/
- /// <summary>The method to call instead of <see cref="SaveGame.loadDataToLocations"/>.</summary>
- /// <param name="gamelocations">The game locations being loaded.</param>
- /// <returns>Returns whether to execute the original method.</returns>
- private static bool Before_SaveGame_LoadDataToLocations(List<GameLocation> gamelocations)
- {
- bool removedAny =
- LoadErrorPatch.RemoveBrokenBuildings(gamelocations)
- | LoadErrorPatch.RemoveInvalidNpcs(gamelocations);
-
- if (removedAny)
- LoadErrorPatch.OnContentRemoved();
-
- return true;
- }
-
- /// <summary>Remove buildings which don't exist in the game data.</summary>
- /// <param name="locations">The current game locations.</param>
- private static bool RemoveBrokenBuildings(IEnumerable<GameLocation> locations)
- {
- bool removedAny = false;
-
- foreach (BuildableGameLocation location in locations.OfType<BuildableGameLocation>())
- {
- foreach (Building building in location.buildings.ToArray())
- {
- try
- {
- BluePrint _ = new BluePrint(building.buildingType.Value);
- }
- catch (SContentLoadException)
- {
- LoadErrorPatch.Monitor.Log($"Removed invalid building type '{building.buildingType.Value}' in {location.Name} ({building.tileX}, {building.tileY}) to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom building mod?)", LogLevel.Warn);
- location.buildings.Remove(building);
- removedAny = true;
- }
- }
- }
-
- return removedAny;
- }
-
- /// <summary>Remove NPCs which don't exist in the game data.</summary>
- /// <param name="locations">The current game locations.</param>
- private static bool RemoveInvalidNpcs(IEnumerable<GameLocation> locations)
- {
- bool removedAny = false;
-
- IDictionary<string, string> data = Game1.content.Load<Dictionary<string, string>>("Data\\NPCDispositions");
- foreach (GameLocation location in LoadErrorPatch.GetAllLocations(locations))
- {
- foreach (NPC npc in location.characters.ToArray())
- {
- if (npc.isVillager() && !data.ContainsKey(npc.Name))
- {
- try
- {
- npc.reloadSprite(); // this won't crash for special villagers like Bouncer
- }
- catch
- {
- LoadErrorPatch.Monitor.Log($"Removed invalid villager '{npc.Name}' in {location.Name} ({npc.getTileLocation()}) to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom NPC mod?)", LogLevel.Warn);
- location.characters.Remove(npc);
- removedAny = true;
- }
- }
- }
- }
-
- return removedAny;
- }
-
- /// <summary>Get all locations, including building interiors.</summary>
- /// <param name="locations">The main game locations.</param>
- private static IEnumerable<GameLocation> GetAllLocations(IEnumerable<GameLocation> locations)
- {
- foreach (GameLocation location in locations)
- {
- yield return location;
- if (location is BuildableGameLocation buildableLocation)
- {
- foreach (GameLocation interior in buildableLocation.buildings.Select(p => p.indoors.Value).Where(p => p != null))
- yield return interior;
- }
- }
- }
- }
-}
diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/NpcPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/NpcPatcher.cs
new file mode 100644
index 00000000..275bb5bf
--- /dev/null
+++ b/src/SMAPI.Mods.ErrorHandler/Patches/NpcPatcher.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Collections.Generic;
+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="NPC"/> which intercept crashes due to invalid schedule data.</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 NpcPatcher : 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 NpcPatcher(IMonitor monitorForGame)
+ {
+ NpcPatcher.MonitorForGame = monitorForGame;
+ }
+
+ /// <inheritdoc />
+ public override void Apply(Harmony harmony, IMonitor monitor)
+ {
+ harmony.Patch(
+ original: this.RequireMethod<NPC>($"get_{nameof(NPC.CurrentDialogue)}"),
+ finalizer: this.GetHarmonyMethod(nameof(NpcPatcher.Finalize_CurrentDialogue))
+ );
+
+ harmony.Patch(
+ original: this.RequireMethod<NPC>(nameof(NPC.parseMasterSchedule)),
+ finalizer: this.GetHarmonyMethod(nameof(NpcPatcher.Finalize_ParseMasterSchedule))
+ );
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>The method to call when <see cref="NPC.CurrentDialogue"/> throws an exception.</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_CurrentDialogue(NPC __instance, ref Stack<Dialogue> __result, Exception __exception)
+ {
+ if (__exception == null)
+ return null;
+
+ NpcPatcher.MonitorForGame.Log($"Failed loading current dialogue for NPC {__instance.Name}:\n{__exception.GetLogSummary()}", LogLevel.Error);
+ __result = new Stack<Dialogue>();
+
+ return null;
+ }
+
+ /// <summary>The method to call instead of <see cref="NPC.parseMasterSchedule"/>.</summary>
+ /// <param name="rawData">The raw schedule data to parse.</param>
+ /// <param name="__instance">The instance being patched.</param>
+ /// <param name="__result">The patched method's return value.</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_ParseMasterSchedule(string rawData, NPC __instance, ref Dictionary<int, SchedulePathDescription> __result, Exception __exception)
+ {
+ if (__exception != null)
+ {
+ NpcPatcher.MonitorForGame.Log($"Failed parsing schedule for NPC {__instance.Name}:\n{rawData}\n{__exception.GetLogSummary()}", LogLevel.Error);
+ __result = new Dictionary<int, SchedulePathDescription>();
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/ObjectErrorPatch.cs b/src/SMAPI.Mods.ErrorHandler/Patches/ObjectErrorPatch.cs
deleted file mode 100644
index 9f8a98cd..00000000
--- a/src/SMAPI.Mods.ErrorHandler/Patches/ObjectErrorPatch.cs
+++ /dev/null
@@ -1,136 +0,0 @@
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using StardewModdingAPI.Framework.Patching;
-using StardewValley;
-using StardewValley.Menus;
-using SObject = StardewValley.Object;
-#if HARMONY_2
-using System;
-using HarmonyLib;
-#else
-using System.Reflection;
-using Harmony;
-#endif
-
-namespace StardewModdingAPI.Mods.ErrorHandler.Patches
-{
- /// <summary>A Harmony patch for <see cref="SObject.getDescription"/> which intercepts crashes due to the item no longer existing.</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 ObjectErrorPatch : IHarmonyPatch
- {
- /*********
- ** Public methods
- *********/
- /// <inheritdoc />
-#if HARMONY_2
- public void Apply(Harmony harmony)
-#else
- public void Apply(HarmonyInstance harmony)
-#endif
- {
- // object.getDescription
- harmony.Patch(
- original: AccessTools.Method(typeof(SObject), nameof(SObject.getDescription)),
- prefix: new HarmonyMethod(this.GetType(), nameof(ObjectErrorPatch.Before_Object_GetDescription))
- );
-
- // object.getDisplayName
- harmony.Patch(
- original: AccessTools.Method(typeof(SObject), "loadDisplayName"),
-#if HARMONY_2
- finalizer: new HarmonyMethod(this.GetType(), nameof(ObjectErrorPatch.Finalize_Object_loadDisplayName))
-#else
- prefix: new HarmonyMethod(this.GetType(), nameof(ObjectErrorPatch.Before_Object_loadDisplayName))
-#endif
- );
-
- // IClickableMenu.drawToolTip
- harmony.Patch(
- original: AccessTools.Method(typeof(IClickableMenu), nameof(IClickableMenu.drawToolTip)),
- prefix: new HarmonyMethod(this.GetType(), nameof(ObjectErrorPatch.Before_IClickableMenu_DrawTooltip))
- );
- }
-
-
- /*********
- ** Private methods
- *********/
- /// <summary>The method to call instead of <see cref="StardewValley.Object.getDescription"/>.</summary>
- /// <param name="__instance">The instance being patched.</param>
- /// <param name="__result">The patched method's return value.</param>
- /// <returns>Returns whether to execute the original method.</returns>
- private static bool Before_Object_GetDescription(SObject __instance, ref string __result)
- {
- // invalid bigcraftables crash instead of showing '???' like invalid non-bigcraftables
- if (!__instance.IsRecipe && __instance.bigCraftable.Value && !Game1.bigCraftablesInformation.ContainsKey(__instance.ParentSheetIndex))
- {
- __result = "???";
- return false;
- }
-
- return true;
- }
-
-#if HARMONY_2
- /// <summary>The method to call after <see cref="StardewValley.Object.loadDisplayName"/>.</summary>
- /// <param name="__result">The patched method's return value.</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_Object_loadDisplayName(ref string __result, Exception __exception)
- {
- if (__exception is KeyNotFoundException)
- {
- __result = "???";
- return null;
- }
-
- return __exception;
- }
-#else
- /// <summary>The method to call instead of <see cref="StardewValley.Object.loadDisplayName"/>.</summary>
- /// <param name="__instance">The instance being patched.</param>
- /// <param name="__result">The patched method's return value.</param>
- /// <param name="__originalMethod">The method being wrapped.</param>
- /// <returns>Returns whether to execute the original method.</returns>
- private static bool Before_Object_loadDisplayName(SObject __instance, ref string __result, MethodInfo __originalMethod)
- {
- const string key = nameof(ObjectErrorPatch.Before_Object_loadDisplayName);
- if (!PatchHelper.StartIntercept(key))
- return true;
-
- try
- {
- __result = (string)__originalMethod.Invoke(__instance, new object[0]);
- return false;
- }
- catch (TargetInvocationException ex) when (ex.InnerException is KeyNotFoundException)
- {
- __result = "???";
- return false;
- }
- catch
- {
- return true;
- }
- finally
- {
- PatchHelper.StopIntercept(key);
- }
- }
-#endif
-
- /// <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_IClickableMenu_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/ObjectPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/ObjectPatcher.cs
new file mode 100644
index 00000000..fd4ea35c
--- /dev/null
+++ b/src/SMAPI.Mods.ErrorHandler/Patches/ObjectPatcher.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using HarmonyLib;
+using StardewModdingAPI.Internal.Patching;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace StardewModdingAPI.Mods.ErrorHandler.Patches
+{
+ /// <summary>Harmony patches for <see cref="SObject"/> 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 ObjectPatcher : BasePatcher
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <inheritdoc />
+ public override void Apply(Harmony harmony, IMonitor monitor)
+ {
+ // object.getDescription
+ harmony.Patch(
+ original: this.RequireMethod<SObject>(nameof(SObject.getDescription)),
+ prefix: this.GetHarmonyMethod(nameof(ObjectPatcher.Before_Object_GetDescription))
+ );
+
+ // object.getDisplayName
+ harmony.Patch(
+ original: this.RequireMethod<SObject>("loadDisplayName"),
+ finalizer: this.GetHarmonyMethod(nameof(ObjectPatcher.Finalize_Object_loadDisplayName))
+ );
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>The method to call instead of <see cref="StardewValley.Object.getDescription"/>.</summary>
+ /// <param name="__instance">The instance being patched.</param>
+ /// <param name="__result">The patched method's return value.</param>
+ /// <returns>Returns whether to execute the original method.</returns>
+ private static bool Before_Object_GetDescription(SObject __instance, ref string __result)
+ {
+ // invalid bigcraftables crash instead of showing '???' like invalid non-bigcraftables
+ if (!__instance.IsRecipe && __instance.bigCraftable.Value && !Game1.bigCraftablesInformation.ContainsKey(__instance.ParentSheetIndex))
+ {
+ __result = "???";
+ return false;
+ }
+
+ return true;
+ }
+
+ /// <summary>The method to call after <see cref="StardewValley.Object.loadDisplayName"/>.</summary>
+ /// <param name="__result">The patched method's return value.</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_Object_loadDisplayName(ref string __result, Exception __exception)
+ {
+ if (__exception is KeyNotFoundException)
+ {
+ __result = "???";
+ return null;
+ }
+
+ return __exception;
+ }
+ }
+}
diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs
new file mode 100644
index 00000000..2a43cb10
--- /dev/null
+++ b/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs
@@ -0,0 +1,145 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using HarmonyLib;
+using Microsoft.Xna.Framework.Content;
+using StardewModdingAPI.Internal.Patching;
+using StardewValley;
+using StardewValley.Buildings;
+using StardewValley.Locations;
+using SObject = StardewValley.Object;
+
+namespace StardewModdingAPI.Mods.ErrorHandler.Patches
+{
+ /// <summary>Harmony patches for <see cref="SaveGame"/> which prevent some errors due to broken save data.</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 SaveGamePatcher : BasePatcher
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>Writes messages to the console and log file.</summary>
+ private static IMonitor Monitor;
+
+ /// <summary>A callback invoked when custom content is removed from the save data to avoid a crash.</summary>
+ private static Action OnContentRemoved;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="monitor">Writes messages to the console and log file.</param>
+ /// <param name="onContentRemoved">A callback invoked when custom content is removed from the save data to avoid a crash.</param>
+ public SaveGamePatcher(IMonitor monitor, Action onContentRemoved)
+ {
+ SaveGamePatcher.Monitor = monitor;
+ SaveGamePatcher.OnContentRemoved = onContentRemoved;
+ }
+
+ /// <inheritdoc />
+ public override void Apply(Harmony harmony, IMonitor monitor)
+ {
+ harmony.Patch(
+ original: this.RequireMethod<SaveGame>(nameof(SaveGame.loadDataToLocations)),
+ prefix: this.GetHarmonyMethod(nameof(SaveGamePatcher.Before_LoadDataToLocations))
+ );
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>The method to call instead of <see cref="SaveGame.loadDataToLocations"/>.</summary>
+ /// <param name="gamelocations">The game locations being loaded.</param>
+ /// <returns>Returns whether to execute the original method.</returns>
+ private static bool Before_LoadDataToLocations(List<GameLocation> gamelocations)
+ {
+ IDictionary<string, string> npcs = Game1.content.Load<Dictionary<string, string>>("Data\\NPCDispositions");
+
+ if (SaveGamePatcher.RemoveBrokenContent(gamelocations, npcs))
+ SaveGamePatcher.OnContentRemoved();
+
+ return true;
+ }
+
+ /// <summary>Remove content which no longer exists in the game data.</summary>
+ /// <param name="locations">The current game locations.</param>
+ /// <param name="npcs">The NPC data.</param>
+ private static bool RemoveBrokenContent(IEnumerable<GameLocation> locations, IDictionary<string, string> npcs)
+ {
+ bool removedAny = false;
+
+ foreach (GameLocation location in locations)
+ removedAny |= SaveGamePatcher.RemoveBrokenContent(location, npcs);
+
+ return removedAny;
+ }
+
+ /// <summary>Remove content which no longer exists in the game data.</summary>
+ /// <param name="location">The current game location.</param>
+ /// <param name="npcs">The NPC data.</param>
+ private static bool RemoveBrokenContent(GameLocation location, IDictionary<string, string> npcs)
+ {
+ bool removedAny = false;
+ if (location == null)
+ return false;
+
+ // check buildings
+ if (location is BuildableGameLocation buildableLocation)
+ {
+ foreach (Building building in buildableLocation.buildings.ToArray())
+ {
+ try
+ {
+ BluePrint _ = new BluePrint(building.buildingType.Value);
+ }
+ catch (ContentLoadException)
+ {
+ SaveGamePatcher.Monitor.Log($"Removed invalid building type '{building.buildingType.Value}' in {location.Name} ({building.tileX}, {building.tileY}) to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom building mod?)", LogLevel.Warn);
+ buildableLocation.buildings.Remove(building);
+ removedAny = true;
+ continue;
+ }
+
+ SaveGamePatcher.RemoveBrokenContent(building.indoors.Value, npcs);
+ }
+ }
+
+ // check NPCs
+ foreach (NPC npc in location.characters.ToArray())
+ {
+ if (npc.isVillager() && !npcs.ContainsKey(npc.Name))
+ {
+ try
+ {
+ npc.reloadSprite(); // this won't crash for special villagers like Bouncer
+ }
+ catch
+ {
+ SaveGamePatcher.Monitor.Log($"Removed invalid villager '{npc.Name}' in {location.Name} ({npc.getTileLocation()}) to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom NPC mod?)", LogLevel.Warn);
+ location.characters.Remove(npc);
+ removedAny = true;
+ }
+ }
+ }
+
+ // check objects
+ foreach (var pair in location.objects.Pairs.ToArray())
+ {
+ // SpaceCore can leave null values when removing its custom content
+ if (pair.Value == null)
+ {
+ location.Objects.Remove(pair.Key);
+ SaveGamePatcher.Monitor.Log($"Removed invalid null object in {location.Name} ({pair.Key}) to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom item mod?)", LogLevel.Warn);
+ removedAny = true;
+ }
+ }
+
+ return removedAny;
+ }
+ }
+}
diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/ScheduleErrorPatch.cs b/src/SMAPI.Mods.ErrorHandler/Patches/ScheduleErrorPatch.cs
deleted file mode 100644
index d2a5e988..00000000
--- a/src/SMAPI.Mods.ErrorHandler/Patches/ScheduleErrorPatch.cs
+++ /dev/null
@@ -1,108 +0,0 @@
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using StardewModdingAPI.Framework.Patching;
-using StardewValley;
-#if HARMONY_2
-using System;
-using HarmonyLib;
-using StardewModdingAPI.Framework;
-#else
-using System.Reflection;
-using Harmony;
-#endif
-
-namespace StardewModdingAPI.Mods.ErrorHandler.Patches
-{
- /// <summary>A Harmony patch for <see cref="NPC.parseMasterSchedule"/> which intercepts crashes due to invalid schedule data.</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 ScheduleErrorPatch : 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 ScheduleErrorPatch(IMonitor monitorForGame)
- {
- ScheduleErrorPatch.MonitorForGame = monitorForGame;
- }
-
- /// <inheritdoc />
-#if HARMONY_2
- public void Apply(Harmony harmony)
-#else
- public void Apply(HarmonyInstance harmony)
-#endif
- {
- harmony.Patch(
- original: AccessTools.Method(typeof(NPC), nameof(NPC.parseMasterSchedule)),
-#if HARMONY_2
- finalizer: new HarmonyMethod(this.GetType(), nameof(ScheduleErrorPatch.Finalize_NPC_parseMasterSchedule))
-#else
- prefix: new HarmonyMethod(this.GetType(), nameof(ScheduleErrorPatch.Before_NPC_parseMasterSchedule))
-#endif
- );
- }
-
-
- /*********
- ** Private methods
- *********/
-#if HARMONY_2
- /// <summary>The method to call instead of <see cref="NPC.parseMasterSchedule"/>.</summary>
- /// <param name="rawData">The raw schedule data to parse.</param>
- /// <param name="__instance">The instance being patched.</param>
- /// <param name="__result">The patched method's return value.</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_parseMasterSchedule(string rawData, NPC __instance, ref Dictionary<int, SchedulePathDescription> __result, Exception __exception)
- {
- if (__exception != null)
- {
- ScheduleErrorPatch.MonitorForGame.Log($"Failed parsing schedule for NPC {__instance.Name}:\n{rawData}\n{__exception.GetLogSummary()}", LogLevel.Error);
- __result = new Dictionary<int, SchedulePathDescription>();
- }
-
- return null;
- }
-#else
- /// <summary>The method to call instead of <see cref="NPC.parseMasterSchedule"/>.</summary>
- /// <param name="rawData">The raw schedule data to parse.</param>
- /// <param name="__instance">The instance being patched.</param>
- /// <param name="__result">The patched method's return value.</param>
- /// <param name="__originalMethod">The method being wrapped.</param>
- /// <returns>Returns whether to execute the original method.</returns>
- private static bool Before_NPC_parseMasterSchedule(string rawData, NPC __instance, ref Dictionary<int, SchedulePathDescription> __result, MethodInfo __originalMethod)
- {
- const string key = nameof(ScheduleErrorPatch.Before_NPC_parseMasterSchedule);
- if (!PatchHelper.StartIntercept(key))
- return true;
-
- try
- {
- __result = (Dictionary<int, SchedulePathDescription>)__originalMethod.Invoke(__instance, new object[] { rawData });
- return false;
- }
- catch (TargetInvocationException ex)
- {
- ScheduleErrorPatch.MonitorForGame.Log($"Failed parsing schedule for NPC {__instance.Name}:\n{rawData}\n{ex.InnerException ?? ex}", LogLevel.Error);
- __result = new Dictionary<int, SchedulePathDescription>();
- return false;
- }
- finally
- {
- PatchHelper.StopIntercept(key);
- }
- }
-#endif
- }
-}
diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchValidationPatches.cs b/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchPatcher.cs
index 8056fd71..6860a4ec 100644
--- a/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchValidationPatches.cs
+++ b/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchPatcher.cs
@@ -1,36 +1,28 @@
-#if HARMONY_2
-using HarmonyLib;
-#else
-using Harmony;
-#endif
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
{
- /// <summary>Harmony patch for <see cref="SpriteBatch"/> to validate textures earlier.</summary>
+ /// <summary>Harmony patches for <see cref="SpriteBatch"/> which validate textures earlier.</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 SpriteBatchValidationPatches : IHarmonyPatch
+ internal class SpriteBatchPatcher : BasePatcher
{
/*********
** Public methods
*********/
/// <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: Constants.GameFramework == GameFramework.Xna
- ? AccessTools.Method(typeof(SpriteBatch), "InternalDraw")
- : AccessTools.Method(typeof(SpriteBatch), "CheckValid", new[] { typeof(Texture2D) }),
- postfix: new HarmonyMethod(this.GetType(), nameof(SpriteBatchValidationPatches.After_SpriteBatch_CheckValid))
+ ? this.RequireMethod<SpriteBatch>("InternalDraw")
+ : this.RequireMethod<SpriteBatch>("CheckValid", new[] { typeof(Texture2D) }),
+ postfix: this.GetHarmonyMethod(nameof(SpriteBatchPatcher.After_CheckValid))
);
}
@@ -39,13 +31,13 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
** Private methods
*********/
#if SMAPI_FOR_XNA
- /// <summary>The method to call instead of <see cref="SpriteBatch.InternalDraw"/>.</summary>
+ /// <summary>The method to call after <see cref="SpriteBatch.InternalDraw"/>.</summary>
/// <param name="texture">The texture to validate.</param>
#else
- /// <summary>The method to call instead of <see cref="SpriteBatch.CheckValid"/>.</summary>
+ /// <summary>The method to call after <see cref="SpriteBatch.CheckValid"/>.</summary>
/// <param name="texture">The texture to validate.</param>
#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/UtilityErrorPatches.cs b/src/SMAPI.Mods.ErrorHandler/Patches/UtilityErrorPatches.cs
deleted file mode 100644
index 1ddd407c..00000000
--- a/src/SMAPI.Mods.ErrorHandler/Patches/UtilityErrorPatches.cs
+++ /dev/null
@@ -1,89 +0,0 @@
-#if HARMONY_2
-using System;
-using HarmonyLib;
-#else
-using Harmony;
-#endif
-using System;
-using System.Diagnostics.CodeAnalysis;
-using System.Reflection;
-using StardewModdingAPI.Framework.Patching;
-using StardewValley;
-
-namespace StardewModdingAPI.Mods.ErrorHandler.Patches
-{
- /// <summary>A Harmony patch for <see cref="Utility"/> methods to log more detailed errors.</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 UtilityErrorPatches : IHarmonyPatch
- {
- /*********
- ** Public methods
- *********/
- /// <inheritdoc />
-#if HARMONY_2
- public void Apply(Harmony harmony)
- {
- harmony.Patch(
- original: AccessTools.Method(typeof(Utility), nameof(Utility.getItemFromStandardTextDescription)),
- finalizer: new HarmonyMethod(this.GetType(), nameof(UtilityErrorPatches.Finalize_Utility_GetItemFromStandardTextDescription))
- );
- }
-#else
- public void Apply(HarmonyInstance harmony)
- {
- harmony.Patch(
- original: AccessTools.Method(typeof(Utility), nameof(Utility.getItemFromStandardTextDescription)),
- prefix: new HarmonyMethod(this.GetType(), nameof(UtilityErrorPatches.Before_Utility_GetItemFromStandardTextDescription))
- );
- }
-#endif
-
-
- /*********
- ** Private methods
- *********/
-#if HARMONY_2
- /// <summary>The method to call instead of <see cref="Utility.getItemFromStandardTextDescription"/>.</summary>
- /// <param name="description">The item text description to parse.</param>
- /// <param name="delimiter">The delimiter by which to split the text description.</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_Utility_GetItemFromStandardTextDescription(string description, char delimiter, ref Exception __exception)
- {
- return __exception != null
- ? new FormatException($"Failed to parse item text description \"{description}\" with delimiter \"{delimiter}\".", __exception)
- : null;
- }
-#else
- /// <summary>The method to call instead of <see cref="Utility.getItemFromStandardTextDescription"/>.</summary>
- /// <param name="__result">The return value of the original method.</param>
- /// <param name="description">The item text description to parse.</param>
- /// <param name="who">The player for which the item is being parsed.</param>
- /// <param name="delimiter">The delimiter by which to split the text description.</param>
- /// <param name="__originalMethod">The method being wrapped.</param>
- /// <returns>Returns whether to execute the original method.</returns>
- private static bool Before_Utility_GetItemFromStandardTextDescription(ref Item __result, string description, Farmer who, char delimiter, MethodInfo __originalMethod)
- {
- const string key = nameof(UtilityErrorPatches.Before_Utility_GetItemFromStandardTextDescription);
- if (!PatchHelper.StartIntercept(key))
- return true;
-
- try
- {
- __result = (Item)__originalMethod.Invoke(null, new object[] { description, who, delimiter });
- return false;
- }
- catch (TargetInvocationException ex)
- {
- throw new FormatException($"Failed to parse item text description \"{description}\" with delimiter \"{delimiter}\".", ex.InnerException);
- }
- finally
- {
- PatchHelper.StopIntercept(key);
- }
- }
-#endif
- }
-}
diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/UtilityPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/UtilityPatcher.cs
new file mode 100644
index 00000000..ce85d0c2
--- /dev/null
+++ b/src/SMAPI.Mods.ErrorHandler/Patches/UtilityPatcher.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using HarmonyLib;
+using StardewModdingAPI.Internal.Patching;
+using StardewValley;
+
+namespace StardewModdingAPI.Mods.ErrorHandler.Patches
+{
+ /// <summary>A Harmony patch for <see cref="Utility"/> methods to log more detailed errors.</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 UtilityPatcher : BasePatcher
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <inheritdoc />
+ public override void Apply(Harmony harmony, IMonitor monitor)
+ {
+ harmony.Patch(
+ original: this.RequireMethod<Utility>(nameof(Utility.getItemFromStandardTextDescription)),
+ finalizer: this.GetHarmonyMethod(nameof(UtilityPatcher.Finalize_GetItemFromStandardTextDescription))
+ );
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>The method to call when <see cref="Utility.getItemFromStandardTextDescription"/> throws an exception.</summary>
+ /// <param name="description">The item text description to parse.</param>
+ /// <param name="delimiter">The delimiter by which to split the text description.</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_GetItemFromStandardTextDescription(string description, char delimiter, ref Exception __exception)
+ {
+ return __exception != null
+ ? new FormatException($"Failed to parse item text description \"{description}\" with delimiter \"{delimiter}\".", __exception)
+ : null;
+ }
+ }
+}