summaryrefslogtreecommitdiff
path: root/src/SMAPI
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI')
-rw-r--r--src/SMAPI/Framework/Content/AssetOperationGroup.cs33
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs100
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManager.cs44
-rw-r--r--src/SMAPI/Framework/Utilities/TickCacheDictionary.cs51
4 files changed, 179 insertions, 49 deletions
diff --git a/src/SMAPI/Framework/Content/AssetOperationGroup.cs b/src/SMAPI/Framework/Content/AssetOperationGroup.cs
new file mode 100644
index 00000000..a2fcb722
--- /dev/null
+++ b/src/SMAPI/Framework/Content/AssetOperationGroup.cs
@@ -0,0 +1,33 @@
+namespace StardewModdingAPI.Framework.Content
+{
+ /// <summary>A set of operations to apply to an asset for a given <see cref="IAssetEditor"/> or <see cref="IAssetLoader"/> implementation.</summary>
+ internal class AssetOperationGroup
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The mod applying the changes.</summary>
+ public IModMetadata Mod { get; }
+
+ /// <summary>The load operations to apply.</summary>
+ public AssetLoadOperation[] LoadOperations { get; }
+
+ /// <summary>The edit operations to apply.</summary>
+ public AssetEditOperation[] EditOperations { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="mod">The mod applying the changes.</param>
+ /// <param name="loadOperations">The load operations to apply.</param>
+ /// <param name="editOperations">The edit operations to apply.</param>
+ public AssetOperationGroup(IModMetadata mod, AssetLoadOperation[] loadOperations, AssetEditOperation[] editOperations)
+ {
+ this.Mod = mod;
+ this.LoadOperations = loadOperations;
+ this.EditOperations = editOperations;
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs
index fbbbe2d2..bf944e23 100644
--- a/src/SMAPI/Framework/ContentCoordinator.cs
+++ b/src/SMAPI/Framework/ContentCoordinator.cs
@@ -10,6 +10,8 @@ using Microsoft.Xna.Framework.Content;
using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.ContentManagers;
using StardewModdingAPI.Framework.Reflection;
+using StardewModdingAPI.Framework.Utilities;
+using StardewModdingAPI.Internal;
using StardewModdingAPI.Metadata;
using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Toolkit.Utilities;
@@ -68,6 +70,9 @@ namespace StardewModdingAPI.Framework
/// <summary>The language enum values indexed by locale code.</summary>
private Lazy<Dictionary<string, LocalizedContentManager.LanguageCode>> LocaleCodes;
+ /// <summary>The cached asset load/edit operations to apply, indexed by asset name.</summary>
+ private readonly TickCacheDictionary<IAssetName, AssetOperationGroup[]> AssetOperationsByKey = new();
+
/*********
** Accessors
@@ -351,17 +356,17 @@ namespace StardewModdingAPI.Framework
public IEnumerable<IAssetName> InvalidateCache(Func<IContentManager, string, Type, bool> predicate, bool dispose = false)
{
// invalidate cache & track removed assets
- IDictionary<IAssetName, Type> removedAssets = new Dictionary<IAssetName, Type>();
+ IDictionary<IAssetName, Type> invalidatedAssets = new Dictionary<IAssetName, Type>();
this.ContentManagerLock.InReadLock(() =>
{
// cached assets
foreach (IContentManager contentManager in this.ContentManagers)
{
- foreach (var entry in contentManager.InvalidateCache((key, type) => predicate(contentManager, key, type), dispose))
+ foreach ((string key, object asset) in contentManager.InvalidateCache((key, type) => predicate(contentManager, key, type), dispose))
{
- AssetName assetName = this.ParseAssetName(entry.Key);
- if (!removedAssets.ContainsKey(assetName))
- removedAssets[assetName] = entry.Value.GetType();
+ AssetName assetName = this.ParseAssetName(key);
+ if (!invalidatedAssets.ContainsKey(assetName))
+ invalidatedAssets[assetName] = asset.GetType();
}
}
@@ -376,18 +381,22 @@ namespace StardewModdingAPI.Framework
// get map path
AssetName mapPath = this.ParseAssetName(this.MainContentManager.AssertAndNormalizeAssetName(location.mapPath.Value));
- if (!removedAssets.ContainsKey(mapPath) && predicate(this.MainContentManager, mapPath.Name, typeof(Map)))
- removedAssets[mapPath] = typeof(Map);
+ if (!invalidatedAssets.ContainsKey(mapPath) && predicate(this.MainContentManager, mapPath.Name, typeof(Map)))
+ invalidatedAssets[mapPath] = typeof(Map);
}
}
});
+ // clear cached editor checks
+ foreach (IAssetName name in invalidatedAssets.Keys)
+ this.AssetOperationsByKey.Remove(name);
+
// reload core game assets
- if (removedAssets.Any())
+ if (invalidatedAssets.Any())
{
// propagate changes to the game
this.CoreAssets.Propagate(
- assets: removedAssets.ToDictionary(p => p.Key, p => p.Value),
+ assets: invalidatedAssets.ToDictionary(p => p.Key, p => p.Value),
ignoreWorld: Context.IsWorldFullyUnloaded,
out IDictionary<IAssetName, bool> propagated,
out bool updatedNpcWarps
@@ -396,7 +405,7 @@ namespace StardewModdingAPI.Framework
// log summary
StringBuilder report = new();
{
- IAssetName[] invalidatedKeys = removedAssets.Keys.ToArray();
+ IAssetName[] invalidatedKeys = invalidatedAssets.Keys.ToArray();
IAssetName[] propagatedKeys = propagated.Where(p => p.Value).Select(p => p.Key).ToArray();
string FormatKeyList(IEnumerable<IAssetName> keys) => string.Join(", ", keys.Select(p => p.Name).OrderBy(p => p, StringComparer.OrdinalIgnoreCase));
@@ -414,7 +423,18 @@ namespace StardewModdingAPI.Framework
else
this.Monitor.Log("Invalidated 0 cache entries.");
- return removedAssets.Keys;
+ return invalidatedAssets.Keys;
+ }
+
+ /// <summary>Get the asset load and edit operations to apply to a given asset if it's (re)loaded now.</summary>
+ /// <typeparam name="T">The asset type.</typeparam>
+ /// <param name="info">The asset info to load or edit.</param>
+ public IEnumerable<AssetOperationGroup> GetAssetOperations<T>(IAssetInfo info)
+ {
+ return this.AssetOperationsByKey.GetOrSet(
+ info.Name,
+ () => this.GetAssetOperationsWithoutCache<T>(info).ToArray()
+ );
}
/// <summary>Get all loaded instances of an asset name.</summary>
@@ -534,5 +554,63 @@ namespace StardewModdingAPI.Framework
return map;
}
+
+ /// <summary>Get the asset load and edit operations to apply to a given asset if it's (re)loaded now, ignoring the <see cref="AssetOperationsByKey"/> cache.</summary>
+ /// <typeparam name="T">The asset type.</typeparam>
+ /// <param name="info">The asset info to load or edit.</param>
+ private IEnumerable<AssetOperationGroup> GetAssetOperationsWithoutCache<T>(IAssetInfo info)
+ {
+ // legacy load operations
+ foreach (ModLinked<IAssetLoader> loader in this.Loaders)
+ {
+ // check if loader applies
+ try
+ {
+ if (!loader.Data.CanLoad<T>(info))
+ continue;
+ }
+ catch (Exception ex)
+ {
+ loader.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{info.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
+ continue;
+ }
+
+ // add operation
+ yield return new AssetOperationGroup(
+ mod: loader.Mod,
+ loadOperations: new[]
+ {
+ new AssetLoadOperation(loader.Mod, assetInfo => loader.Data.Load<T>(assetInfo))
+ },
+ editOperations: Array.Empty<AssetEditOperation>()
+ );
+ }
+
+ // legacy edit operations
+ foreach (var editor in this.Editors)
+ {
+ // check if editor applies
+ try
+ {
+ if (!editor.Data.CanEdit<T>(info))
+ continue;
+ }
+ catch (Exception ex)
+ {
+ editor.Mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{info.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
+ continue;
+ }
+
+ // add operation
+ yield return new AssetOperationGroup(
+ mod: editor.Mod,
+ loadOperations: Array.Empty<AssetLoadOperation>(),
+ editOperations: new[]
+ {
+ new AssetEditOperation(editor.Mod, assetData => editor.Data.Edit<T>(assetData))
+ }
+ );
+ }
+ }
}
}
diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
index 7ed1fcda..642e526c 100644
--- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
@@ -26,12 +26,6 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>The assets currently being intercepted by <see cref="IAssetLoader"/> instances. This is used to prevent infinite loops when a loader loads a new asset.</summary>
private readonly ContextHash<string> AssetsBeingLoaded = new();
- /// <summary>Interceptors which provide the initial versions of matching assets.</summary>
- private IList<ModLinked<IAssetLoader>> Loaders => this.Coordinator.Loaders;
-
- /// <summary>Interceptors which edit matching assets after they're loaded.</summary>
- private IList<ModLinked<IAssetEditor>> Editors => this.Coordinator.Editors;
-
/// <summary>Maps asset names to their localized form, like <c>LooseSprites\Billboard => LooseSprites\Billboard.fr-FR</c> (localized) or <c>Maps\AnimalShop => Maps\AnimalShop</c> (not localized).</summary>
private IDictionary<string, string> LocalizedAssetNames => LocalizedContentManager.localizedAssetNames;
@@ -370,22 +364,9 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="info">The basic asset metadata.</param>
private IEnumerable<AssetLoadOperation> GetLoaders<T>(IAssetInfo info)
{
- return this.Loaders
- .Where(loader =>
- {
- try
- {
- return loader.Data.CanLoad<T>(info);
- }
- catch (Exception ex)
- {
- loader.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{info.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
- return false;
- }
- })
- .Select(
- loader => new AssetLoadOperation(loader.Mod, assetInfo => loader.Data.Load<T>(assetInfo))
- );
+ return this.Coordinator
+ .GetAssetOperations<T>(info)
+ .SelectMany(p => p.LoadOperations);
}
/// <summary>Get the asset editors to apply to an asset.</summary>
@@ -393,22 +374,9 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="info">The basic asset metadata.</param>
private IEnumerable<AssetEditOperation> GetEditors<T>(IAssetInfo info)
{
- return this.Editors
- .Where(editor =>
- {
- try
- {
- return editor.Data.CanEdit<T>(info);
- }
- catch (Exception ex)
- {
- editor.Mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{info.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
- return false;
- }
- })
- .Select(
- editor => new AssetEditOperation(editor.Mod, assetData => editor.Data.Edit<T>(assetData))
- );
+ return this.Coordinator
+ .GetAssetOperations<T>(info)
+ .SelectMany(p => p.EditOperations);
}
/// <summary>Assert that at most one loader will be applied to an asset.</summary>
diff --git a/src/SMAPI/Framework/Utilities/TickCacheDictionary.cs b/src/SMAPI/Framework/Utilities/TickCacheDictionary.cs
new file mode 100644
index 00000000..1613a480
--- /dev/null
+++ b/src/SMAPI/Framework/Utilities/TickCacheDictionary.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Collections.Generic;
+using StardewValley;
+
+namespace StardewModdingAPI.Framework.Utilities
+{
+ /// <summary>An in-memory dictionary cache that stores data for the duration of a game update tick.</summary>
+ /// <typeparam name="TKey">The dictionary key type.</typeparam>
+ /// <typeparam name="TValue">The dictionary value type.</typeparam>
+ internal class TickCacheDictionary<TKey, TValue>
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>The last game tick for which data was cached.</summary>
+ private int LastGameTick = -1;
+
+ /// <summary>The underlying cached data.</summary>
+ private readonly Dictionary<TKey, TValue> Cache = new();
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Get a value from the cache, fetching it first if it's not cached yet.</summary>
+ /// <param name="cacheKey">The unique key for the cached value.</param>
+ /// <param name="get">Get the latest data if it's not in the cache yet.</param>
+ public TValue GetOrSet(TKey cacheKey, Func<TValue> get)
+ {
+ // clear cache on new tick
+ if (Game1.ticks != this.LastGameTick)
+ {
+ this.Cache.Clear();
+ this.LastGameTick = Game1.ticks;
+ }
+
+ // fetch value
+ if (!this.Cache.TryGetValue(cacheKey, out TValue cached))
+ this.Cache[cacheKey] = cached = get();
+ return cached;
+ }
+
+ /// <summary>Remove an entry from the cache.</summary>
+ /// <param name="cacheKey">The unique key for the cached value.</param>
+ /// <returns>Returns whether the key was present in the dictionary.</returns>
+ public bool Remove(TKey cacheKey)
+ {
+ return this.Cache.Remove(cacheKey);
+ }
+ }
+}