auto-fix save data when a custom NPC mod is removed

This commit is contained in:
Jesse Plamondon-Willard 2019-09-26 19:48:01 -04:00
parent 1b5055dfaa
commit 9461494a35
No known key found for this signature in database
GPG Key ID: CF8B1456B3E29F49
8 changed files with 127 additions and 22 deletions

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -10,6 +10,9 @@ 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>
/// <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
{
/*********
@ -29,7 +32,7 @@ namespace StardewModdingAPI.Patches
** Accessors
*********/
/// <summary>A unique name for this patch.</summary>
public string Name => $"{nameof(DialogueErrorPatch)}";
public string Name => nameof(DialogueErrorPatch);
/*********
@ -68,8 +71,6 @@ namespace StardewModdingAPI.Patches
/// <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>
/// <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_Dialogue_Constructor(Dialogue __instance, string masterDialogue, NPC speaker)
{
// get private members
@ -109,8 +110,6 @@ namespace StardewModdingAPI.Patches
/// <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)

View File

@ -7,6 +7,9 @@ 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>
/// <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 EventErrorPatch : IHarmonyPatch
{
/*********
@ -23,7 +26,7 @@ namespace StardewModdingAPI.Patches
** Accessors
*********/
/// <summary>A unique name for this patch.</summary>
public string Name => $"{nameof(EventErrorPatch)}";
public string Name => nameof(EventErrorPatch);
/*********
@ -56,8 +59,6 @@ namespace StardewModdingAPI.Patches
/// <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)

View File

@ -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
{
/// <summary>Harmony patches which notify 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>
/// <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 LoadContextPatch : IHarmonyPatch
{
/*********
@ -27,7 +30,7 @@ namespace StardewModdingAPI.Patches
** Accessors
*********/
/// <summary>A unique name for this patch.</summary>
public string Name => $"{nameof(LoadContextPatch)}";
public string Name => nameof(LoadContextPatch);
/*********

View File

@ -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
{
/// <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;
/*********
** Accessors
*********/
/// <summary>A unique name for this patch.</summary>
public string Name => nameof(LoadErrorPatch);
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="monitor">Writes messages to the console and log file.</param>
public LoadErrorPatch(IMonitor monitor)
{
LoadErrorPatch.Monitor = monitor;
}
/// <summary>Apply the Harmony patch.</summary>
/// <param name="harmony">The Harmony instance.</param>
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
*********/
/// <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)
{
// get building interiors
var interiors =
(
from location in gamelocations.OfType<BuildableGameLocation>()
from building in location.buildings
where building.indoors.Value != null
select building.indoors.Value
);
// remove custom NPCs which no longer exist
IDictionary<string, string> data = Game1.content.Load<Dictionary<string, string>>("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;
}
}
}

View File

@ -8,13 +8,16 @@ using SObject = StardewValley.Object;
namespace StardewModdingAPI.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
{
/*********
** Accessors
*********/
/// <summary>A unique name for this patch.</summary>
public string Name => $"{nameof(ObjectErrorPatch)}";
public string Name => nameof(ObjectErrorPatch);
/*********
@ -45,8 +48,6 @@ namespace StardewModdingAPI.Patches
/// <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>
/// <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_Object_GetDescription(SObject __instance, ref string __result)
{
// invalid bigcraftables crash instead of showing '???' like invalid non-bigcraftables
@ -63,8 +64,6 @@ namespace StardewModdingAPI.Patches
/// <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