From 9461494a35b789c679a799fc9c5db2321d19d803 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 26 Sep 2019 19:48:01 -0400 Subject: [PATCH] auto-fix save data when a custom NPC mod is removed --- docs/README.md | 11 +-- docs/release-notes.md | 9 ++- src/SMAPI/Framework/SCore.cs | 3 +- src/SMAPI/Patches/DialogueErrorPatch.cs | 9 ++- src/SMAPI/Patches/EventErrorPatch.cs | 7 +- src/SMAPI/Patches/LoadContextPatch.cs | 7 +- src/SMAPI/Patches/LoadErrorPatch.cs | 94 +++++++++++++++++++++++++ src/SMAPI/Patches/ObjectErrorPatch.cs | 9 ++- 8 files changed, 127 insertions(+), 22 deletions(-) create mode 100644 src/SMAPI/Patches/LoadErrorPatch.cs diff --git a/docs/README.md b/docs/README.md index 4b9c97a1..625e7eeb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -20,10 +20,13 @@ doesn't change any of your game files. It serves eight main purposes: many mods, and rewrites the mod so it's compatible._ 5. **Intercept errors.** - _SMAPI intercepts errors that happen in the game, displays the error details in the console - window, and in most cases automatically recovers the game. This prevents mods from accidentally - crashing the game, and makes it possible to troubleshoot errors in the game itself that would - otherwise show a generic 'program has stopped working' type of message._ + _SMAPI intercepts errors, shows the error info in the SMAPI console, and in most cases + automatically recovers the game. That prevents mods from crashing the game, and makes it + possible to troubleshoot errors in the game itself that would otherwise show a generic 'program + has stopped working' type of message._ + + _That also includes automatically fixing save data when a load would crash, e.g. due to a custom + NPC mod the player removed._ 6. **Provide update checks.** _SMAPI automatically checks for new versions of your installed mods, and notifies you when any diff --git a/docs/release-notes.md b/docs/release-notes.md index 285384b0..2a1b333e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,12 +8,16 @@ These changes have not been released yet. For players: * **Updated for Stardew Valley 1.4.** SMAPI 3.0 adds compatibility with the latest game version, and improves mod APIs using changes in the game code. + * **Improved performance.** SMAPI should have less impact on game performance and startup time for some players. + * **Added more error recovery.** - SMAPI now detects and prevents more crashes due to game or mod bugs. + SMAPI now detects and prevents more crashes due to game or mod bugs, or due to removing some mods which add custom content. + * **Improved mod scanning.** SMAPI now supports some non-standard mod structures automatically, improves compatibility with the Vortex mod manager, and improves various error/skip messages related to mod loading. + * **Fixed many bugs and edge cases.** For modders: @@ -39,11 +43,12 @@ For modders: * Changes: * Updated for Stardew Valley 1.4. * Improved performance. - * Rewrote launch script on Linux to improve compatibility (thanks to kurumushi and toastal!). * Improved mod scanning: * Now ignores metadata files and folders (like `__MACOSX` and `__folder_managed_by_vortex`) and content files (like `.txt` or `.png`), which avoids missing-manifest errors in some common cases. * Now detects XNB mods more accurately, and consolidates multi-folder XNB mods in logged messages. + * SMAPI now automatically fixes your save if you remove a custom NPC mod. (Invalid NPCs are now removed on load, with a warning in the console.) * Added support for configuring console colors via `smapi-internal/config.json` (intended for players with unusual consoles). + * Improved launch script compatibility on Linux (thanks to kurumushi and toastal!). * Save Backup now works in the background, to avoid affecting startup time for players with a large number of saves. * The installer now recognises custom game paths stored in `stardewvalley.targets`. * Duplicate-mod errors now show the mod version in each folder. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index e293cefd..bc893abc 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -239,7 +239,8 @@ namespace StardewModdingAPI.Framework new EventErrorPatch(this.MonitorForGame), new DialogueErrorPatch(this.MonitorForGame, this.Reflection), new ObjectErrorPatch(), - new LoadContextPatch(this.Reflection, this.GameInstance.OnLoadStageChanged) + new LoadContextPatch(this.Reflection, this.GameInstance.OnLoadStageChanged), + new LoadErrorPatch(this.Monitor) ); // add exit handler diff --git a/src/SMAPI/Patches/DialogueErrorPatch.cs b/src/SMAPI/Patches/DialogueErrorPatch.cs index f1c25c05..24f97259 100644 --- a/src/SMAPI/Patches/DialogueErrorPatch.cs +++ b/src/SMAPI/Patches/DialogueErrorPatch.cs @@ -10,6 +10,9 @@ using StardewValley; namespace StardewModdingAPI.Patches { /// A Harmony patch for the constructor which intercepts invalid dialogue lines and logs an error instead of crashing. + /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] internal class DialogueErrorPatch : IHarmonyPatch { /********* @@ -29,7 +32,7 @@ namespace StardewModdingAPI.Patches ** Accessors *********/ /// A unique name for this patch. - public string Name => $"{nameof(DialogueErrorPatch)}"; + public string Name => nameof(DialogueErrorPatch); /********* @@ -68,8 +71,6 @@ namespace StardewModdingAPI.Patches /// The dialogue being parsed. /// The NPC for which the dialogue is being parsed. /// Returns whether to execute the original method. - /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony.")] private static bool Before_Dialogue_Constructor(Dialogue __instance, string masterDialogue, NPC speaker) { // get private members @@ -109,8 +110,6 @@ namespace StardewModdingAPI.Patches /// The return value of the original method. /// The method being wrapped. /// Returns whether to execute the original method. - /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony.")] private static bool Before_NPC_CurrentDialogue(NPC __instance, ref Stack __result, MethodInfo __originalMethod) { if (DialogueErrorPatch.IsInterceptingCurrentDialogue) diff --git a/src/SMAPI/Patches/EventErrorPatch.cs b/src/SMAPI/Patches/EventErrorPatch.cs index cd530616..1dc7e8c3 100644 --- a/src/SMAPI/Patches/EventErrorPatch.cs +++ b/src/SMAPI/Patches/EventErrorPatch.cs @@ -7,6 +7,9 @@ using StardewValley; namespace StardewModdingAPI.Patches { /// A Harmony patch for the constructor which intercepts invalid dialogue lines and logs an error instead of crashing. + /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] internal class EventErrorPatch : IHarmonyPatch { /********* @@ -23,7 +26,7 @@ namespace StardewModdingAPI.Patches ** Accessors *********/ /// A unique name for this patch. - public string Name => $"{nameof(EventErrorPatch)}"; + public string Name => nameof(EventErrorPatch); /********* @@ -56,8 +59,6 @@ namespace StardewModdingAPI.Patches /// The precondition to be parsed. /// The method being wrapped. /// Returns whether to execute the original method. - /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. - [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) diff --git a/src/SMAPI/Patches/LoadContextPatch.cs b/src/SMAPI/Patches/LoadContextPatch.cs index 93a059aa..0cc8c8eb 100644 --- a/src/SMAPI/Patches/LoadContextPatch.cs +++ b/src/SMAPI/Patches/LoadContextPatch.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Harmony; using StardewModdingAPI.Enums; using StardewModdingAPI.Framework.Patching; @@ -10,7 +11,9 @@ using StardewValley.Minigames; namespace StardewModdingAPI.Patches { /// Harmony patches which notify SMAPI for save creation load stages. - /// This patch hooks into , checks if TitleMenu.transitioningCharacterCreationMenu is true (which means the player is creating a new save file), then raises after the location list is cleared twice (the second clear happens right before locations are created), and when the method ends. + /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] internal class LoadContextPatch : IHarmonyPatch { /********* @@ -27,7 +30,7 @@ namespace StardewModdingAPI.Patches ** Accessors *********/ /// A unique name for this patch. - public string Name => $"{nameof(LoadContextPatch)}"; + public string Name => nameof(LoadContextPatch); /********* diff --git a/src/SMAPI/Patches/LoadErrorPatch.cs b/src/SMAPI/Patches/LoadErrorPatch.cs new file mode 100644 index 00000000..87e8ee14 --- /dev/null +++ b/src/SMAPI/Patches/LoadErrorPatch.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Harmony; +using StardewModdingAPI.Framework.Patching; +using StardewValley; +using StardewValley.Locations; + +namespace StardewModdingAPI.Patches +{ + /// A Harmony patch for which prevents some errors due to broken save data. + /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + internal class LoadErrorPatch : IHarmonyPatch + { + /********* + ** Fields + *********/ + /// Writes messages to the console and log file. + private static IMonitor Monitor; + + + /********* + ** Accessors + *********/ + /// A unique name for this patch. + public string Name => nameof(LoadErrorPatch); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Writes messages to the console and log file. + public LoadErrorPatch(IMonitor monitor) + { + LoadErrorPatch.Monitor = monitor; + } + + + /// Apply the Harmony patch. + /// The Harmony instance. + public void Apply(HarmonyInstance harmony) + { + harmony.Patch( + original: AccessTools.Method(typeof(SaveGame), nameof(SaveGame.loadDataToLocations)), + prefix: new HarmonyMethod(this.GetType(), nameof(LoadErrorPatch.Before_SaveGame_LoadDataToLocations)) + ); + } + + + /********* + ** Private methods + *********/ + /// The method to call instead of . + /// The game locations being loaded. + /// Returns whether to execute the original method. + private static bool Before_SaveGame_LoadDataToLocations(List gamelocations) + { + // get building interiors + var interiors = + ( + from location in gamelocations.OfType() + from building in location.buildings + where building.indoors.Value != null + select building.indoors.Value + ); + + // remove custom NPCs which no longer exist + IDictionary data = Game1.content.Load>("Data\\NPCDispositions"); + foreach (GameLocation location in gamelocations.Concat(interiors)) + { + 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}' to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom NPC mod?)", LogLevel.Warn); + location.characters.Remove(npc); + } + } + } + } + + return true; + } + } +} diff --git a/src/SMAPI/Patches/ObjectErrorPatch.cs b/src/SMAPI/Patches/ObjectErrorPatch.cs index 5b918d39..d716b29b 100644 --- a/src/SMAPI/Patches/ObjectErrorPatch.cs +++ b/src/SMAPI/Patches/ObjectErrorPatch.cs @@ -8,13 +8,16 @@ using SObject = StardewValley.Object; namespace StardewModdingAPI.Patches { /// A Harmony patch for which intercepts crashes due to the item no longer existing. + /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] internal class ObjectErrorPatch : IHarmonyPatch { /********* ** Accessors *********/ /// A unique name for this patch. - public string Name => $"{nameof(ObjectErrorPatch)}"; + public string Name => nameof(ObjectErrorPatch); /********* @@ -45,8 +48,6 @@ namespace StardewModdingAPI.Patches /// The instance being patched. /// The patched method's return value. /// Returns whether to execute the original method. - /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony.")] private static bool Before_Object_GetDescription(SObject __instance, ref string __result) { // invalid bigcraftables crash instead of showing '???' like invalid non-bigcraftables @@ -63,8 +64,6 @@ namespace StardewModdingAPI.Patches /// The instance being patched. /// The item for which to draw a tooltip. /// Returns whether to execute the original method. - /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. - [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