summaryrefslogtreecommitdiff
path: root/src/SMAPI
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2022-03-22 23:00:18 -0400
committerJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2022-03-22 23:00:18 -0400
commitb07d2340a9a6da22ee0fd95f2c6ccca3939cb7ab (patch)
treec61a7108ff66071b9b6feb5446c6bd3e14ba8182 /src/SMAPI
parentd3fbdf484a4d90365a55fb5058d75a8623f17d0f (diff)
downloadSMAPI-b07d2340a9a6da22ee0fd95f2c6ccca3939cb7ab.tar.gz
SMAPI-b07d2340a9a6da22ee0fd95f2c6ccca3939cb7ab.tar.bz2
SMAPI-b07d2340a9a6da22ee0fd95f2c6ccca3939cb7ab.zip
encapsulate & cache asset operation groups (#766)
This is needed for the upcoming Stardew Valley 1.6 to avoid duplicate checks between DoesAssetExist and Load calls, and to make sure the answer doesn't change between them.
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);
+ }
+ }
+}