using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using HarmonyLib; using Microsoft.Xna.Framework.Content; using StardewModdingAPI.Internal; using StardewModdingAPI.Internal.Patching; using StardewValley; using StardewValley.Buildings; using StardewValley.Locations; namespace StardewModdingAPI.Mods.ErrorHandler.Patches { /// Harmony patches for which prevent 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 SaveGamePatcher : BasePatcher { /********* ** Fields *********/ /// Writes messages to the console and log file. private static IMonitor Monitor = null!; /// A callback invoked when custom content is removed from the save data to avoid a crash. private static Action OnContentRemoved = null!; /********* ** Public methods *********/ /// Construct an instance. /// Writes messages to the console and log file. /// A callback invoked when custom content is removed from the save data to avoid a crash. public SaveGamePatcher(IMonitor monitor, Action onContentRemoved) { SaveGamePatcher.Monitor = monitor; SaveGamePatcher.OnContentRemoved = onContentRemoved; } /// public override void Apply(Harmony harmony, IMonitor monitor) { harmony.Patch( original: this.RequireMethod(nameof(SaveGame.loadDataToLocations)), prefix: this.GetHarmonyMethod(nameof(SaveGamePatcher.Before_LoadDataToLocations)) ); harmony.Patch( original: this.RequireMethod(nameof(SaveGame.LoadFarmType)), finalizer: this.GetHarmonyMethod(nameof(SaveGamePatcher.Finalize_LoadFarmType)) ); } /********* ** Private methods *********/ /// The method to call instead of . /// The game locations being loaded. /// Returns whether to execute the original method. private static bool Before_LoadDataToLocations(List gamelocations) { // missing locations/NPCs IDictionary npcs = Game1.content.Load>("Data\\NPCDispositions"); if (SaveGamePatcher.RemoveBrokenContent(gamelocations, npcs)) SaveGamePatcher.OnContentRemoved(); return true; } /// The method to call after throws an exception. /// The exception thrown by the wrapped method, if any. /// Returns the exception to throw, if any. private static Exception? Finalize_LoadFarmType(Exception? __exception) { // missing custom farm type if (__exception?.Message.Contains("not a valid farm type") == true && !int.TryParse(SaveGame.loaded.whichFarm, out _)) { SaveGamePatcher.Monitor.Log(__exception.GetLogSummary(), LogLevel.Error); SaveGamePatcher.Monitor.Log($"Removed invalid custom farm type '{SaveGame.loaded.whichFarm}' to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom farm type mod?)", LogLevel.Warn); SaveGame.loaded.whichFarm = Farm.default_layout.ToString(); SaveGame.LoadFarmType(); SaveGamePatcher.OnContentRemoved(); __exception = null; } return __exception; } /// Remove content which no longer exists in the game data. /// The current game locations. /// The NPC data. private static bool RemoveBrokenContent(IEnumerable locations, IDictionary npcs) { bool removedAny = false; foreach (GameLocation location in locations) removedAny |= SaveGamePatcher.RemoveBrokenContent(location, npcs); return removedAny; } /// Remove content which no longer exists in the game data. /// The current game location. /// The NPC data. private static bool RemoveBrokenContent(GameLocation? location, IDictionary npcs) { bool removedAny = false; if (location == null) return false; // check buildings if (location is BuildableGameLocation buildableLocation) { foreach (Building building in buildableLocation.buildings.ToArray()) { try { BluePrint _ = new(building.buildingType.Value); } catch (ContentLoadException) { SaveGamePatcher.Monitor.Log($"Removed invalid building type '{building.buildingType.Value}' in {location.Name} ({building.tileX}, {building.tileY}) to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom building mod?)", LogLevel.Warn); buildableLocation.buildings.Remove(building); removedAny = true; continue; } SaveGamePatcher.RemoveBrokenContent(building.indoors.Value, npcs); } } // check NPCs foreach (NPC npc in location.characters.ToArray()) { if (npc.isVillager() && !npcs.ContainsKey(npc.Name)) { try { npc.reloadSprite(); // this won't crash for special villagers like Bouncer } catch { SaveGamePatcher.Monitor.Log($"Removed invalid villager '{npc.Name}' in {location.Name} ({npc.getTileLocation()}) to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom NPC mod?)", LogLevel.Warn); location.characters.Remove(npc); removedAny = true; } } } // check objects foreach (var pair in location.objects.Pairs.ToArray()) { // SpaceCore can leave null values when removing its custom content if (pair.Value == null) { location.Objects.Remove(pair.Key); SaveGamePatcher.Monitor.Log($"Removed invalid null object in {location.Name} ({pair.Key}) to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom item mod?)", LogLevel.Warn); removedAny = true; } } return removedAny; } } }