From 397f338394c31e2a9bad2e90383f1ff621ae5d91 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 2 Jan 2021 22:24:45 -0500 Subject: detect and block map replacements that would crash the game due to tilesheet changes --- .../ContentManagers/GameContentManager.cs | 84 +++++++++++++++++++--- 1 file changed, 76 insertions(+), 8 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index ad8f2ef1..424d6ff3 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -11,6 +11,7 @@ using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Utilities; using StardewValley; using xTile; +using xTile.Tiles; namespace StardewModdingAPI.Framework.ContentManagers { @@ -308,15 +309,10 @@ namespace StardewModdingAPI.Framework.ContentManagers return null; } - // validate asset - if (data == null) - { - mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Error); - return null; - } - // return matched asset - return new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName); + return this.TryValidateLoadedAsset(info, data, mod) + ? new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName) + : null; } /// Apply any to a loaded asset. @@ -386,5 +382,77 @@ namespace StardewModdingAPI.Framework.ContentManagers // return result return asset; } + + /// Validate that an asset loaded by a mod is valid and won't cause issues. + /// 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) + { + // can't load a null asset + if (data == null) + { + mod.LogAsMod($"SMAPI blocked asset replacement for '{info.AssetName}': mod incorrectly set asset to a null value.", LogLevel.Error); + return false; + } + + // when replacing a map, the vanilla tilesheets must have the same order and IDs + if (data is Map loadedMap && this.Coordinator.TryLoadVanillaAsset(info.AssetName, out Map vanillaMap)) + { + for (int i = 0; i < vanillaMap.TileSheets.Count; i++) + { + // check for match + TileSheet vanillaSheet = vanillaMap.TileSheets[i]; + bool found = this.TryFindTilesheet(loadedMap, vanillaSheet.Id, out int loadedIndex, out TileSheet loadedSheet); + if (found && loadedIndex == i) + continue; + + // handle mismatch + { + // 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"); + + + string reason = found + ? $"mod reordered the original tilesheets, which {(isFarmMap ? "would cause a crash" : "often causes crashes")}.\n\nTechnical details for mod author:\nExpected order [{string.Join(", ", vanillaMap.TileSheets.Select(p => $"'{p.ImageSource}' (id: {p.Id})"))}], but found tilesheet '{vanillaSheet.Id}' at index {loadedIndex} instead of {i}. Make sure custom tilesheet IDs are prefixed with 'z_' to avoid reordering tilesheets." + : $"mod has no tilesheet with ID '{vanillaSheet.Id}'. Map replacements must keep the original tilesheets to avoid errors or crashes."; + + SCore.DeprecationManager.PlaceholderWarn("3.8.2", DeprecationLevel.PendingRemoval); + if (isFarmMap) + { + mod.LogAsMod($"SMAPI blocked asset replacement for '{info.AssetName}': {reason}", LogLevel.Error); + return false; + } + mod.LogAsMod($"SMAPI detected a potential issue with asset replacement for '{info.AssetName}' map: {reason}", LogLevel.Warn); + } + } + } + + return true; + } + + /// Find a map tilesheet by ID. + /// The map whose tilesheets to search. + /// The tilesheet ID to match. + /// The matched tilesheet index, if any. + /// The matched tilesheet, if any. + private bool TryFindTilesheet(Map map, string id, out int index, out TileSheet tilesheet) + { + for (int i = 0; i < map.TileSheets.Count; i++) + { + if (map.TileSheets[i].Id == id) + { + index = i; + tilesheet = map.TileSheets[i]; + return true; + } + } + + index = -1; + tilesheet = null; + return false; + } } } -- cgit