summaryrefslogtreecommitdiff
path: root/src/SMAPI
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI')
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs23
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManager.cs84
-rw-r--r--src/SMAPI/Framework/DeprecationManager.cs5
3 files changed, 104 insertions, 8 deletions
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()
{