using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Harmony; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Patching; using StardewValley; using StardewValley.Buildings; 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; /// A callback invoked when custom content is removed from the save data to avoid a crash. private static Action OnContentRemoved; /********* ** 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. /// 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; } /// 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) { bool removedAny = LoadErrorPatch.RemoveBrokenBuildings(gamelocations) | LoadErrorPatch.RemoveInvalidNpcs(gamelocations); if (removedAny) LoadErrorPatch.OnContentRemoved(); return true; } /// Remove buildings which don't exist in the game data. /// The current game locations. private static bool RemoveBrokenBuildings(IEnumerable locations) { bool removedAny = false; foreach (BuildableGameLocation location in locations.OfType()) { foreach (Building building in location.buildings.ToArray()) { try { BluePrint _ = new BluePrint(building.buildingType.Value); } catch (SContentLoadException) { LoadErrorPatch.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); location.buildings.Remove(building); removedAny = true; } } } return removedAny; } /// Remove NPCs which don't exist in the game data. /// The current game locations. private static bool RemoveInvalidNpcs(IEnumerable locations) { bool removedAny = false; IDictionary data = Game1.content.Load>("Data\\NPCDispositions"); foreach (GameLocation location in LoadErrorPatch.GetAllLocations(locations)) { 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}' 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; } } } } return removedAny; } /// Get all locations, including building interiors. /// The main game locations. private static IEnumerable GetAllLocations(IEnumerable locations) { foreach (GameLocation location in locations) { yield return location; if (location is BuildableGameLocation buildableLocation) { foreach (GameLocation interior in buildableLocation.buildings.Select(p => p.indoors.Value).Where(p => p != null)) yield return interior; } } } } }