From 0d7d4476004d33b395d6df81386e4159d8898027 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 20 Dec 2021 22:18:09 -0500 Subject: auto-fix maps broken due to missing vanilla tilesheet --- docs/release-notes.md | 1 + src/SMAPI.sln.DotSettings | 1 + src/SMAPI/Framework/Content/TilesheetReference.cs | 15 ++++++++- src/SMAPI/Framework/ContentCoordinator.cs | 4 +-- .../ContentManagers/GameContentManager.cs | 39 +++++++++------------- 5 files changed, 33 insertions(+), 27 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index e0c6977d..caa5cc68 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,6 +3,7 @@ # Release notes ## Upcoming release * For players: + * SMAPI now auto-fixes maps loaded without a required tilesheet to prevent errors. * Fixed outdated instructions in Steam error message. * Simplified [running without a terminal on Linux/macOS](https://stardewvalleywiki.com/Modding:Player_Guide/Troubleshooting#SMAPI_doesn.27t_recognize_controller_.28Steam_only.29) when needed. * Updated compatibility list. diff --git a/src/SMAPI.sln.DotSettings b/src/SMAPI.sln.DotSettings index 9a6cad37..71cd7b82 100644 --- a/src/SMAPI.sln.DotSettings +++ b/src/SMAPI.sln.DotSettings @@ -70,6 +70,7 @@ True True True + True True True \ No newline at end of file diff --git a/src/SMAPI/Framework/Content/TilesheetReference.cs b/src/SMAPI/Framework/Content/TilesheetReference.cs index 2ea38430..0919bb44 100644 --- a/src/SMAPI/Framework/Content/TilesheetReference.cs +++ b/src/SMAPI/Framework/Content/TilesheetReference.cs @@ -1,3 +1,6 @@ +using System.Numerics; +using xTile.Dimensions; + namespace StardewModdingAPI.Framework.Content { /// Basic metadata about a vanilla tilesheet. @@ -15,6 +18,12 @@ namespace StardewModdingAPI.Framework.Content /// The asset path for the tilesheet texture. public readonly string ImageSource; + /// The number of tiles in the tilesheet. + public readonly Size SheetSize; + + /// The size of each tile in pixels. + public readonly Size TileSize; + /********* ** Public methods @@ -23,11 +32,15 @@ namespace StardewModdingAPI.Framework.Content /// The tilesheet's index in the list. /// The tilesheet's unique ID in the map. /// The asset path for the tilesheet texture. - public TilesheetReference(int index, string id, string imageSource) + /// The number of tiles in the tilesheet. + /// The size of each tile in pixels. + public TilesheetReference(int index, string id, string imageSource, Size sheetSize, Size tileSize) { this.Index = index; this.Id = id; this.ImageSource = imageSource; + this.SheetSize = sheetSize; + this.TileSize = tileSize; } } } diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index b6f1669a..99091f3e 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -406,14 +406,14 @@ namespace StardewModdingAPI.Framework if (!this.VanillaTilesheets.TryGetValue(assetName, out TilesheetReference[] tilesheets)) { tilesheets = this.TryLoadVanillaAsset(assetName, out Map map) - ? map.TileSheets.Select((sheet, index) => new TilesheetReference(index, sheet.Id, sheet.ImageSource)).ToArray() + ? map.TileSheets.Select((sheet, index) => new TilesheetReference(index, sheet.Id, sheet.ImageSource, sheet.SheetSize, sheet.TileSize)).ToArray() : null; this.VanillaTilesheets[assetName] = tilesheets; this.VanillaContentManager.Unload(); } - return tilesheets ?? new TilesheetReference[0]; + return tilesheets ?? Array.Empty(); } /// Get the language enum which corresponds to a locale code (e.g. given fr-FR). diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 7a49dd36..ab198076 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -13,6 +13,7 @@ using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Internal; using StardewValley; using xTile; +using xTile.Tiles; namespace StardewModdingAPI.Framework.ContentManagers { @@ -308,7 +309,7 @@ namespace StardewModdingAPI.Framework.ContentManagers } // return matched asset - return this.TryValidateLoadedAsset(info, data, mod) + return this.TryFixAndValidateLoadedAsset(info, data, mod) ? new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName) : null; } @@ -381,12 +382,13 @@ namespace StardewModdingAPI.Framework.ContentManagers return asset; } - /// Validate that an asset loaded by a mod is valid and won't cause issues. + /// Validate that an asset loaded by a mod is valid and won't cause issues, and fix issues if possible. /// The asset type. /// The basic asset metadata. /// The loaded asset data. /// The mod which loaded the asset. - private bool TryValidateLoadedAsset(IAssetInfo info, T data, IModMetadata mod) + /// Returns whether the asset passed validation checks (after any fixes were applied). + private bool TryFixAndValidateLoadedAsset(IAssetInfo info, T data, IModMetadata mod) { // can't load a null asset if (data == null) @@ -401,20 +403,23 @@ namespace StardewModdingAPI.Framework.ContentManagers TilesheetReference[] vanillaTilesheetRefs = this.Coordinator.GetVanillaTilesheetIds(info.AssetName); foreach (TilesheetReference vanillaSheet in vanillaTilesheetRefs) { - // skip if match - if (loadedMap.TileSheets.Count > vanillaSheet.Index && loadedMap.TileSheets[vanillaSheet.Index].Id == vanillaSheet.Id) - continue; + // add missing tilesheet + if (loadedMap.GetTileSheet(vanillaSheet.Id) == null) + { + mod.Monitor.LogOnce("SMAPI fixed maps loaded by this mod to prevent errors. See the log file for details.", LogLevel.Warn); + this.Monitor.Log($"Fixed broken map replacement: {mod.DisplayName} loaded '{info.AssetName}' without a required tilesheet (id: {vanillaSheet.Id}, source: {vanillaSheet.ImageSource})."); + + loadedMap.AddTileSheet(new TileSheet(vanillaSheet.Id, loadedMap, vanillaSheet.ImageSource, vanillaSheet.SheetSize, vanillaSheet.TileSize)); + } // handle mismatch + if (loadedMap.TileSheets.Count <= vanillaSheet.Index || loadedMap.TileSheets[vanillaSheet.Index].Id != vanillaSheet.Id) { // only show warning if not farm map // This is temporary: mods shouldn't do this for any vanilla map, but these are the ones we know will crash. Showing a warning for others instead gives modders time to update their mods, while still simplifying troubleshooting. bool isFarmMap = info.AssetNameEquals("Maps/Farm") || info.AssetNameEquals("Maps/Farm_Combat") || info.AssetNameEquals("Maps/Farm_Fishing") || info.AssetNameEquals("Maps/Farm_Foraging") || info.AssetNameEquals("Maps/Farm_FourCorners") || info.AssetNameEquals("Maps/Farm_Island") || info.AssetNameEquals("Maps/Farm_Mining"); - int loadedIndex = this.TryFindTilesheet(loadedMap, vanillaSheet.Id); - string reason = loadedIndex != -1 - ? $"mod reordered the original tilesheets, which {(isFarmMap ? "would cause a crash" : "often causes crashes")}.\nTechnical details for mod author: Expected order: {string.Join(", ", vanillaTilesheetRefs.Select(p => p.Id))}. See https://stardewvalleywiki.com/Modding:Maps#Tilesheet_order for help." - : $"mod has no tilesheet with ID '{vanillaSheet.Id}'. Map replacements must keep the original tilesheets to avoid errors or crashes."; + string reason = $"mod reordered the original tilesheets, which {(isFarmMap ? "would cause a crash" : "often causes crashes")}.\nTechnical details for mod author: Expected order: {string.Join(", ", vanillaTilesheetRefs.Select(p => p.Id))}. See https://stardewvalleywiki.com/Modding:Maps#Tilesheet_order for help."; SCore.DeprecationManager.PlaceholderWarn("3.8.2", DeprecationLevel.PendingRemoval); if (isFarmMap) @@ -429,19 +434,5 @@ namespace StardewModdingAPI.Framework.ContentManagers return true; } - - /// Find a map tilesheet by ID. - /// The map whose tilesheets to search. - /// The tilesheet ID to match. - private int TryFindTilesheet(Map map, string id) - { - for (int i = 0; i < map.TileSheets.Count; i++) - { - if (map.TileSheets[i].Id == id) - return i; - } - - return -1; - } } } -- cgit