diff options
Diffstat (limited to 'src/SMAPI/Patches')
-rw-r--r-- | src/SMAPI/Patches/DialogueErrorPatch.cs | 49 | ||||
-rw-r--r-- | src/SMAPI/Patches/EventErrorPatch.cs | 84 | ||||
-rw-r--r-- | src/SMAPI/Patches/LoadContextPatch.cs (renamed from src/SMAPI/Patches/LoadForNewGamePatch.cs) | 49 | ||||
-rw-r--r-- | src/SMAPI/Patches/ObjectErrorPatch.cs | 32 |
4 files changed, 178 insertions, 36 deletions
diff --git a/src/SMAPI/Patches/DialogueErrorPatch.cs b/src/SMAPI/Patches/DialogueErrorPatch.cs index d8905fd1..f1c25c05 100644 --- a/src/SMAPI/Patches/DialogueErrorPatch.cs +++ b/src/SMAPI/Patches/DialogueErrorPatch.cs @@ -13,7 +13,7 @@ namespace StardewModdingAPI.Patches internal class DialogueErrorPatch : IHarmonyPatch { /********* - ** Private methods + ** Fields *********/ /// <summary>Writes messages to the console and log file on behalf of the game.</summary> private static IMonitor MonitorForGame; @@ -21,6 +21,9 @@ namespace StardewModdingAPI.Patches /// <summary>Simplifies access to private code.</summary> private static Reflector Reflection; + /// <summary>Whether the <see cref="NPC.CurrentDialogue"/> getter is currently being intercepted.</summary> + private static bool IsInterceptingCurrentDialogue; + /********* ** Accessors @@ -46,10 +49,14 @@ namespace StardewModdingAPI.Patches /// <param name="harmony">The Harmony instance.</param> public void Apply(HarmonyInstance harmony) { - ConstructorInfo constructor = AccessTools.Constructor(typeof(Dialogue), new[] { typeof(string), typeof(NPC) }); - MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(DialogueErrorPatch.Prefix)); - - harmony.Patch(constructor, new HarmonyMethod(prefix), null); + 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)) + ); } @@ -63,7 +70,7 @@ namespace StardewModdingAPI.Patches /// <returns>Returns whether to execute the original method.</returns> /// <remarks>This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments.</remarks> [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony.")] - private static bool Prefix(Dialogue __instance, string masterDialogue, NPC speaker) + 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(); @@ -96,5 +103,35 @@ namespace StardewModdingAPI.Patches 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> + /// <remarks>This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments.</remarks> + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony.")] + private static bool Before_NPC_CurrentDialogue(NPC __instance, ref Stack<Dialogue> __result, MethodInfo __originalMethod) + { + if (DialogueErrorPatch.IsInterceptingCurrentDialogue) + return true; + + try + { + DialogueErrorPatch.IsInterceptingCurrentDialogue = true; + __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 + { + DialogueErrorPatch.IsInterceptingCurrentDialogue = false; + } + } } } diff --git a/src/SMAPI/Patches/EventErrorPatch.cs b/src/SMAPI/Patches/EventErrorPatch.cs new file mode 100644 index 00000000..cd530616 --- /dev/null +++ b/src/SMAPI/Patches/EventErrorPatch.cs @@ -0,0 +1,84 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Harmony; +using StardewModdingAPI.Framework.Patching; +using StardewValley; + +namespace StardewModdingAPI.Patches +{ + /// <summary>A Harmony patch for the <see cref="Dialogue"/> constructor which intercepts invalid dialogue lines and logs an error instead of crashing.</summary> + internal class EventErrorPatch : IHarmonyPatch + { + /********* + ** Fields + *********/ + /// <summary>Writes messages to the console and log file on behalf of the game.</summary> + private static IMonitor MonitorForGame; + + /// <summary>Whether the method is currently being intercepted.</summary> + private static bool IsIntercepted; + + + /********* + ** Accessors + *********/ + /// <summary>A unique name for this patch.</summary> + public string Name => $"{nameof(EventErrorPatch)}"; + + + /********* + ** 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 EventErrorPatch(IMonitor monitorForGame) + { + EventErrorPatch.MonitorForGame = monitorForGame; + } + + /// <summary>Apply the Harmony patch.</summary> + /// <param name="harmony">The Harmony instance.</param> + public void Apply(HarmonyInstance harmony) + { + harmony.Patch( + original: AccessTools.Method(typeof(GameLocation), "checkEventPrecondition"), + prefix: new HarmonyMethod(this.GetType(), nameof(EventErrorPatch.Before_GameLocation_CheckEventPrecondition)) + ); + } + + + /********* + ** Private methods + *********/ + /// <summary>The method to call instead of the 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> + /// <remarks>This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments.</remarks> + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony.")] + private static bool Before_GameLocation_CheckEventPrecondition(GameLocation __instance, ref int __result, string precondition, MethodInfo __originalMethod) + { + if (EventErrorPatch.IsIntercepted) + return true; + + try + { + EventErrorPatch.IsIntercepted = true; + __result = (int)__originalMethod.Invoke(__instance, new object[] { precondition }); + return false; + } + catch (TargetInvocationException ex) + { + __result = -1; + EventErrorPatch.MonitorForGame.Log($"Failed parsing event precondition ({precondition}):\n{ex.InnerException}", LogLevel.Error); + return false; + } + finally + { + EventErrorPatch.IsIntercepted = false; + } + } + } +} diff --git a/src/SMAPI/Patches/LoadForNewGamePatch.cs b/src/SMAPI/Patches/LoadContextPatch.cs index 9e788e84..3f86c9a9 100644 --- a/src/SMAPI/Patches/LoadForNewGamePatch.cs +++ b/src/SMAPI/Patches/LoadContextPatch.cs @@ -1,7 +1,6 @@ using System; using System.Collections.ObjectModel; using System.Collections.Specialized; -using System.Reflection; using Harmony; using StardewModdingAPI.Enums; using StardewModdingAPI.Framework.Patching; @@ -13,10 +12,10 @@ namespace StardewModdingAPI.Patches { /// <summary>A Harmony patch for <see cref="Game1.loadForNewGame"/> which notifies SMAPI for save creation load stages.</summary> /// <remarks>This patch hooks into <see cref="Game1.loadForNewGame"/>, checks if <c>TitleMenu.transitioningCharacterCreationMenu</c> is true (which means the player is creating a new save file), then raises <see cref="LoadStage.CreatedBasicInfo"/> after the location list is cleared twice (the second clear happens right before locations are created), and <see cref="LoadStage.CreatedLocations"/> when the method ends.</remarks> - internal class LoadForNewGamePatch : IHarmonyPatch + internal class LoadContextPatch : IHarmonyPatch { /********* - ** Accessors + ** Fields *********/ /// <summary>Simplifies access to private code.</summary> private static Reflector Reflection; @@ -28,14 +27,14 @@ namespace StardewModdingAPI.Patches private static bool IsCreating; /// <summary>The number of times that <see cref="Game1.locations"/> has been cleared since <see cref="Game1.loadForNewGame"/> started.</summary> - private static int TimesLocationsCleared = 0; + private static int TimesLocationsCleared; /********* ** Accessors *********/ /// <summary>A unique name for this patch.</summary> - public string Name => $"{nameof(LoadForNewGamePatch)}"; + public string Name => $"{nameof(LoadContextPatch)}"; /********* @@ -44,21 +43,21 @@ namespace StardewModdingAPI.Patches /// <summary>Construct an instance.</summary> /// <param name="reflection">Simplifies access to private code.</param> /// <param name="onStageChanged">A callback to invoke when the load stage changes.</param> - public LoadForNewGamePatch(Reflector reflection, Action<LoadStage> onStageChanged) + public LoadContextPatch(Reflector reflection, Action<LoadStage> onStageChanged) { - LoadForNewGamePatch.Reflection = reflection; - LoadForNewGamePatch.OnStageChanged = onStageChanged; + LoadContextPatch.Reflection = reflection; + LoadContextPatch.OnStageChanged = onStageChanged; } /// <summary>Apply the Harmony patch.</summary> /// <param name="harmony">The Harmony instance.</param> public void Apply(HarmonyInstance harmony) { - MethodInfo method = AccessTools.Method(typeof(Game1), nameof(Game1.loadForNewGame)); - MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(LoadForNewGamePatch.Prefix)); - MethodInfo postfix = AccessTools.Method(this.GetType(), nameof(LoadForNewGamePatch.Postfix)); - - harmony.Patch(method, new HarmonyMethod(prefix), new HarmonyMethod(postfix)); + harmony.Patch( + original: AccessTools.Method(typeof(Game1), nameof(Game1.loadForNewGame)), + prefix: new HarmonyMethod(this.GetType(), nameof(LoadContextPatch.Before_Game1_LoadForNewGame)), + postfix: new HarmonyMethod(this.GetType(), nameof(LoadContextPatch.After_Game1_LoadForNewGame)) + ); } @@ -68,15 +67,15 @@ namespace StardewModdingAPI.Patches /// <summary>The method to call instead of <see cref="Game1.loadForNewGame"/>.</summary> /// <returns>Returns whether to execute the original method.</returns> /// <remarks>This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments.</remarks> - private static bool Prefix() + private static bool Before_Game1_LoadForNewGame() { - LoadForNewGamePatch.IsCreating = Game1.activeClickableMenu is TitleMenu menu && LoadForNewGamePatch.Reflection.GetField<bool>(menu, "transitioningCharacterCreationMenu").GetValue(); - LoadForNewGamePatch.TimesLocationsCleared = 0; - if (LoadForNewGamePatch.IsCreating) + LoadContextPatch.IsCreating = Game1.activeClickableMenu is TitleMenu menu && LoadContextPatch.Reflection.GetField<bool>(menu, "transitioningCharacterCreationMenu").GetValue(); + LoadContextPatch.TimesLocationsCleared = 0; + if (LoadContextPatch.IsCreating) { // raise CreatedBasicInfo after locations are cleared twice ObservableCollection<GameLocation> locations = (ObservableCollection<GameLocation>)Game1.locations; - locations.CollectionChanged += LoadForNewGamePatch.OnLocationListChanged; + locations.CollectionChanged += LoadContextPatch.OnLocationListChanged; } return true; @@ -84,16 +83,16 @@ namespace StardewModdingAPI.Patches /// <summary>The method to call instead after <see cref="Game1.loadForNewGame"/>.</summary> /// <remarks>This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments.</remarks> - private static void Postfix() + private static void After_Game1_LoadForNewGame() { - if (LoadForNewGamePatch.IsCreating) + if (LoadContextPatch.IsCreating) { // clean up - ObservableCollection<GameLocation> locations = (ObservableCollection<GameLocation>) Game1.locations; - locations.CollectionChanged -= LoadForNewGamePatch.OnLocationListChanged; + ObservableCollection<GameLocation> locations = (ObservableCollection<GameLocation>)Game1.locations; + locations.CollectionChanged -= LoadContextPatch.OnLocationListChanged; // raise stage changed - LoadForNewGamePatch.OnStageChanged(LoadStage.CreatedLocations); + LoadContextPatch.OnStageChanged(LoadStage.CreatedLocations); } } @@ -102,8 +101,8 @@ namespace StardewModdingAPI.Patches /// <param name="e">The event arguments.</param> private static void OnLocationListChanged(object sender, NotifyCollectionChangedEventArgs e) { - if (++LoadForNewGamePatch.TimesLocationsCleared == 2) - LoadForNewGamePatch.OnStageChanged(LoadStage.CreatedBasicInfo); + if (++LoadContextPatch.TimesLocationsCleared == 2) + LoadContextPatch.OnStageChanged(LoadStage.CreatedBasicInfo); } } } diff --git a/src/SMAPI/Patches/ObjectErrorPatch.cs b/src/SMAPI/Patches/ObjectErrorPatch.cs index 0481259d..5b918d39 100644 --- a/src/SMAPI/Patches/ObjectErrorPatch.cs +++ b/src/SMAPI/Patches/ObjectErrorPatch.cs @@ -1,8 +1,8 @@ using System.Diagnostics.CodeAnalysis; -using System.Reflection; using Harmony; using StardewModdingAPI.Framework.Patching; using StardewValley; +using StardewValley.Menus; using SObject = StardewValley.Object; namespace StardewModdingAPI.Patches @@ -24,10 +24,17 @@ namespace StardewModdingAPI.Patches /// <param name="harmony">The Harmony instance.</param> public void Apply(HarmonyInstance harmony) { - MethodInfo method = AccessTools.Method(typeof(SObject), nameof(SObject.getDescription)); - MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(ObjectErrorPatch.Prefix)); + // object.getDescription + harmony.Patch( + original: AccessTools.Method(typeof(SObject), nameof(SObject.getDescription)), + prefix: new HarmonyMethod(this.GetType(), nameof(ObjectErrorPatch.Before_Object_GetDescription)) + ); - harmony.Patch(method, new HarmonyMethod(prefix), null); + // IClickableMenu.drawToolTip + harmony.Patch( + original: AccessTools.Method(typeof(IClickableMenu), nameof(IClickableMenu.drawToolTip)), + prefix: new HarmonyMethod(this.GetType(), nameof(ObjectErrorPatch.Before_IClickableMenu_DrawTooltip)) + ); } @@ -40,7 +47,7 @@ namespace StardewModdingAPI.Patches /// <returns>Returns whether to execute the original method.</returns> /// <remarks>This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments.</remarks> [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony.")] - private static bool Prefix(SObject __instance, ref string __result) + 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)) @@ -51,5 +58,20 @@ namespace StardewModdingAPI.Patches return true; } + + /// <summary>The method to call instead of <see cref="IClickableMenu.drawToolTip"/>.</summary> + /// <param name="__instance">The instance being patched.</param> + /// <param name="hoveredItem">The item for which to draw a tooltip.</param> + /// <returns>Returns whether to execute the original method.</returns> + /// <remarks>This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments.</remarks> + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony.")] + private static bool Before_IClickableMenu_DrawTooltip(IClickableMenu __instance, 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; + } } } |