summaryrefslogtreecommitdiff
path: root/src/SMAPI
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI')
-rw-r--r--src/SMAPI/Constants.cs2
-rw-r--r--src/SMAPI/Events/ChestInventoryChangedEventArgs.cs48
-rw-r--r--src/SMAPI/Events/IWorldEvents.cs3
-rw-r--r--src/SMAPI/Events/InventoryChangedEventArgs.cs34
-rw-r--r--src/SMAPI/Events/ItemStackChange.cs20
-rw-r--r--src/SMAPI/Framework/Content/AssetDataForImage.cs4
-rw-r--r--src/SMAPI/Framework/Content/AssetInterceptorChange.cs93
-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.cs38
-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/Events/EventManager.cs4
-rw-r--r--src/SMAPI/Framework/Events/ModWorldEvents.cs7
-rw-r--r--src/SMAPI/Framework/IModMetadata.cs4
-rw-r--r--src/SMAPI/Framework/Input/SInputState.cs3
-rw-r--r--src/SMAPI/Framework/ModHelpers/DataHelper.cs48
-rw-r--r--src/SMAPI/Framework/ModLoading/AssemblyLoader.cs5
-rw-r--r--src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs3
-rw-r--r--src/SMAPI/Framework/ModLoading/ModMetadata.cs21
-rw-r--r--src/SMAPI/Framework/Models/ModFolderExport.cs21
-rw-r--r--src/SMAPI/Framework/Models/SConfig.cs6
-rw-r--r--src/SMAPI/Framework/Reflection/ReflectedMethod.cs4
-rw-r--r--src/SMAPI/Framework/SCore.cs154
-rw-r--r--src/SMAPI/Framework/SGame.cs75
-rw-r--r--src/SMAPI/Framework/SnapshotItemListDiff.cs66
-rw-r--r--src/SMAPI/Framework/StateTracking/ChestTracker.cs101
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/NetListWatcher.cs143
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs28
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs8
-rw-r--r--src/SMAPI/Framework/StateTracking/LocationTracker.cs60
-rw-r--r--src/SMAPI/Framework/StateTracking/PlayerTracker.cs32
-rw-r--r--src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs13
-rw-r--r--src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs15
-rw-r--r--src/SMAPI/Metadata/CoreAssetPropagator.cs144
-rw-r--r--src/SMAPI/Metadata/InstructionMetadata.cs1
-rw-r--r--src/SMAPI/SMAPI.config.json6
-rw-r--r--src/SMAPI/SMAPI.csproj21
-rw-r--r--src/SMAPI/i18n/es.json3
-rw-r--r--src/SMAPI/i18n/ja.json3
-rw-r--r--src/SMAPI/i18n/pt.json3
-rw-r--r--src/SMAPI/i18n/zh.json6
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);