diff options
Diffstat (limited to 'src/SMAPI/Framework')
-rw-r--r-- | src/SMAPI/Framework/Content/AssetInterceptorChange.cs | 91 | ||||
-rw-r--r-- | src/SMAPI/Framework/ContentCoordinator.cs | 82 | ||||
-rw-r--r-- | src/SMAPI/Framework/SCore.cs | 27 | ||||
-rw-r--r-- | src/SMAPI/Framework/SGame.cs | 50 |
4 files changed, 142 insertions, 108 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/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 08ebe6a5..97b54c5b 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -3,7 +3,6 @@ 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; @@ -188,59 +187,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> @@ -308,33 +254,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/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 *********/ |