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.
This commit is contained in:
parent
d3fbdf484a
commit
b07d2340a9
|
@ -16,6 +16,7 @@
|
||||||
* Added `Constants.ContentPath`.
|
* Added `Constants.ContentPath`.
|
||||||
* Added `IAssetName Name` field to the info received by `IAssetEditor` and `IAssetLoader` methods.
|
* Added `IAssetName Name` field to the info received by `IAssetEditor` and `IAssetLoader` methods.
|
||||||
_This adds methods for working with asset names, parsed locales, etc._
|
_This adds methods for working with asset names, parsed locales, etc._
|
||||||
|
* If an asset is loaded multiple times in the same tick, `IAssetLoader.CanLoad` and `IAssetEditor.CanEdit` are now cached unless invalidated via `helper.Content.InvalidateCache`.
|
||||||
* Fixed the `SDate` constructor being case-sensitive.
|
* Fixed the `SDate` constructor being case-sensitive.
|
||||||
* Fixed support for using locale codes from custom languages in asset names (e.g. `Data/Achievements.eo-EU`).
|
* Fixed support for using locale codes from custom languages in asset names (e.g. `Data/Achievements.eo-EU`).
|
||||||
* Fixed issue where suppressing `[Left|Right]Thumbstick[Down|Left]` keys would suppress the opposite direction instead.
|
* Fixed issue where suppressing `[Left|Right]Thumbstick[Down|Left]` keys would suppress the opposite direction instead.
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,8 @@ using Microsoft.Xna.Framework.Content;
|
||||||
using StardewModdingAPI.Framework.Content;
|
using StardewModdingAPI.Framework.Content;
|
||||||
using StardewModdingAPI.Framework.ContentManagers;
|
using StardewModdingAPI.Framework.ContentManagers;
|
||||||
using StardewModdingAPI.Framework.Reflection;
|
using StardewModdingAPI.Framework.Reflection;
|
||||||
|
using StardewModdingAPI.Framework.Utilities;
|
||||||
|
using StardewModdingAPI.Internal;
|
||||||
using StardewModdingAPI.Metadata;
|
using StardewModdingAPI.Metadata;
|
||||||
using StardewModdingAPI.Toolkit.Serialization;
|
using StardewModdingAPI.Toolkit.Serialization;
|
||||||
using StardewModdingAPI.Toolkit.Utilities;
|
using StardewModdingAPI.Toolkit.Utilities;
|
||||||
|
@ -68,6 +70,9 @@ namespace StardewModdingAPI.Framework
|
||||||
/// <summary>The language enum values indexed by locale code.</summary>
|
/// <summary>The language enum values indexed by locale code.</summary>
|
||||||
private Lazy<Dictionary<string, LocalizedContentManager.LanguageCode>> LocaleCodes;
|
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
|
** Accessors
|
||||||
|
@ -351,17 +356,17 @@ namespace StardewModdingAPI.Framework
|
||||||
public IEnumerable<IAssetName> InvalidateCache(Func<IContentManager, string, Type, bool> predicate, bool dispose = false)
|
public IEnumerable<IAssetName> InvalidateCache(Func<IContentManager, string, Type, bool> predicate, bool dispose = false)
|
||||||
{
|
{
|
||||||
// invalidate cache & track removed assets
|
// invalidate cache & track removed assets
|
||||||
IDictionary<IAssetName, Type> removedAssets = new Dictionary<IAssetName, Type>();
|
IDictionary<IAssetName, Type> invalidatedAssets = new Dictionary<IAssetName, Type>();
|
||||||
this.ContentManagerLock.InReadLock(() =>
|
this.ContentManagerLock.InReadLock(() =>
|
||||||
{
|
{
|
||||||
// cached assets
|
// cached assets
|
||||||
foreach (IContentManager contentManager in this.ContentManagers)
|
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);
|
AssetName assetName = this.ParseAssetName(key);
|
||||||
if (!removedAssets.ContainsKey(assetName))
|
if (!invalidatedAssets.ContainsKey(assetName))
|
||||||
removedAssets[assetName] = entry.Value.GetType();
|
invalidatedAssets[assetName] = asset.GetType();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -376,18 +381,22 @@ namespace StardewModdingAPI.Framework
|
||||||
|
|
||||||
// get map path
|
// get map path
|
||||||
AssetName mapPath = this.ParseAssetName(this.MainContentManager.AssertAndNormalizeAssetName(location.mapPath.Value));
|
AssetName mapPath = this.ParseAssetName(this.MainContentManager.AssertAndNormalizeAssetName(location.mapPath.Value));
|
||||||
if (!removedAssets.ContainsKey(mapPath) && predicate(this.MainContentManager, mapPath.Name, typeof(Map)))
|
if (!invalidatedAssets.ContainsKey(mapPath) && predicate(this.MainContentManager, mapPath.Name, typeof(Map)))
|
||||||
removedAssets[mapPath] = typeof(Map);
|
invalidatedAssets[mapPath] = typeof(Map);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// clear cached editor checks
|
||||||
|
foreach (IAssetName name in invalidatedAssets.Keys)
|
||||||
|
this.AssetOperationsByKey.Remove(name);
|
||||||
|
|
||||||
// reload core game assets
|
// reload core game assets
|
||||||
if (removedAssets.Any())
|
if (invalidatedAssets.Any())
|
||||||
{
|
{
|
||||||
// propagate changes to the game
|
// propagate changes to the game
|
||||||
this.CoreAssets.Propagate(
|
this.CoreAssets.Propagate(
|
||||||
assets: removedAssets.ToDictionary(p => p.Key, p => p.Value),
|
assets: invalidatedAssets.ToDictionary(p => p.Key, p => p.Value),
|
||||||
ignoreWorld: Context.IsWorldFullyUnloaded,
|
ignoreWorld: Context.IsWorldFullyUnloaded,
|
||||||
out IDictionary<IAssetName, bool> propagated,
|
out IDictionary<IAssetName, bool> propagated,
|
||||||
out bool updatedNpcWarps
|
out bool updatedNpcWarps
|
||||||
|
@ -396,7 +405,7 @@ namespace StardewModdingAPI.Framework
|
||||||
// log summary
|
// log summary
|
||||||
StringBuilder report = new();
|
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();
|
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));
|
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
|
else
|
||||||
this.Monitor.Log("Invalidated 0 cache entries.");
|
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>
|
/// <summary>Get all loaded instances of an asset name.</summary>
|
||||||
|
@ -534,5 +554,63 @@ namespace StardewModdingAPI.Framework
|
||||||
|
|
||||||
return map;
|
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))
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
/// <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();
|
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>
|
/// <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;
|
private IDictionary<string, string> LocalizedAssetNames => LocalizedContentManager.localizedAssetNames;
|
||||||
|
|
||||||
|
@ -370,22 +364,9 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
/// <param name="info">The basic asset metadata.</param>
|
/// <param name="info">The basic asset metadata.</param>
|
||||||
private IEnumerable<AssetLoadOperation> GetLoaders<T>(IAssetInfo info)
|
private IEnumerable<AssetLoadOperation> GetLoaders<T>(IAssetInfo info)
|
||||||
{
|
{
|
||||||
return this.Loaders
|
return this.Coordinator
|
||||||
.Where(loader =>
|
.GetAssetOperations<T>(info)
|
||||||
{
|
.SelectMany(p => p.LoadOperations);
|
||||||
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))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Get the asset editors to apply to an asset.</summary>
|
/// <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>
|
/// <param name="info">The basic asset metadata.</param>
|
||||||
private IEnumerable<AssetEditOperation> GetEditors<T>(IAssetInfo info)
|
private IEnumerable<AssetEditOperation> GetEditors<T>(IAssetInfo info)
|
||||||
{
|
{
|
||||||
return this.Editors
|
return this.Coordinator
|
||||||
.Where(editor =>
|
.GetAssetOperations<T>(info)
|
||||||
{
|
.SelectMany(p => p.EditOperations);
|
||||||
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))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Assert that at most one loader will be applied to an asset.</summary>
|
/// <summary>Assert that at most one loader will be applied to an asset.</summary>
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue