From f2dd11fe3fbe8b31665ad25e6e58b0026c00098e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 10 May 2019 23:49:08 -0400 Subject: fix inconsistent LoadStage behavior when creating a new save --- src/SMAPI/Patches/LoadContextPatch.cs | 55 +++++++++++------------------------ 1 file changed, 17 insertions(+), 38 deletions(-) (limited to 'src/SMAPI/Patches') diff --git a/src/SMAPI/Patches/LoadContextPatch.cs b/src/SMAPI/Patches/LoadContextPatch.cs index 3f86c9a9..93a059aa 100644 --- a/src/SMAPI/Patches/LoadContextPatch.cs +++ b/src/SMAPI/Patches/LoadContextPatch.cs @@ -1,16 +1,15 @@ using System; -using System.Collections.ObjectModel; -using System.Collections.Specialized; using Harmony; using StardewModdingAPI.Enums; using StardewModdingAPI.Framework.Patching; using StardewModdingAPI.Framework.Reflection; using StardewValley; using StardewValley.Menus; +using StardewValley.Minigames; namespace StardewModdingAPI.Patches { - /// A Harmony patch for which notifies SMAPI for save creation load stages. + /// 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. internal class LoadContextPatch : IHarmonyPatch { @@ -23,12 +22,6 @@ namespace StardewModdingAPI.Patches /// A callback to invoke when the load stage changes. private static Action OnStageChanged; - /// Whether was called as part of save creation. - private static bool IsCreating; - - /// The number of times that has been cleared since started. - private static int TimesLocationsCleared; - /********* ** Accessors @@ -53,9 +46,15 @@ namespace StardewModdingAPI.Patches /// The Harmony instance. public void Apply(HarmonyInstance harmony) { + // detect CreatedBasicInfo + harmony.Patch( + original: AccessTools.Method(typeof(TitleMenu), nameof(TitleMenu.createdNewCharacter)), + prefix: new HarmonyMethod(this.GetType(), nameof(LoadContextPatch.Before_TitleMenu_CreatedNewCharacter)) + ); + + // detect CreatedLocations 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)) ); } @@ -64,45 +63,25 @@ namespace StardewModdingAPI.Patches /********* ** Private methods *********/ - /// The method to call instead of . + /// Called before . /// Returns whether to execute the original method. /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. - private static bool Before_Game1_LoadForNewGame() + private static bool Before_TitleMenu_CreatedNewCharacter() { - LoadContextPatch.IsCreating = Game1.activeClickableMenu is TitleMenu menu && LoadContextPatch.Reflection.GetField(menu, "transitioningCharacterCreationMenu").GetValue(); - LoadContextPatch.TimesLocationsCleared = 0; - if (LoadContextPatch.IsCreating) - { - // raise CreatedBasicInfo after locations are cleared twice - ObservableCollection locations = (ObservableCollection)Game1.locations; - locations.CollectionChanged += LoadContextPatch.OnLocationListChanged; - } - + LoadContextPatch.OnStageChanged(LoadStage.CreatedBasicInfo); return true; } - /// The method to call instead after . + /// Called after . /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. private static void After_Game1_LoadForNewGame() { - if (LoadContextPatch.IsCreating) - { - // clean up - ObservableCollection locations = (ObservableCollection)Game1.locations; - locations.CollectionChanged -= LoadContextPatch.OnLocationListChanged; + bool creating = + (Game1.currentMinigame is Intro) // creating save with intro + || (Game1.activeClickableMenu is TitleMenu menu && LoadContextPatch.Reflection.GetField(menu, "transitioningCharacterCreationMenu").GetValue()); // creating save, skipped intro - // raise stage changed + if (creating) LoadContextPatch.OnStageChanged(LoadStage.CreatedLocations); - } - } - - /// Raised when changes. - /// The event sender. - /// The event arguments. - private static void OnLocationListChanged(object sender, NotifyCollectionChangedEventArgs e) - { - if (++LoadContextPatch.TimesLocationsCleared == 2) - LoadContextPatch.OnStageChanged(LoadStage.CreatedBasicInfo); } } } -- cgit From 9461494a35b789c679a799fc9c5db2321d19d803 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 26 Sep 2019 19:48:01 -0400 Subject: auto-fix save data when a custom NPC mod is removed --- 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 ++-- 5 files changed, 111 insertions(+), 15 deletions(-) create mode 100644 src/SMAPI/Patches/LoadErrorPatch.cs (limited to 'src/SMAPI/Patches') 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 -- cgit From 65997c1243a60ae15cc0b832ebcd41d96c3ea06a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 1 Oct 2019 21:41:15 -0400 Subject: auto-fix save data when a custom location mod is removed --- src/SMAPI/Patches/LoadErrorPatch.cs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) (limited to 'src/SMAPI/Patches') diff --git a/src/SMAPI/Patches/LoadErrorPatch.cs b/src/SMAPI/Patches/LoadErrorPatch.cs index 87e8ee14..eedb4164 100644 --- a/src/SMAPI/Patches/LoadErrorPatch.cs +++ b/src/SMAPI/Patches/LoadErrorPatch.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -20,6 +21,9 @@ namespace StardewModdingAPI.Patches /// Writes messages to the console and log file. private static IMonitor Monitor; + /// A callback invoked when custom content is removed from the save data to avoid a crash. + private static Action OnContentRemoved; + /********* ** Accessors @@ -33,9 +37,11 @@ namespace StardewModdingAPI.Patches *********/ /// Construct an instance. /// Writes messages to the console and log file. - public LoadErrorPatch(IMonitor monitor) + /// A callback invoked when custom content is removed from the save data to avoid a crash. + public LoadErrorPatch(IMonitor monitor, Action onContentRemoved) { LoadErrorPatch.Monitor = monitor; + LoadErrorPatch.OnContentRemoved = onContentRemoved; } @@ -58,6 +64,22 @@ namespace StardewModdingAPI.Patches /// Returns whether to execute the original method. private static bool Before_SaveGame_LoadDataToLocations(List gamelocations) { + bool removedAny = false; + + // remove invalid locations + foreach (GameLocation location in gamelocations.ToArray()) + { + if (location is Cellar) + continue; // missing cellars will be added by the game code + + if (Game1.getLocationFromName(location.name) == null) + { + LoadErrorPatch.Monitor.Log($"Removed invalid location '{location.Name}' to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom location mod?)", LogLevel.Warn); + gamelocations.Remove(location); + removedAny = true; + } + } + // get building interiors var interiors = ( @@ -83,11 +105,15 @@ namespace StardewModdingAPI.Patches { 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); + removedAny = true; } } } } + if (removedAny) + LoadErrorPatch.OnContentRemoved(); + return true; } } -- cgit