diff options
Diffstat (limited to 'src/SMAPI')
43 files changed, 1078 insertions, 367 deletions
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 7fdfb8d0..97204d86 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -20,7 +20,7 @@ namespace StardewModdingAPI ** Public ****/ /// <summary>SMAPI's current semantic version.</summary> - public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.0.1"); + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.1.0"); /// <summary>The minimum supported version of Stardew Valley.</summary> public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.4.0"); diff --git a/src/SMAPI/Events/ChestInventoryChangedEventArgs.cs b/src/SMAPI/Events/ChestInventoryChangedEventArgs.cs new file mode 100644 index 00000000..4b4c4210 --- /dev/null +++ b/src/SMAPI/Events/ChestInventoryChangedEventArgs.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using StardewValley; +using StardewValley.Objects; + +namespace StardewModdingAPI.Events +{ + /// <summary>Event arguments for a <see cref="IWorldEvents.ChestInventoryChanged"/> event.</summary> + public class ChestInventoryChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// <summary>The chest whose inventory changed.</summary> + public Chest Chest { get; } + + /// <summary>The location containing the chest.</summary> + public GameLocation Location { get; } + + /// <summary>The added item stacks.</summary> + public IEnumerable<Item> Added { get; } + + /// <summary>The removed item stacks.</summary> + public IEnumerable<Item> Removed { get; } + + /// <summary>The item stacks whose size changed.</summary> + public IEnumerable<ItemStackSizeChange> QuantityChanged { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="chest">The chest whose inventory changed.</param> + /// <param name="location">The location containing the chest.</param> + /// <param name="added">The added item stacks.</param> + /// <param name="removed">The removed item stacks.</param> + /// <param name="quantityChanged">The item stacks whose size changed.</param> + internal ChestInventoryChangedEventArgs(Chest chest, GameLocation location, Item[] added, Item[] removed, ItemStackSizeChange[] quantityChanged) + { + this.Location = location; + this.Chest = chest; + this.Added = added; + this.Removed = removed; + this.QuantityChanged = quantityChanged; + } + } +} diff --git a/src/SMAPI/Events/IWorldEvents.cs b/src/SMAPI/Events/IWorldEvents.cs index 0ceffcc1..9569a57b 100644 --- a/src/SMAPI/Events/IWorldEvents.cs +++ b/src/SMAPI/Events/IWorldEvents.cs @@ -23,6 +23,9 @@ namespace StardewModdingAPI.Events /// <summary>Raised after objects are added or removed in a location.</summary> event EventHandler<ObjectListChangedEventArgs> ObjectListChanged; + /// <summary>Raised after items are added or removed from a chest.</summary> + event EventHandler<ChestInventoryChangedEventArgs> ChestInventoryChanged; + /// <summary>Raised after terrain features (like floors and trees) are added or removed in a location.</summary> event EventHandler<TerrainFeatureListChangedEventArgs> TerrainFeatureListChanged; } diff --git a/src/SMAPI/Events/InventoryChangedEventArgs.cs b/src/SMAPI/Events/InventoryChangedEventArgs.cs index 874c2e48..40cd4128 100644 --- a/src/SMAPI/Events/InventoryChangedEventArgs.cs +++ b/src/SMAPI/Events/InventoryChangedEventArgs.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using StardewValley; namespace StardewModdingAPI.Events @@ -14,13 +13,13 @@ namespace StardewModdingAPI.Events /// <summary>The player whose inventory changed.</summary> public Farmer Player { get; } - /// <summary>The added items.</summary> + /// <summary>The added item stacks.</summary> public IEnumerable<Item> Added { get; } - /// <summary>The removed items.</summary> + /// <summary>The removed item stacks.</summary> public IEnumerable<Item> Removed { get; } - /// <summary>The items whose stack sizes changed, with the relative change.</summary> + /// <summary>The item stacks whose size changed.</summary> public IEnumerable<ItemStackSizeChange> QuantityChanged { get; } /// <summary>Whether the affected player is the local one.</summary> @@ -32,28 +31,15 @@ namespace StardewModdingAPI.Events *********/ /// <summary>Construct an instance.</summary> /// <param name="player">The player whose inventory changed.</param> - /// <param name="changedItems">The inventory changes.</param> - internal InventoryChangedEventArgs(Farmer player, ItemStackChange[] changedItems) + /// <param name="added">The added item stacks.</param> + /// <param name="removed">The removed item stacks.</param> + /// <param name="quantityChanged">The item stacks whose size changed.</param> + internal InventoryChangedEventArgs(Farmer player, Item[] added, Item[] removed, ItemStackSizeChange[] quantityChanged) { this.Player = player; - this.Added = changedItems - .Where(n => n.ChangeType == ChangeType.Added) - .Select(p => p.Item) - .ToArray(); - - this.Removed = changedItems - .Where(n => n.ChangeType == ChangeType.Removed) - .Select(p => p.Item) - .ToArray(); - - this.QuantityChanged = changedItems - .Where(n => n.ChangeType == ChangeType.StackChange) - .Select(change => new ItemStackSizeChange( - item: change.Item, - oldSize: change.Item.Stack - change.StackChange, - newSize: change.Item.Stack - )) - .ToArray(); + this.Added = added; + this.Removed = removed; + this.QuantityChanged = quantityChanged; } } } diff --git a/src/SMAPI/Events/ItemStackChange.cs b/src/SMAPI/Events/ItemStackChange.cs deleted file mode 100644 index f9ae6df6..00000000 --- a/src/SMAPI/Events/ItemStackChange.cs +++ /dev/null @@ -1,20 +0,0 @@ -using StardewValley; - -namespace StardewModdingAPI.Events -{ - /// <summary>Represents an inventory slot that changed.</summary> - public class ItemStackChange - { - /********* - ** Accessors - *********/ - /// <summary>The item in the slot.</summary> - public Item Item { get; set; } - - /// <summary>The amount by which the item's stack size changed.</summary> - public int StackChange { get; set; } - - /// <summary>How the inventory slot changed.</summary> - public ChangeType ChangeType { get; set; } - } -}
\ No newline at end of file diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index 4ae2ad68..aa615a0b 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -42,8 +42,8 @@ namespace StardewModdingAPI.Framework.Content Texture2D target = this.Data; // get areas - sourceArea = sourceArea ?? new Rectangle(0, 0, source.Width, source.Height); - targetArea = targetArea ?? new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, target.Width), Math.Min(sourceArea.Value.Height, target.Height)); + sourceArea ??= new Rectangle(0, 0, source.Width, source.Height); + targetArea ??= new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, target.Width), Math.Min(sourceArea.Value.Height, target.Height)); // validate if (sourceArea.Value.X < 0 || sourceArea.Value.Y < 0 || sourceArea.Value.Right > source.Width || sourceArea.Value.Bottom > source.Height) diff --git a/src/SMAPI/Framework/Content/AssetInterceptorChange.cs b/src/SMAPI/Framework/Content/AssetInterceptorChange.cs new file mode 100644 index 00000000..037d9f89 --- /dev/null +++ b/src/SMAPI/Framework/Content/AssetInterceptorChange.cs @@ -0,0 +1,93 @@ +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 + { + if (editor.CanEdit<TAsset>(asset)) + return true; + } + 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 + { + if (loader.CanLoad<TAsset>(asset)) + return true; + } + 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..36f2f650 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -41,6 +41,10 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <summary>A list of disposable assets.</summary> private readonly List<WeakReference<IDisposable>> Disposables = new List<WeakReference<IDisposable>>(); + /// <summary>The disposable assets tracked by the base content manager.</summary> + /// <remarks>This should be kept empty to avoid keeping disposable assets referenced forever, which prevents garbage collection when they're unused. Disposable assets are tracked by <see cref="Disposables"/> instead, which avoids a hard reference.</remarks> + private readonly List<IDisposable> BaseDisposableReferences; + /********* ** Accessors @@ -84,6 +88,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // get asset data this.LanguageCodes = this.GetKeyLocales().ToDictionary(p => p.Value, p => p.Key, StringComparer.InvariantCultureIgnoreCase); + this.BaseDisposableReferences = reflection.GetField<List<IDisposable>>(this, "disposableAssets").GetValue(); } /// <summary>Load an asset that has been processed by the content pipeline.</summary> @@ -184,25 +189,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 +263,27 @@ 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); |
