let mods invalidate cached assets by name or type (#335)

This commit is contained in:
Jesse Plamondon-Willard 2017-07-23 17:36:31 -04:00
parent 4ea6a4102b
commit 467ad2ffd4
4 changed files with 59 additions and 10 deletions

View File

@ -33,6 +33,9 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>The friendly mod name for use in errors.</summary> /// <summary>The friendly mod name for use in errors.</summary>
private readonly string ModName; private readonly string ModName;
/// <summary>Encapsulates monitoring and logging for a given module.</summary>
private readonly IMonitor Monitor;
/********* /*********
** Accessors ** Accessors
@ -58,13 +61,15 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <param name="modFolderPath">The absolute path to the mod folder.</param> /// <param name="modFolderPath">The absolute path to the mod folder.</param>
/// <param name="modID">The unique ID of the relevant mod.</param> /// <param name="modID">The unique ID of the relevant mod.</param>
/// <param name="modName">The friendly mod name for use in errors.</param> /// <param name="modName">The friendly mod name for use in errors.</param>
public ContentHelper(SContentManager contentManager, string modFolderPath, string modID, string modName) /// <param name="monitor">Encapsulates monitoring and logging.</param>
public ContentHelper(SContentManager contentManager, string modFolderPath, string modID, string modName, IMonitor monitor)
: base(modID) : base(modID)
{ {
this.ContentManager = contentManager; this.ContentManager = contentManager;
this.ModFolderPath = modFolderPath; this.ModFolderPath = modFolderPath;
this.ModName = modName; this.ModName = modName;
this.ModFolderPathFromContent = this.GetRelativePath(contentManager.FullRootDirectory, modFolderPath); this.ModFolderPathFromContent = this.GetRelativePath(contentManager.FullRootDirectory, modFolderPath);
this.Monitor = monitor;
} }
/// <summary>Load content from the game folder or mod folder (if not already cached), and return it. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary> /// <summary>Load content from the game folder or mod folder (if not already cached), and return it. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary>
@ -176,6 +181,26 @@ namespace StardewModdingAPI.Framework.ModHelpers
} }
} }
/// <summary>Remove an asset from the content cache so it's reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content.</summary>
/// <param name="key">The asset key to fetch (if the <paramref name="source"/> is <see cref="ContentSource.GameContent"/>), or the local path to a content file relative to the mod folder.</param>
/// <param name="source">Where to search for a matching content asset.</param>
/// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception>
/// <returns>Returns whether the given asset key was cached.</returns>
public bool InvalidateCache(string key, ContentSource source = ContentSource.ModFolder)
{
this.Monitor.Log($"Requested cache invalidation for '{key}' in {source}.", LogLevel.Trace);
string actualKey = this.GetActualAssetKey(key, source);
return this.ContentManager.InvalidateCache((otherKey, type) => otherKey.Equals(actualKey, StringComparison.InvariantCultureIgnoreCase));
}
/// <summary>Remove all assets of the given type from the cache so they're reloaded on the next request. <b>This can be a very expensive operation and should only be used in very specific cases.</b> This will reload core game assets if needed, but references to the former assets will still show the previous content.</summary>
/// <typeparam name="T">The asset type to remove from the cache.</typeparam>
/// <returns>Returns whether any assets were invalidated.</returns>
public bool InvalidateCache<T>()
{
this.Monitor.Log($"Requested cache invalidation for all assets of type {typeof(T)}. This is an expensive operation and should be avoided if possible.", LogLevel.Trace);
return this.ContentManager.InvalidateCache((key, type) => typeof(T).IsAssignableFrom(type));
}
/********* /*********
** Private methods ** Private methods

View File

@ -220,8 +220,9 @@ namespace StardewModdingAPI.Framework
/// <summary>Reset the asset cache and reload the game's static assets.</summary> /// <summary>Reset the asset cache and reload the game's static assets.</summary>
/// <param name="predicate">Matches the asset keys to invalidate.</param> /// <param name="predicate">Matches the asset keys to invalidate.</param>
/// <returns>Returns whether any cache entries were invalidated.</returns>
/// <remarks>This implementation is derived from <see cref="Game1.LoadContent"/>.</remarks> /// <remarks>This implementation is derived from <see cref="Game1.LoadContent"/>.</remarks>
public void InvalidateCache(Func<string, bool> predicate) public bool InvalidateCache(Func<string, Type, bool> predicate)
{ {
// find matching asset keys // find matching asset keys
HashSet<string> purgeCacheKeys = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase); HashSet<string> purgeCacheKeys = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
@ -229,7 +230,8 @@ namespace StardewModdingAPI.Framework
foreach (string cacheKey in this.Cache.Keys) foreach (string cacheKey in this.Cache.Keys)
{ {
this.ParseCacheKey(cacheKey, out string assetKey, out string localeCode); this.ParseCacheKey(cacheKey, out string assetKey, out string localeCode);
if (predicate(assetKey)) Type type = this.Cache[cacheKey].GetType();
if (predicate(assetKey, type))
{ {
purgeAssetKeys.Add(assetKey); purgeAssetKeys.Add(assetKey);
purgeCacheKeys.Add(cacheKey); purgeCacheKeys.Add(cacheKey);
@ -251,7 +253,14 @@ namespace StardewModdingAPI.Framework
} }
} }
// report result
if (purgeCacheKeys.Any())
{
this.Monitor.Log($"Invalidated {purgeCacheKeys.Count} cache entries for {purgeAssetKeys.Count} asset keys: {string.Join(", ", purgeCacheKeys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace); this.Monitor.Log($"Invalidated {purgeCacheKeys.Count} cache entries for {purgeAssetKeys.Count} asset keys: {string.Join(", ", purgeCacheKeys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace);
return true;
}
this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace);
return false;
} }
@ -438,7 +447,7 @@ namespace StardewModdingAPI.Framework
// the cache, but don't dispose them to avoid crashing any code that still references // the cache, but don't dispose them to avoid crashing any code that still references
// them. The garbage collector will eventually clean up any unused assets. // them. The garbage collector will eventually clean up any unused assets.
this.Monitor.Log("Content manager disposed, resetting cache.", LogLevel.Trace); this.Monitor.Log("Content manager disposed, resetting cache.", LogLevel.Trace);
this.InvalidateCache(p => true); this.InvalidateCache((key, type) => true);
} }
} }
} }

View File

@ -20,5 +20,19 @@ namespace StardewModdingAPI
/// <param name="source">Where to search for a matching content asset.</param> /// <param name="source">Where to search for a matching content asset.</param>
/// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception> /// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception>
string GetActualAssetKey(string key, ContentSource source = ContentSource.ModFolder); string GetActualAssetKey(string key, ContentSource source = ContentSource.ModFolder);
#if !SMAPI_1_x
/// <summary>Remove an asset from the content cache so it's reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content.</summary>
/// <param name="key">The asset key to fetch (if the <paramref name="source"/> is <see cref="ContentSource.GameContent"/>), or the local path to a content file relative to the mod folder.</param>
/// <param name="source">Where to search for a matching content asset.</param>
/// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception>
/// <returns>Returns whether the given asset key was cached.</returns>
bool InvalidateCache(string key, ContentSource source = ContentSource.ModFolder);
/// <summary>Remove all assets of the given type from the cache so they're reloaded on the next request. <b>This can be a very expensive operation and should only be used in very specific cases.</b> This will reload core game assets if needed, but references to the former assets will still show the previous content.</summary>
/// <typeparam name="T">The asset type to remove from the cache.</typeparam>
/// <returns>Returns whether any assets were invalidated.</returns>
bool InvalidateCache<T>();
#endif
} }
} }

View File

@ -707,15 +707,16 @@ namespace StardewModdingAPI
// inject data // inject data
{ {
IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName);
ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager);
IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName); IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor);
IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, this.Reflection); IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, this.Reflection);
IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry);
ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage());
mod.ModManifest = manifest; mod.ModManifest = manifest;
mod.Helper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper); mod.Helper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper);
mod.Monitor = this.GetSecondaryMonitor(metadata.DisplayName); mod.Monitor = monitor;
#if SMAPI_1_x #if SMAPI_1_x
mod.PathOnDisk = metadata.DirectoryPath; mod.PathOnDisk = metadata.DirectoryPath;
#endif #endif
@ -818,7 +819,7 @@ namespace StardewModdingAPI
if (e.NewItems.Count > 0) if (e.NewItems.Count > 0)
{ {
this.Monitor.Log("Detected new asset editor, resetting cache...", LogLevel.Trace); this.Monitor.Log("Detected new asset editor, resetting cache...", LogLevel.Trace);
this.ContentManager.InvalidateCache(p => true); this.ContentManager.InvalidateCache((key, type) => true);
} }
}; };
helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => helper.ObservableAssetLoaders.CollectionChanged += (sender, e) =>
@ -826,13 +827,13 @@ namespace StardewModdingAPI
if (e.NewItems.Count > 0) if (e.NewItems.Count > 0)
{ {
this.Monitor.Log("Detected new asset loader, resetting cache...", LogLevel.Trace); this.Monitor.Log("Detected new asset loader, resetting cache...", LogLevel.Trace);
this.ContentManager.InvalidateCache(p => true); this.ContentManager.InvalidateCache((key, type) => true);
} }
}; };
} }
} }
this.Monitor.Log("Resetting cache to enable interception...", LogLevel.Trace); this.Monitor.Log("Resetting cache to enable interception...", LogLevel.Trace);
this.ContentManager.InvalidateCache(p => true); this.ContentManager.InvalidateCache((key, type) => true);
} }
/// <summary>Reload translations for all mods.</summary> /// <summary>Reload translations for all mods.</summary>