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