diff options
-rw-r--r-- | docs/release-notes.md | 2 | ||||
-rw-r--r-- | src/SMAPI/Framework/ContentCoordinator.cs | 23 | ||||
-rw-r--r-- | src/SMAPI/Framework/ContentManagers/GameContentManager.cs | 84 | ||||
-rw-r--r-- | src/SMAPI/Framework/DeprecationManager.cs | 5 |
4 files changed, 106 insertions, 8 deletions
diff --git a/docs/release-notes.md b/docs/release-notes.md index 9fbfcbb3..e671f373 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,7 @@ ## Upcoming release * For players: * Updated compatibility list. + * SMAPI now blocks farm map replacements that would crash the game in Stardew Valley 1.5. * On Linux, the SMAPI installer now auto-detects flatpak Steam paths. * Fixed errors when multiple players join in split-screen mode. * Fixed 'skipped mods' section repeating mods in some cases. @@ -18,6 +19,7 @@ * For modders: * When a mod is blocked by SMAPI's internal compatibility list, the `TRACE` messages while loading it now indicates that and specifies the reason. * Message data from the `ModMessageReceived` event now uses the same serializer settings as the rest of SMAPI. This mainly adds support for sending crossplatform `Color`, `Point`, `Vector2`, `Rectangle`, and `SemanticVersion` fields through network messages. + * Added warning when a map replacement changes the tilesheet order/IDs, which may cause crashes. * Fixed how the input API handles UI scaling. This mainly affects `ICursorPosition` values returned by the API; see [the wiki docs](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Input#ICursorPosition) for how to account for UI scaling. ## 3.8.1 diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index f9027972..3d5bb29d 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -54,6 +54,9 @@ namespace StardewModdingAPI.Framework /// <remarks>The game may adds content managers in asynchronous threads (e.g. when populating the load screen).</remarks> private readonly ReaderWriterLockSlim ContentManagerLock = new ReaderWriterLockSlim(); + /// <summary>An unmodified content manager which doesn't intercept assets, used to compare asset data.</summary> + private readonly LocalizedContentManager VanillaContentManager; + /********* ** Accessors @@ -95,6 +98,7 @@ namespace StardewModdingAPI.Framework this.ContentManagers.Add( this.MainContentManager = new GameContentManager("Game1.content", serviceProvider, rootDirectory, currentCulture, this, monitor, reflection, this.OnDisposing, onLoadingFirstAsset) ); + this.VanillaContentManager = new LocalizedContentManager(serviceProvider, rootDirectory); this.CoreAssets = new CoreAssetPropagator(this.MainContentManager.AssertAndNormalizeAssetName, reflection); } @@ -150,6 +154,8 @@ namespace StardewModdingAPI.Framework { foreach (IContentManager contentManager in this.ContentManagers) contentManager.OnLocaleChanged(); + + this.VanillaContentManager.Unload(); }); } @@ -287,6 +293,23 @@ namespace StardewModdingAPI.Framework }); } + /// <summary>Get a vanilla asset without interception.</summary> + /// <typeparam name="T">The type of asset to load.</typeparam> + /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> + public bool TryLoadVanillaAsset<T>(string assetName, out T asset) + { + try + { + asset = this.VanillaContentManager.Load<T>(assetName); + return true; + } + catch + { + asset = default; + return false; + } + } + /// <summary>Dispose held resources.</summary> public void Dispose() { 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; } /// <summary>Apply any <see cref="Editors"/> to a loaded asset.</summary> @@ -386,5 +382,77 @@ namespace StardewModdingAPI.Framework.ContentManagers // return result return asset; } + + /// <summary>Validate that an asset loaded by a mod is valid and won't cause issues.</summary> + /// <typeparam name="T">The asset type.</typeparam> + /// <param name="info">The basic asset metadata.</param> + /// <param name="data">The loaded asset data.</param> + /// <param name="mod">The mod which loaded the asset.</param> + private bool TryValidateLoadedAsset<T>(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; + } + + /// <summary>Find a map tilesheet by ID.</summary> + /// <param name="map">The map whose tilesheets to search.</param> + /// <param name="id">The tilesheet ID to match.</param> + /// <param name="index">The matched tilesheet index, if any.</param> + /// <param name="tilesheet">The matched tilesheet, if any.</param> + 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; + } } } diff --git a/src/SMAPI/Framework/DeprecationManager.cs b/src/SMAPI/Framework/DeprecationManager.cs index c22b5718..fc1b434b 100644 --- a/src/SMAPI/Framework/DeprecationManager.cs +++ b/src/SMAPI/Framework/DeprecationManager.cs @@ -63,6 +63,11 @@ namespace StardewModdingAPI.Framework this.QueuedWarnings.Add(new DeprecationWarning(source, nounPhrase, version, severity, Environment.StackTrace)); } + /// <summary>A placeholder method used to track deprecated code for which a separate warning will be shown.</summary> + /// <param name="version">The SMAPI version which deprecated it.</param> + /// <param name="severity">How deprecated the code is.</param> + public void PlaceholderWarn(string version, DeprecationLevel severity) { } + /// <summary>Print any queued messages.</summary> public void PrintQueued() { |