summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/SMAPI/Framework/Content/AssetInterceptorChange.cs91
-rw-r--r--src/SMAPI/Framework/Content/ContentCache.cs5
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs107
-rw-r--r--src/SMAPI/Framework/ContentManagers/BaseContentManager.cs30
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManager.cs49
-rw-r--r--src/SMAPI/Framework/ContentManagers/IContentManager.cs4
-rw-r--r--src/SMAPI/Framework/ContentManagers/ModContentManager.cs30
-rw-r--r--src/SMAPI/Framework/SCore.cs27
-rw-r--r--src/SMAPI/Framework/SGame.cs50
-rw-r--r--src/SMAPI/Metadata/CoreAssetPropagator.cs92
10 files changed, 284 insertions, 201 deletions
diff --git a/src/SMAPI/Framework/Content/AssetInterceptorChange.cs b/src/SMAPI/Framework/Content/AssetInterceptorChange.cs
new file mode 100644
index 00000000..498afe36
--- /dev/null
+++ b/src/SMAPI/Framework/Content/AssetInterceptorChange.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Reflection;
+
+namespace StardewModdingAPI.Framework.Content
+{
+ /// <summary>A wrapper for <see cref="IAssetEditor"/> and <see cref="IAssetLoader"/> for internal cache invalidation.</summary>
+ internal class AssetInterceptorChange
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The mod which registered the interceptor.</summary>
+ public IModMetadata Mod { get; }
+
+ /// <summary>The interceptor instance.</summary>
+ public object Instance { get; }
+
+ /// <summary>Whether the asset interceptor was added since the last tick. Mutually exclusive with <see cref="WasRemoved"/>.</summary>
+ public bool WasAdded { get; }
+
+ /// <summary>Whether the asset interceptor was removed since the last tick. Mutually exclusive with <see cref="WasRemoved"/>.</summary>
+ public bool WasRemoved => this.WasAdded;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="mod">The mod registering the interceptor.</param>
+ /// <param name="instance">The interceptor. This must be an <see cref="IAssetEditor"/> or <see cref="IAssetLoader"/> instance.</param>
+ /// <param name="wasAdded">Whether the asset interceptor was added since the last tick; else removed.</param>
+ public AssetInterceptorChange(IModMetadata mod, object instance, bool wasAdded)
+ {
+ this.Mod = mod ?? throw new ArgumentNullException(nameof(mod));
+ this.Instance = instance ?? throw new ArgumentNullException(nameof(instance));
+ this.WasAdded = wasAdded;
+
+ if (!(instance is IAssetEditor) && !(instance is IAssetLoader))
+ throw new InvalidCastException($"The provided {nameof(instance)} value must be an {nameof(IAssetEditor)} or {nameof(IAssetLoader)} instance.");
+ }
+
+ /// <summary>Get whether this instance can intercept the given asset.</summary>
+ /// <param name="asset">Basic metadata about the asset being loaded.</param>
+ public bool CanIntercept(IAssetInfo asset)
+ {
+ MethodInfo canIntercept = this.GetType().GetMethod(nameof(this.CanInterceptImpl), BindingFlags.Instance | BindingFlags.NonPublic);
+ if (canIntercept == null)
+ throw new InvalidOperationException($"SMAPI couldn't access the {nameof(AssetInterceptorChange)}.{nameof(this.CanInterceptImpl)} implementation.");
+
+ return (bool)canIntercept.MakeGenericMethod(asset.DataType).Invoke(this, new object[] { asset });
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get whether this instance can intercept the given asset.</summary>
+ /// <typeparam name="TAsset">The asset type.</typeparam>
+ /// <param name="asset">Basic metadata about the asset being loaded.</param>
+ private bool CanInterceptImpl<TAsset>(IAssetInfo asset)
+ {
+ // check edit
+ if (this.Instance is IAssetEditor editor)
+ {
+ try
+ {
+ return editor.CanEdit<TAsset>(asset);
+ }
+ catch (Exception ex)
+ {
+ this.Mod.LogAsMod($"Mod failed when checking whether it could edit asset '{asset.AssetName}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
+ }
+ }
+
+ // check load
+ if (this.Instance is IAssetLoader loader)
+ {
+ try
+ {
+ return loader.CanLoad<TAsset>(asset);
+ }
+ catch (Exception ex)
+ {
+ this.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{asset.AssetName}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
+ }
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs
index 4178b663..f33ff84d 100644
--- a/src/SMAPI/Framework/Content/ContentCache.cs
+++ b/src/SMAPI/Framework/Content/ContentCache.cs
@@ -119,13 +119,12 @@ namespace StardewModdingAPI.Framework.Content
/// <param name="predicate">Matches the asset keys to invalidate.</param>
/// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param>
/// <returns>Returns the removed keys (if any).</returns>
- public IEnumerable<string> Remove(Func<string, Type, bool> predicate, bool dispose = false)
+ public IEnumerable<string> Remove(Func<string, object, bool> predicate, bool dispose)
{
List<string> removed = new List<string>();
foreach (string key in this.Cache.Keys.ToArray())
{
- Type type = this.Cache[key].GetType();
- if (predicate(key, type))
+ if (predicate(key, this.Cache[key]))
{
this.Remove(key, dispose);
removed.Add(key);
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs
index 08ebe6a5..82d3805b 100644
--- a/src/SMAPI/Framework/ContentCoordinator.cs
+++ b/src/SMAPI/Framework/ContentCoordinator.cs
@@ -3,11 +3,11 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Reflection;
using Microsoft.Xna.Framework.Content;
using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.ContentManagers;
using StardewModdingAPI.Framework.Reflection;
+using StardewModdingAPI.Framework.StateTracking.Comparers;
using StardewModdingAPI.Metadata;
using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Toolkit.Utilities;
@@ -188,59 +188,6 @@ namespace StardewModdingAPI.Framework
return contentManager.Load<T>(relativePath, this.DefaultLanguage, useCache: false);
}
- /// <summary>Purge assets from the cache that match one of the interceptors.</summary>
- /// <param name="editors">The asset editors for which to purge matching assets.</param>
- /// <param name="loaders">The asset loaders for which to purge matching assets.</param>
- /// <returns>Returns the invalidated asset names.</returns>
- public IEnumerable<string> InvalidateCacheFor(IAssetEditor[] editors, IAssetLoader[] loaders)
- {
- if (!editors.Any() && !loaders.Any())
- return new string[0];
-
- // get CanEdit/Load methods
- MethodInfo canEdit = typeof(IAssetEditor).GetMethod(nameof(IAssetEditor.CanEdit));
- MethodInfo canLoad = typeof(IAssetLoader).GetMethod(nameof(IAssetLoader.CanLoad));
- if (canEdit == null || canLoad == null)
- throw new InvalidOperationException("SMAPI could not access the interceptor methods."); // should never happen
-
- // invalidate matching keys
- return this.InvalidateCache(asset =>
- {
- // check loaders
- MethodInfo canLoadGeneric = canLoad.MakeGenericMethod(asset.DataType);
- foreach (IAssetLoader loader in loaders)
- {
- try
- {
- if ((bool)canLoadGeneric.Invoke(loader, new object[] { asset }))
- return true;
- }
- catch (Exception ex)
- {
- this.GetModFor(loader).LogAsMod($"Mod failed when checking whether it could load asset '{asset.AssetName}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
- }
- }
-
- // check editors
- MethodInfo canEditGeneric = canEdit.MakeGenericMethod(asset.DataType);
- foreach (IAssetEditor editor in editors)
- {
- try
- {
- if ((bool)canEditGeneric.Invoke(editor, new object[] { asset }))
- return true;
- }
- catch (Exception ex)
- {
- this.GetModFor(editor).LogAsMod($"Mod failed when checking whether it could edit asset '{asset.AssetName}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
- }
- }
-
- // asset not affected by a loader or editor
- return false;
- });
- }
-
/// <summary>Purge matched assets from the cache.</summary>
/// <param name="predicate">Matches the asset keys to invalidate.</param>
/// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param>
@@ -261,24 +208,28 @@ namespace StardewModdingAPI.Framework
/// <returns>Returns the invalidated asset names.</returns>
public IEnumerable<string> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false)
{
- // invalidate cache
- IDictionary<string, Type> removedAssetNames = new Dictionary<string, Type>(StringComparer.InvariantCultureIgnoreCase);
+ // invalidate cache & track removed assets
+ IDictionary<string, ISet<object>> removedAssets = new Dictionary<string, ISet<object>>(StringComparer.InvariantCultureIgnoreCase);
foreach (IContentManager contentManager in this.ContentManagers)
{
- foreach (Tuple<string, Type> asset in contentManager.InvalidateCache(predicate, dispose))
- removedAssetNames[asset.Item1] = asset.Item2;
+ foreach (var entry in contentManager.InvalidateCache(predicate, dispose))
+ {
+ if (!removedAssets.TryGetValue(entry.Key, out ISet<object> assets))
+ removedAssets[entry.Key] = assets = new HashSet<object>(new ObjectReferenceComparer<object>());
+ assets.Add(entry.Value);
+ }
}
// reload core game assets
- int reloaded = this.CoreAssets.Propagate(this.MainContentManager, removedAssetNames); // use an intercepted content manager
-
- // report result
- if (removedAssetNames.Any())
- this.Monitor.Log($"Invalidated {removedAssetNames.Count} asset names: {string.Join(", ", removedAssetNames.Keys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace);
+ if (removedAssets.Any())
+ {
+ IDictionary<string, bool> propagated = this.CoreAssets.Propagate(this.MainContentManager, removedAssets.ToDictionary(p => p.Key, p => p.Value.First().GetType())); // use an intercepted content manager
+ this.Monitor.Log($"Invalidated {removedAssets.Count} asset names ({string.Join(", ", removedAssets.Keys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}); propagated {propagated.Count(p => p.Value)} core assets.", LogLevel.Trace);
+ }
else
this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace);
- return removedAssetNames.Keys;
+ return removedAssets.Keys;
}
/// <summary>Dispose held resources.</summary>
@@ -308,33 +259,5 @@ namespace StardewModdingAPI.Framework
this.ContentManagers.Remove(contentManager);
}
-
- /// <summary>Get the mod which registered an asset loader.</summary>
- /// <param name="loader">The asset loader.</param>
- /// <exception cref="KeyNotFoundException">The given loader couldn't be matched to a mod.</exception>
- private IModMetadata GetModFor(IAssetLoader loader)
- {
- foreach (var pair in this.Loaders)
- {
- if (pair.Value.Contains(loader))
- return pair.Key;
- }
-
- throw new KeyNotFoundException("This loader isn't associated with a known mod.");
- }
-
- /// <summary>Get the mod which registered an asset editor.</summary>
- /// <param name="editor">The asset editor.</param>
- /// <exception cref="KeyNotFoundException">The given editor couldn't be matched to a mod.</exception>
- private IModMetadata GetModFor(IAssetEditor editor)
- {
- foreach (var pair in this.Editors)
- {
- if (pair.Value.Contains(editor))
- return pair.Key;
- }
-
- throw new KeyNotFoundException("This editor isn't associated with a known mod.");
- }
}
}
diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
index 5283340e..41ce7c37 100644
--- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
@@ -184,25 +184,25 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>Purge matched assets from the cache.</summary>
/// <param name="predicate">Matches the asset keys to invalidate.</param>
/// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param>
- /// <returns>Returns the invalidated asset names and types.</returns>
- public IEnumerable<Tuple<string, Type>> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false)
+ /// <returns>Returns the invalidated asset names and instances.</returns>
+ public IDictionary<string, object> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false)
{
- Dictionary<string, Type> removeAssetNames = new Dictionary<string, Type>(StringComparer.InvariantCultureIgnoreCase);
- this.Cache.Remove((key, type) =>
+ IDictionary<string, object> removeAssets = new Dictionary<string, object>(StringComparer.InvariantCultureIgnoreCase);
+ this.Cache.Remove((key, asset) =>
{
this.ParseCacheKey(key, out string assetName, out _);
- if (removeAssetNames.ContainsKey(assetName))
+ if (removeAssets.ContainsKey(assetName))
return true;
- if (predicate(assetName, type))
+ if (predicate(assetName, asset.GetType()))
{
- removeAssetNames[assetName] = type;
+ removeAssets[assetName] = asset;
return true;
}
return false;
- });
+ }, dispose);
- return removeAssetNames.Select(p => Tuple.Create(p.Key, p.Value));
+ return removeAssets;
}
/// <summary>Dispose held resources.</summary>
@@ -258,20 +258,24 @@ namespace StardewModdingAPI.Framework.ContentManagers
: base.ReadAsset<T>(assetName, disposable => this.Disposables.Add(new WeakReference<IDisposable>(disposable)));
}
- /// <summary>Inject an asset into the cache.</summary>
+ /// <summary>Add tracking data to an asset and add it to the cache.</summary>
/// <typeparam name="T">The type of asset to inject.</typeparam>
/// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
/// <param name="value">The asset value.</param>
/// <param name="language">The language code for which to inject the asset.</param>
- protected virtual void Inject<T>(string assetName, T value, LanguageCode language)
+ /// <param name="useCache">Whether to save the asset to the asset cache.</param>
+ protected virtual void TrackAsset<T>(string assetName, T value, LanguageCode language, bool useCache)
{
// track asset key
if (value is Texture2D texture)
texture.Name = assetName;
// cache asset
- assetName = this.AssertAndNormalizeAssetName(assetName);
- this.Cache[assetName] = value;
+ if (useCache)
+ {
+ assetName = this.AssertAndNormalizeAssetName(assetName);
+ this.Cache[assetName] = value;
+ }
}
/// <summary>Parse a cache key into its component parts.</summary>
diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
index 0b563555..8930267d 100644
--- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
@@ -83,8 +83,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
if (this.Coordinator.TryParseManagedAssetKey(assetName, out string contentManagerID, out string relativePath))
{
T managedAsset = this.Coordinator.LoadManagedAsset<T>(contentManagerID, relativePath);
- if (useCache)
- this.Inject(assetName, managedAsset, language);
+ this.TrackAsset(assetName, managedAsset, language, useCache);
return managedAsset;
}
@@ -111,7 +110,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
}
// update cache & return data
- this.Inject(assetName, data, language);
+ this.TrackAsset(assetName, data, language, useCache);
return data;
}
@@ -131,7 +130,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
removeAssetNames.Contains(key)
|| (this.TryParseExplicitLanguageAssetKey(key, out string assetName, out _) && removeAssetNames.Contains(assetName))
)
- .Select(p => p.Item1)
+ .Select(p => p.Key)
.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase)
.ToArray();
if (invalidated.Any())
@@ -169,18 +168,19 @@ namespace StardewModdingAPI.Framework.ContentManagers
return false;
}
- /// <summary>Inject an asset into the cache.</summary>
+ /// <summary>Add tracking data to an asset and add it to the cache.</summary>
/// <typeparam name="T">The type of asset to inject.</typeparam>
/// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
/// <param name="value">The asset value.</param>
/// <param name="language">The language code for which to inject the asset.</param>
- protected override void Inject<T>(string assetName, T value, LanguageCode language)
+ /// <param name="useCache">Whether to save the asset to the asset cache.</param>
+ protected override void TrackAsset<T>(string assetName, T value, LanguageCode language, bool useCache)
{
// handle explicit language in asset name
{
if (this.TryParseExplicitLanguageAssetKey(assetName, out string newAssetName, out LanguageCode newLanguage))
{
- this.Inject(newAssetName, value, newLanguage);
+ this.TrackAsset(newAssetName, value, newLanguage, useCache);
return;
}
}
@@ -192,24 +192,27 @@ namespace StardewModdingAPI.Framework.ContentManagers
// only caches by the most specific key).
// 2. Because a mod asset loader/editor may have changed the asset in a way that
// doesn't change the instance stored in the cache, e.g. using `asset.ReplaceWith`.
- string keyWithLocale = $"{assetName}.{this.GetLocale(language)}";
- base.Inject(assetName, value, language);
- if (this.Cache.ContainsKey(keyWithLocale))
- base.Inject(keyWithLocale, value, language);
-
- // track whether the injected asset is translatable for is-loaded lookups
- if (this.Cache.ContainsKey(keyWithLocale))
- {
- this.IsLocalizableLookup[assetName] = true;
- this.IsLocalizableLookup[keyWithLocale] = true;
- }
- else if (this.Cache.ContainsKey(assetName))
+ if (useCache)
{
- this.IsLocalizableLookup[assetName] = false;
- this.IsLocalizableLookup[keyWithLocale] = false;
+ string keyWithLocale = $"{assetName}.{this.GetLocale(language)}";
+ base.TrackAsset(assetName, value, language, useCache: true);
+ if (this.Cache.ContainsKey(keyWithLocale))
+ base.TrackAsset(keyWithLocale, value, language, useCache: true);
+
+ // track whether the injected asset is translatable for is-loaded lookups
+ if (this.Cache.ContainsKey(keyWithLocale))
+ {
+ this.IsLocalizableLookup[assetName] = true;
+ this.IsLocalizableLookup[keyWithLocale] = true;
+ }
+ else if (this.Cache.ContainsKey(assetName))
+ {
+ this.IsLocalizableLookup[assetName] = false;
+ this.IsLocalizableLookup[keyWithLocale] = false;
+ }
+ else
+ this.Monitor.Log($"Asset '{assetName}' could not be found in the cache immediately after injection.", LogLevel.Error);
}
- else
- this.Monitor.Log($"Asset '{assetName}' could not be found in the cache immediately after injection.", LogLevel.Error);
}
/// <summary>Load an asset file directly from the underlying content manager.</summary>
diff --git a/src/SMAPI/Framework/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs
index 12c01352..8da9a777 100644
--- a/src/SMAPI/Framework/ContentManagers/IContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/IContentManager.cs
@@ -66,7 +66,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>Purge matched assets from the cache.</summary>
/// <param name="predicate">Matches the asset keys to invalidate.</param>
/// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param>
- /// <returns>Returns the invalidated asset names and types.</returns>
- IEnumerable<Tuple<string, Type>> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false);
+ /// <returns>Returns the invalidated asset names and instances.</returns>
+ IDictionary<string, object> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false);
}
}
diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
index 90b86179..fdf76b24 100644
--- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
@@ -105,6 +105,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
// get local asset
SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}");
+ T asset;
try
{
// get file
@@ -118,22 +119,22 @@ namespace StardewModdingAPI.Framework.ContentManagers
// XNB file
case ".xnb":
{
- T data = this.RawLoad<T>(assetName, useCache: false);
- if (data is Map map)
+ asset = this.RawLoad<T>(assetName, useCache: false);
+ if (asset is Map map)
{
this.NormalizeTilesheetPaths(map);
this.FixCustomTilesheetPaths(map, relativeMapPath: assetName);
}
- return data;
}
+ break;
// unpacked data
case ".json":
{
- if (!this.JsonHelper.ReadJsonFileIfExists(file.FullName, out T data))
+ if (!this.JsonHelper.ReadJsonFileIfExists(file.FullName, out asset))
throw GetContentError("the JSON file is invalid."); // should never happen since we check for file existence above
- return data;
}
+ break;
// unpacked image
case ".png":
@@ -143,13 +144,13 @@ namespace StardewModdingAPI.Framework.ContentManagers
throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'.");
// fetch & cache
- using (FileStream stream = File.OpenRead(file.FullName))
- {
- Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream);
- texture = this.PremultiplyTransparency(texture);
- return (T)(object)texture;
- }
+ using FileStream stream = File.OpenRead(file.FullName);
+
+ Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream);
+ texture = this.PremultiplyTransparency(texture);
+ asset = (T)(object)texture;
}
+ break;
// unpacked map
case ".tbin":
@@ -163,8 +164,9 @@ namespace StardewModdingAPI.Framework.ContentManagers
Map map = formatManager.LoadMap(file.FullName);
this.NormalizeTilesheetPaths(map);
this.FixCustomTilesheetPaths(map, relativeMapPath: assetName);
- return (T)(object)map;
+ asset = (T)(object)map;
}
+ break;
default:
throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.json', '.png', '.tbin', or '.xnb'.");
@@ -176,6 +178,10 @@ namespace StardewModdingAPI.Framework.ContentManagers
throw GetContentError("couldn't find libgdiplus, which is needed to load mod images. Make sure Mono is installed and you're running the game through the normal launcher.");
throw new SContentLoadException($"The content manager failed loading content asset '{assetName}' from {this.Name}.", ex);
}
+
+ // track & return asset
+ this.TrackAsset(assetName, asset, language, useCache);
+ return asset;
}
/// <summary>Create a new content manager for temporary use.</summary>
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index f1873391..fb3506b4 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -842,34 +842,11 @@ namespace StardewModdingAPI.Framework
{
if (metadata.Mod.Helper.Content is ContentHelper helper)
{
- helper.ObservableAssetEditors.CollectionChanged += (sender, e) =>
- {
- if (e.NewItems?.Count > 0)
- {
- this.Monitor.Log("Invalidating cache entries for new asset editors...", LogLevel.Trace);
- this.ContentCore.InvalidateCacheFor(e.NewItems.Cast<IAssetEditor>().ToArray(), new IAssetLoader[0]);
- }
- };
- helper.ObservableAssetLoaders.CollectionChanged += (sender, e) =>
- {
- if (e.NewItems?.Count > 0)
- {
- this.Monitor.Log("Invalidating cache entries for new asset loaders...", LogLevel.Trace);
- this.ContentCore.InvalidateCacheFor(new IAssetEditor[0], e.NewItems.Cast<IAssetLoader>().ToArray());
- }
- };
+ helper.ObservableAssetEditors.CollectionChanged += (sender, e) => this.GameInstance.OnAssetInterceptorsChanged(metadata, e.NewItems, e.OldItems);
+ helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => this.GameInstance.OnAssetInterceptorsChanged(metadata, e.NewItems, e.OldItems);
}
}
- // reset cache now if any editors or loaders were added during entry
- IAssetEditor[] editors = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetEditors).ToArray();
- IAssetLoader[] loaders = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetLoaders).ToArray();
- if (editors.Any() || loaders.Any())
- {
- this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace);
- this.ContentCore.InvalidateCacheFor(editors, loaders);
- }
-
// unlock mod integrations
this.ModRegistry.AreAllModsInitialized = true;
}
diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs
index 47261862..4774233e 100644
--- a/src/SMAPI/Framework/SGame.cs
+++ b/src/SMAPI/Framework/SGame.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@@ -12,6 +13,7 @@ using Microsoft.Xna.Framework.Graphics;
using Netcode;
using StardewModdingAPI.Enums;
using StardewModdingAPI.Events;
+using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.Events;
using StardewModdingAPI.Framework.Input;
using StardewModdingAPI.Framework.Networking;
@@ -99,7 +101,7 @@ namespace StardewModdingAPI.Framework
private WatcherCore Watchers;
/// <summary>A snapshot of the current <see cref="Watchers"/> state.</summary>
- private WatcherSnapshot WatcherSnapshot = new WatcherSnapshot();
+ private readonly WatcherSnapshot WatcherSnapshot = new WatcherSnapshot();
/// <summary>Whether post-game-startup initialization has been performed.</summary>
private bool IsInitialized;
@@ -133,6 +135,9 @@ namespace StardewModdingAPI.Framework
/// <remarks>This property must be threadsafe, since it's accessed from a separate console input thread.</remarks>
public ConcurrentQueue<string> CommandQueue { get; } = new ConcurrentQueue<string>();
+ /// <summary>Asset interceptors added or removed since the last tick.</summary>
+ private readonly List<AssetInterceptorChange> ReloadAssetInterceptorsQueue = new List<AssetInterceptorChange>();
+
/*********
** Protected methods
@@ -249,6 +254,24 @@ namespace StardewModdingAPI.Framework
this.Events.ReturnedToTitle.RaiseEmpty();
}
+ /// <summary>A callback invoked when a mod adds or removes an asset interceptor.</summary>
+ /// <param name="mod">The mod which added or removed interceptors.</param>
+ /// <param name="added">The added interceptors.</param>
+ /// <param name="removed">The removed interceptors.</param>
+ internal void OnAssetInterceptorsChanged(IModMetadata mod, IEnumerable added, IEnumerable removed)
+ {
+ if (added != null)
+ {
+ foreach (object instance in added)
+ this.ReloadAssetInterceptorsQueue.Add(new AssetInterceptorChange(mod, instance, wasAdded: true));
+ }
+ if (removed != null)
+ {
+ foreach (object instance in removed)
+ this.ReloadAssetInterceptorsQueue.Add(new AssetInterceptorChange(mod, instance, wasAdded: false));
+ }
+ }
+
/// <summary>Constructor a content manager to read XNB files.</summary>
/// <param name="serviceProvider">The service provider to use to locate services.</param>
/// <param name="rootDirectory">The root directory to search for content.</param>
@@ -404,6 +427,31 @@ namespace StardewModdingAPI.Framework
return;
}
+
+ /*********
+ ** Reload assets when interceptors are added/removed
+ *********/
+ if (this.ReloadAssetInterceptorsQueue.Any())
+ {
+ this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace);
+ this.Monitor.Log(
+ "changed: "
+ + string.Join(", ",
+ this.ReloadAssetInterceptorsQueue
+ .GroupBy(p => p.Mod)
+ .OrderBy(p => p.Key.DisplayName)
+ .Select(modGroup =>
+ $"{modGroup.Key.DisplayName} ("
+ + string.Join(", ", modGroup.GroupBy(p => p.WasAdded).ToDictionary(p => p.Key, p => p.Count()).Select(p => $"{(p.Key ? "added" : "removed")} {p.Value}"))
+ + ")"
+ )
+ )
+ );
+
+ this.ContentCore.InvalidateCache(asset => this.ReloadAssetInterceptorsQueue.Any(p => p.CanIntercept(asset)));
+ this.ReloadAssetInterceptorsQueue.Clear();
+ }
+
/*********
** Execute commands
*********/
diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs
index 8b00d893..97093636 100644
--- a/src/SMAPI/Metadata/CoreAssetPropagator.cs
+++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Xna.Framework.Graphics;
+using Netcode;
using StardewModdingAPI.Framework.Reflection;
using StardewValley;
using StardewValley.BellsAndWhistles;
@@ -11,6 +12,7 @@ using StardewValley.Characters;
using StardewValley.GameData.Movies;
using StardewValley.Locations;
using StardewValley.Menus;
+using StardewValley.Network;
using StardewValley.Objects;
using StardewValley.Projectiles;
using StardewValley.TerrainFeatures;
@@ -65,8 +67,8 @@ namespace StardewModdingAPI.Metadata
/// <summary>Reload one of the game's core assets (if applicable).</summary>
/// <param name="content">The content manager through which to reload the asset.</param>
/// <param name="assets">The asset keys and types to reload.</param>
- /// <returns>Returns the number of reloaded assets.</returns>
- public int Propagate(LocalizedContentManager content, IDictionary<string, Type> assets)
+ /// <returns>Returns a lookup of asset names to whether they've been propagated.</returns>
+ public IDictionary<string, bool> Propagate(LocalizedContentManager content, IDictionary<string, Type> assets)
{
// group into optimized lists
var buckets = assets.GroupBy(p =>
@@ -81,25 +83,26 @@ namespace StardewModdingAPI.Metadata
});
// reload assets
- int reloaded = 0;
+ IDictionary<string, bool> propagated = assets.ToDictionary(p => p.Key, p => false, StringComparer.InvariantCultureIgnoreCase);
foreach (var bucket in buckets)
{
switch (bucket.Key)
{
case AssetBucket.Sprite:
- reloaded += this.ReloadNpcSprites(content, bucket.Select(p => p.Key));
+ this.ReloadNpcSprites(content, bucket.Select(p => p.Key), propagated);
break;
case AssetBucket.Portrait:
- reloaded += this.ReloadNpcPortraits(content, bucket.Select(p => p.Key));
+ this.ReloadNpcPortraits(content, bucket.Select(p => p.Key), propagated);
break;
default:
- reloaded += bucket.Count(p => this.PropagateOther(content, p.Key, p.Value));
+ foreach (var entry in bucket)
+ propagated[entry.Key] = this.PropagateOther(content, entry.Key, entry.Value);
break;
}
}
- return reloaded;
+ return propagated;
}
@@ -226,6 +229,29 @@ namespace StardewModdingAPI.Metadata
Game1.bigCraftablesInformation = content.Load<Dictionary<int, string>>(key);
return true;
+ case "data\\bundles": // NetWorldState constructor
+ {
+ var bundles = this.Reflection.GetField<NetBundles>(Game1.netWorldState.Value, "bundles").GetValue();
+ var rewards = this.Reflection.GetField<NetIntDictionary<bool, NetBool>>(Game1.netWorldState.Value, "bundleRewards").GetValue();
+ foreach (var pair in content.Load<Dictionary<string, string>>(key))
+ {
+ int bundleKey = int.Parse(pair.Key.Split('/')[1]);
+ int rewardsCount = pair.Value.Split('/')[2].Split(' ').Length;
+
+ // add bundles
+ if (bundles.TryGetValue(bundleKey, out bool[] values))
+ bundles.Remove(bundleKey);
+ else
+ values = new bool[0];
+ bundles[bundleKey] = values.Concat(Enumerable.Repeat(false, rewardsCount - values.Length)).ToArray();
+
+ // add bundle rewards
+ if (!rewards.ContainsKey(bundleKey))
+ rewards[bundleKey] = false;
+ }
+ }
+ break;
+
case "data\\clothinginformation": // Game1.LoadContent
Game1.clothingInformation = content.Load<Dictionary<int, string>>(key);
return true;
@@ -750,51 +776,57 @@ namespace StardewModdingAPI.Metadata
/// <summary>Reload the sprites for matching NPCs.</summary>
/// <param name="content">The content manager through which to reload the asset.</param>
/// <param name="keys">The asset keys to reload.</param>
- /// <returns>Returns the number of reloaded assets.</returns>
- private int ReloadNpcSprites(LocalizedContentManager content, IEnumerable<string> keys)
+ /// <param name="propagated">The asset keys which have been propagated.</param>
+ private void ReloadNpcSprites(LocalizedContentManager content, IEnumerable<string> keys, IDictionary<string, bool> propagated)
{
// get NPCs
HashSet<string> lookup = new HashSet<string>(keys, StringComparer.InvariantCultureIgnoreCase);
- NPC[] characters = this.GetCharacters()
- .Where(npc => npc.Sprite != null && lookup.Contains(this.NormalizeAssetNameIgnoringEmpty(npc.Sprite?.Texture?.Name)))
+ var characters =
+ (
+ from npc in this.GetCharacters()
+ let key = this.NormalizeAssetNameIgnoringEmpty(npc.Sprite?.Texture?.Name)
+ where key != null && lookup.Contains(key)
+ select new { Npc = npc, Key = key }
+ )
.ToArray();
if (!characters.Any())
- return 0;
+ return;
// update sprite
- int reloaded = 0;
- foreach (NPC npc in characters)
+ foreach (var target in characters)
{
- this.SetSpriteTexture(npc.Sprite, content.Load<Texture2D>(npc.Sprite.textureName.Value));
- reloaded++;
+ this.SetSpriteTexture(target.Npc.Sprite, content.Load<Texture2D>(target.Key));
+ propagated[target.Key] = true;
}
-
- return reloaded;
}
/// <summary>Reload the portraits for matching NPCs.</summary>
/// <param name="content">The content manager through which to reload the asset.</param>
/// <param name="keys">The asset key to reload.</param>
- /// <returns>Returns the number of reloaded assets.</returns>
- private int ReloadNpcPortraits(LocalizedContentManager content, IEnumerable<string> keys)
+ /// <param name="propagated">The asset keys which have been propagated.</param>
+ private void ReloadNpcPortraits(LocalizedContentManager content, IEnumerable<string> keys, IDictionary<string, bool> propagated)
{
// get NPCs
HashSet<string> lookup = new HashSet<string>(keys, StringComparer.InvariantCultureIgnoreCase);
- var villagers = this
- .GetCharacters()
- .Where(npc => npc.isVillager() && lookup.Contains(this.NormalizeAssetNameIgnoringEmpty(npc.Portrait?.Name)))
+ var characters =
+ (
+ from npc in this.GetCharacters()
+ where npc.isVillager()
+
+ let key = this.NormalizeAssetNameIgnoringEmpty(npc.Portrait?.Name)
+ where key != null && lookup.Contains(key)
+ select new { Npc = npc, Key = key }
+ )
.ToArray();
- if (!villagers.Any())
- return 0;
+ if (!characters.Any())
+ return;
// update portrait
- int reloaded = 0;
- foreach (NPC npc in villagers)
+ foreach (var target in characters)
{
- npc.Portrait = content.Load<Texture2D>(npc.Portrait.Name);
- reloaded++;
+ target.Npc.Portrait = content.Load<Texture2D>(target.Key);
+ propagated[target.Key] = true;
}
- return reloaded;
}
/// <summary>Reload tree textures.</summary>