using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.Exceptions;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Framework.Utilities;
using StardewModdingAPI.Metadata;
using StardewValley;
namespace StardewModdingAPI.Framework
{
/// A thread-safe content handler which loads assets with support for mod injection and editing.
///
/// This is the centralised content logic which manages all game assets. The game and mods don't use this class
/// directly; instead they use one of several instances, which proxy requests to
/// this class. That ensures that when the game disposes one content manager, the others can continue unaffected.
/// That notably requires this class to be thread-safe, since the content managers can be disposed asynchronously.
///
/// Note that assets in the cache have two identifiers: the asset name (like "bundles") and key (like "bundles.pt-BR").
/// For English and non-translatable assets, these have the same value. The underlying cache only knows about asset
/// keys, and the game and mods only know about asset names. The content manager handles resolving them.
///
internal class ContentCore : IDisposable
{
/*********
** Properties
*********/
/// The underlying content manager.
private readonly LocalizedContentManager Content;
/// Encapsulates monitoring and logging.
private readonly IMonitor Monitor;
/// The underlying asset cache.
private readonly ContentCache Cache;
#if STARDEW_VALLEY_1_3
/// A lookup which indicates whether the asset is localisable (i.e. the filename contains the locale), if previously loaded.
private readonly IDictionary IsLocalisableLookup;
#endif
/// The locale codes used in asset keys indexed by enum value.
private readonly IDictionary Locales;
/// The language enum values indexed by locale code.
private readonly IDictionary LanguageCodes;
/// Provides metadata for core game assets.
private readonly CoreAssetPropagator CoreAssets;
/// The assets currently being intercepted by instances. This is used to prevent infinite loops when a loader loads a new asset.
private readonly ContextHash AssetsBeingLoaded = new ContextHash();
/// A lookup of the content managers which loaded each asset.
private readonly IDictionary> ContentManagersByAssetKey = new Dictionary>();
/// The path prefix for assets in mod folders.
private readonly string ModContentPrefix;
/// A lock used to prevents concurrent changes to the cache while data is being read.
private readonly ReaderWriterLockSlim Lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
/*********
** Accessors
*********/
/// The current language as a constant.
public LocalizedContentManager.LanguageCode Language => this.Content.GetCurrentLanguage();
/// Interceptors which provide the initial versions of matching assets.
public IDictionary> Loaders { get; } = new Dictionary>();
/// Interceptors which edit matching assets after they're loaded.
public IDictionary> Editors { get; } = new Dictionary>();
/// The absolute path to the .
public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.Content.RootDirectory);
/*********
** Public methods
*********/
/****
** Constructor
****/
/// Construct an instance.
/// The service provider to use to locate services.
/// The root directory to search for content.
/// The current culture for which to localise content.
/// The current language code for which to localise content.
/// Encapsulates monitoring and logging.
/// Simplifies access to private code.
public ContentCore(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, string languageCodeOverride, IMonitor monitor, Reflector reflection)
{
// init
this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor));
this.Content = new LocalizedContentManager(serviceProvider, rootDirectory, currentCulture, languageCodeOverride);
this.Cache = new ContentCache(this.Content, reflection);
this.ModContentPrefix = this.GetAssetNameFromFilePath(Constants.ModPath);
// get asset data
this.CoreAssets = new CoreAssetPropagator(this.NormaliseAssetName, reflection);
this.Locales = this.GetKeyLocales(reflection);
this.LanguageCodes = this.Locales.ToDictionary(p => p.Value, p => p.Key, StringComparer.InvariantCultureIgnoreCase);
#if STARDEW_VALLEY_1_3
this.IsLocalisableLookup = reflection.GetField>(this.Content, "_localizedAsset").GetValue();
#endif
}
/// Get a new content manager which defers loading to the content core.
/// The content manager's name for logs (if any).
/// The root directory to search for content (or null. for the default)
public ContentManagerShim CreateContentManager(string name, string rootDirectory = null)
{
return new ContentManagerShim(this, name, this.Content.ServiceProvider, rootDirectory ?? this.Content.RootDirectory, this.Content.CurrentCulture, this.Content.LanguageCodeOverride);
}
/****
** Asset key/name handling
****/
/// Normalise path separators in a file path. For asset keys, see instead.
/// The file path to normalise.
[Pure]
public string NormalisePathSeparators(string path)
{
return this.Cache.NormalisePathSeparators(path);
}
/// Normalise an asset name so it's consistent with the underlying cache.
/// The asset key.
[Pure]
public string NormaliseAssetName(string assetName)
{
return this.Cache.NormaliseKey(assetName);
}
/// Assert that the given key has a valid format.
/// The asset key to check.
/// The asset key is empty or contains invalid characters.
[SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")]
public void AssertValidAssetKeyFormat(string key)
{
if (string.IsNullOrWhiteSpace(key))
throw new ArgumentException("The asset key or local path is empty.");
if (key.Intersect(Path.GetInvalidPathChars()).Any())
throw new ArgumentException("The asset key or local path contains invalid characters.");
}
/// Convert an absolute file path into a appropriate asset name.
/// The absolute path to the file.
public string GetAssetNameFromFilePath(string absolutePath)
{
#if SMAPI_FOR_WINDOWS
// XNA doesn't allow absolute asset paths, so get a path relative to the content folder
return this.GetRelativePath(absolutePath);
#else
// MonoGame is weird about relative paths on Mac, but allows absolute paths
return absolutePath;
#endif
}
/****
** Content loading
****/
/// Get the current content locale.
public string GetLocale()
{
return this.GetLocale(this.Content.GetCurrentLanguage());
}
/// The locale for a language.
/// The language.
public string GetLocale(LocalizedContentManager.LanguageCode language)
{
return this.Locales[language];
}
/// Get whether the content manager has already loaded and cached the given asset.
/// The asset path relative to the loader root directory, not including the .xnb extension.
public bool IsLoaded(string assetName)
{
assetName = this.Cache.NormaliseKey(assetName);
return this.WithReadLock(() => this.IsNormalisedKeyLoaded(assetName));
}
/// Get the cached asset keys.
public IEnumerable GetAssetKeys()
{
return this.WithReadLock(() =>
this.Cache.Keys
.Select(this.GetAssetName)
.Distinct()
);
}
/// Load an asset through the content pipeline. When loading a .png file, this must be called outside the game's draw loop.
/// The expected asset type.
/// The asset path relative to the content directory.
/// The content manager instance for which to load the asset.
/// The language code for which to load content.
/// The is empty or contains invalid characters.
/// The content asset couldn't be loaded (e.g. because it doesn't exist).
public T Load(string assetName, ContentManager instance
#if STARDEW_VALLEY_1_3
, LocalizedContentManager.LanguageCode language
#endif
)
{
// normalise asset key
this.AssertValidAssetKeyFormat(assetName);
assetName = this.NormaliseAssetName(assetName);
// load game content
if (!assetName.StartsWith(this.ModContentPrefix))
#if STARDEW_VALLEY_1_3
return this.LoadImpl(assetName, instance, language);
#else
return this.LoadImpl(assetName, instance);
#endif
// load mod content
SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading content asset '{assetName}': {reasonPhrase}");
try
{
return this.WithWriteLock(() =>
{
// try cache
if (this.IsLoaded(assetName))
#if STARDEW_VALLEY_1_3
return this.LoadImpl(assetName, instance, language);
#else
return this.LoadImpl(assetName, instance);
#endif
// get file
FileInfo file = this.GetModFile(assetName);
if (!file.Exists)
throw GetContentError("the specified path doesn't exist.");
// load content
switch (file.Extension.ToLower())
{
// XNB file
case ".xnb":
#if STARDEW_VALLEY_1_3
return this.LoadImpl(assetName, instance, language);
#else
return this.LoadImpl(assetName, instance);
#endif
// unpacked map
case ".tbin":
throw GetContentError($"can't read unpacked map file '{assetName}' directly from the underlying content manager. It must be loaded through the mod's {typeof(IModHelper)}.{nameof(IModHelper.Content)} helper.");
// unpacked image
case ".png":
// validate
if (typeof(T) != typeof(Texture2D))
throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'.");
// fetch & cache
using (FileStream stream = File.OpenRead(file.FullName))
{
Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream);
texture = this.PremultiplyTransparency(texture);
this.InjectWithoutLock(assetName, texture, instance);
return (T)(object)texture;
}
default:
throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.png', '.tbin', or '.xnb'.");
}
});
}
catch (Exception ex) when (!(ex is SContentLoadException))
{
if (ex.GetInnermostException() is DllNotFoundException dllEx && dllEx.Message == "libgdiplus.dylib")
throw GetContentError("couldn't find libgdiplus, which is needed to load mod images. Make sure Mono is installed and you're running the game through the normal launcher.");
throw new SContentLoadException($"The content manager failed loading content asset '{assetName}'.", ex);
}
}
/// Inject an asset into the cache.
/// The type of asset to inject.
/// The asset path relative to the loader root directory, not including the .xnb extension.
/// The asset value.
/// The content manager instance for which to load the asset.
public void Inject(string assetName, T value, ContentManager instance)
{
this.WithWriteLock(() => this.InjectWithoutLock(assetName, value, instance));
}
/****
** Cache invalidation
****/
/// Purge assets from the cache that match one of the interceptors.
/// The asset editors for which to purge matching assets.
/// The asset loaders for which to purge matching assets.
/// Returns whether any cache entries were invalidated.
public bool InvalidateCacheFor(IAssetEditor[] editors, IAssetLoader[] loaders)
{
if (!editors.Any() && !loaders.Any())
return false;
// 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);
if (loaders.Any(loader => (bool)canLoadGeneric.Invoke(loader, new object[] { asset })))
return true;
// check editors
MethodInfo canEditGeneric = canEdit.MakeGenericMethod(asset.DataType);
return editors.Any(editor => (bool)canEditGeneric.Invoke(editor, new object[] { asset }));
});
}
/// Purge matched assets from the cache.
/// Matches the asset keys to invalidate.
/// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game.
/// Returns whether any cache entries were invalidated.
public bool InvalidateCache(Func predicate, bool dispose = false)
{
string locale = this.GetLocale();
return this.InvalidateCache((assetName, type) =>
{
IAssetInfo info = new AssetInfo(locale, assetName, type, this.NormaliseAssetName);
return predicate(info);
});
}
/// Purge matched assets from the cache.
/// Matches the asset keys to invalidate.
/// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game.
/// Returns whether any cache entries were invalidated.
public bool InvalidateCache(Func predicate, bool dispose = false)
{
return this.WithWriteLock(() =>
{
// invalidate matching keys
HashSet removeKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase);
HashSet removeAssetNames = new HashSet(StringComparer.InvariantCultureIgnoreCase);
this.Cache.Remove((key, type) =>
{
this.ParseCacheKey(key, out string assetName, out _);
if (removeAssetNames.Contains(assetName) || predicate(assetName, type))
{
removeAssetNames.Add(assetName);
removeKeys.Add(key);
return true;
}
return false;
});
// update reference tracking
foreach (string key in removeKeys)
this.ContentManagersByAssetKey.Remove(key);
// reload core game assets
int reloaded = 0;
foreach (string key in removeAssetNames)
{
if (this.CoreAssets.Propagate(Game1.content, key)) // use an intercepted content manager
reloaded++;
}
// report result
if (removeKeys.Any())
{
this.Monitor.Log($"Invalidated {removeAssetNames.Count} asset names: {string.Join(", ", removeKeys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace);
return true;
}
this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace);
return false;
});
}
/****
** Disposal
****/
/// Dispose assets for the given content manager shim.
/// The content manager whose assets to dispose.
internal void DisposeFor(ContentManagerShim shim)
{
this.Monitor.Log($"Content manager '{shim.Name}' disposed, disposing assets that aren't needed by any other asset loader.", LogLevel.Trace);
this.WithWriteLock(() =>
{
foreach (var entry in this.ContentManagersByAssetKey)
entry.Value.Remove(shim);
this.InvalidateCache((key, type) => !this.ContentManagersByAssetKey[key].Any(), dispose: true);
});
}
/*********
** Private methods
*********/
/****
** Disposal
****/
/// Dispose held resources.
public void Dispose()
{
this.Monitor.Log("Disposing SMAPI's main content manager. It will no longer be usable after this point.", LogLevel.Trace);
this.Content.Dispose();
}
/****
** Asset name/key handling
****/
/// Get a directory or file path relative to the content root.
/// The target file path.
private string GetRelativePath(string targetPath)
{
return PathUtilities.GetRelativePath(this.FullRootDirectory, targetPath);
}
/// Get the locale codes (like ja-JP) used in asset keys.
/// Simplifies access to private game code.
private IDictionary GetKeyLocales(Reflector reflection)
{
#if !STARDEW_VALLEY_1_3
IReflectedField codeField = reflection.GetField(typeof(LocalizedContentManager), "_currentLangCode");
LocalizedContentManager.LanguageCode previousCode = codeField.GetValue();
#endif
string previousOverride = this.Content.LanguageCodeOverride;
try
{
// temporarily disable language override
this.Content.LanguageCodeOverride = null;
// create locale => code map
IReflectedMethod languageCodeString = reflection
#if STARDEW_VALLEY_1_3
.GetMethod(this.Content, "languageCodeString");
#else
.GetMethod(this.Content, "languageCode");
#endif
IDictionary map = new Dictionary();
foreach (LocalizedContentManager.LanguageCode code in Enum.GetValues(typeof(LocalizedContentManager.LanguageCode)))
{
#if STARDEW_VALLEY_1_3
map[code] = languageCodeString.Invoke(code);
#else
codeField.SetValue(code);
map[code] = languageCodeString.Invoke();
#endif
}
return map;
}
finally
{
// restore previous settings
this.Content.LanguageCodeOverride = previousOverride;
#if !STARDEW_VALLEY_1_3
codeField.SetValue(previousCode);
#endif
}
}
/// Get the asset name from a cache key.
/// The input cache key.
private string GetAssetName(string cacheKey)
{
this.ParseCacheKey(cacheKey, out string assetName, out string _);
return assetName;
}
/// Parse a cache key into its component parts.
/// The input cache key.
/// The original asset name.
/// The asset locale code (or null if not localised).
private void ParseCacheKey(string cacheKey, out string assetName, out string localeCode)
{
// handle localised key
if (!string.IsNullOrWhiteSpace(cacheKey))
{
int lastSepIndex = cacheKey.LastIndexOf(".", StringComparison.InvariantCulture);
if (lastSepIndex >= 0)
{
string suffix = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1);
if (this.LanguageCodes.ContainsKey(suffix))
{
assetName = cacheKey.Substring(0, lastSepIndex);
localeCode = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1);
return;
}
}
}
// handle simple key
assetName = cacheKey;
localeCode = null;
}
/****
** Cache handling
****/
/// Get whether an asset has already been loaded.
/// The normalised asset name.
private bool IsNormalisedKeyLoaded(string normalisedAssetName)
{
#if STARDEW_VALLEY_1_3
if (!this.IsLocalisableLookup.TryGetValue(normalisedAssetName, out bool localisable))
return false;
return localisable
? this.Cache.ContainsKey($"{normalisedAssetName}.{this.Locales[this.Content.GetCurrentLanguage()]}")
: this.Cache.ContainsKey(normalisedAssetName);
#else
return
this.Cache.ContainsKey(normalisedAssetName)
|| this.Cache.ContainsKey($"{normalisedAssetName}.{this.Locales[this.Content.GetCurrentLanguage()]}"); // translated asset
#endif
}
/// Track that a content manager loaded an asset.
/// The asset key that was loaded.
/// The content manager that loaded the asset.
private void TrackAssetLoader(string key, ContentManager manager)
{
if (!this.ContentManagersByAssetKey.TryGetValue(key, out HashSet hash))
hash = this.ContentManagersByAssetKey[key] = new HashSet();
hash.Add(manager);
}
/****
** Content loading
****/
/// Load an asset name without heuristics to support mod content.
/// The type of asset to load.
/// The asset path relative to the loader root directory, not including the .xnb extension.
/// The content manager instance for which to load the asset.
/// The language code for which to load content.
private T LoadImpl(string assetName, ContentManager instance
#if STARDEW_VALLEY_1_3
, LocalizedContentManager.LanguageCode language
#endif
)
{
return this.WithWriteLock(() =>
{
// skip if already loaded
if (this.IsNormalisedKeyLoaded(assetName))
{
this.TrackAssetLoader(assetName, instance);
return this.Content
#if STARDEW_VALLEY_1_3
.Load(assetName, language);
#else
.Load(assetName);
#endif
}
// load asset
T data;
if (this.AssetsBeingLoaded.Contains(assetName))
{
this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn);
this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}", LogLevel.Trace);
data = this.Content
#if STARDEW_VALLEY_1_3
.Load(assetName, language);
#else
.Load(assetName);
#endif
}
else
{
data = this.AssetsBeingLoaded.Track(assetName, () =>
{
string locale =
#if STARDEW_VALLEY_1_3
this.GetLocale(language);
#else
this.GetLocale();
#endif
IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.NormaliseAssetName);
IAssetData asset = this.ApplyLoader(info)
#if STARDEW_VALLEY_1_3
?? new AssetDataForObject(info, this.Content.Load(assetName, language), this.NormaliseAssetName);
#else
?? new AssetDataForObject(info, this.Content.Load(assetName), this.NormaliseAssetName);
#endif
asset = this.ApplyEditors(info, asset);
return (T)asset.Data;
});
}
// update cache & return data
this.InjectWithoutLock(assetName, data, instance);
return data;
});
}
/// Inject an asset into the cache without acquiring a write lock. This should only be called from within a write lock.
/// The type of asset to inject.
/// The asset path relative to the loader root directory, not including the .xnb extension.
/// The asset value.
/// The content manager instance for which to load the asset.
private void InjectWithoutLock(string assetName, T value, ContentManager instance)
{
assetName = this.NormaliseAssetName(assetName);
this.Cache[assetName] = value;
this.TrackAssetLoader(assetName, instance);
}
/// Get a file from the mod folder.
/// The asset path relative to the content folder.
private FileInfo GetModFile(string path)
{
// try exact match
FileInfo file = new FileInfo(Path.Combine(this.FullRootDirectory, path));
// try with default extension
if (!file.Exists && file.Extension.ToLower() != ".xnb")
{
FileInfo result = new FileInfo(file.FullName + ".xnb");
if (result.Exists)
file = result;
}
return file;
}
/// Load the initial asset from the registered .
/// The basic asset metadata.
/// Returns the loaded asset metadata, or null if no loader matched.
private IAssetData ApplyLoader(IAssetInfo info)
{
// find matching loaders
var loaders = this.GetInterceptors(this.Loaders)
.Where(entry =>
{
try
{
return entry.Value.CanLoad(info);
}
catch (Exception ex)
{
entry.Key.LogAsMod($"Mod failed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
return false;
}
})
.ToArray();
// validate loaders
if (!loaders.Any())
return null;
if (loaders.Length > 1)
{
string[] loaderNames = loaders.Select(p => p.Key.DisplayName).ToArray();
this.Monitor.Log($"Multiple mods want to provide the '{info.AssetName}' asset ({string.Join(", ", loaderNames)}), but an asset can't be loaded multiple times. SMAPI will use the default asset instead; uninstall one of the mods to fix this. (Message for modders: you should usually use {typeof(IAssetEditor)} instead to avoid conflicts.)", LogLevel.Warn);
return null;
}
// fetch asset from loader
IModMetadata mod = loaders[0].Key;
IAssetLoader loader = loaders[0].Value;
T data;
try
{
data = loader.Load(info);
this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.AssetName}'.", LogLevel.Trace);
}
catch (Exception ex)
{
mod.LogAsMod($"Mod crashed when loading asset '{info.AssetName}'. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
return null;
}
// validate asset
if (data == null)
{
mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Error);
return null;
}
// return matched asset
return new AssetDataForObject(info, data, this.NormaliseAssetName);
}
/// Apply any to a loaded asset.
/// The asset type.
/// The basic asset metadata.
/// The loaded asset.
private IAssetData ApplyEditors(IAssetInfo info, IAssetData asset)
{
IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.NormaliseAssetName);
// edit asset
foreach (var entry in this.GetInterceptors(this.Editors))
{
// check for match
IModMetadata mod = entry.Key;
IAssetEditor editor = entry.Value;
try
{
if (!editor.CanEdit(info))
continue;
}
catch (Exception ex)
{
mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
continue;
}
// try edit
object prevAsset = asset.Data;
try
{
editor.Edit(asset);
this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace);
}
catch (Exception ex)
{
mod.LogAsMod($"Mod crashed when editing asset '{info.AssetName}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
}
// validate edit
if (asset.Data == null)
{
mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Warn);
asset = GetNewData(prevAsset);
}
else if (!(asset.Data is T))
{
mod.LogAsMod($"Mod incorrectly set asset '{asset.AssetName}' to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn);
asset = GetNewData(prevAsset);
}
}
// return result
return asset;
}
/// Get all registered interceptors from a list.
private IEnumerable> GetInterceptors(IDictionary> entries)
{
foreach (var entry in entries)
{
IModMetadata mod = entry.Key;
IList interceptors = entry.Value;
// registered editors
foreach (T interceptor in interceptors)
yield return new KeyValuePair(mod, interceptor);
}
}
/// Premultiply a texture's alpha values to avoid transparency issues in the game. This is only possible if the game isn't currently drawing.
/// The texture to premultiply.
/// Returns a premultiplied texture.
/// Based on code by Layoric.
private Texture2D PremultiplyTransparency(Texture2D texture)
{
// validate
if (Context.IsInDrawLoop)
throw new NotSupportedException("Can't load a PNG file while the game is drawing to the screen. Make sure you load content outside the draw loop.");
// process texture
SpriteBatch spriteBatch = Game1.spriteBatch;
GraphicsDevice gpu = Game1.graphics.GraphicsDevice;
using (RenderTarget2D renderTarget = new RenderTarget2D(Game1.graphics.GraphicsDevice, texture.Width, texture.Height))
{
// create blank render target to premultiply
gpu.SetRenderTarget(renderTarget);
gpu.Clear(Color.Black);
// multiply each color by the source alpha, and write just the color values into the final texture
spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState
{
ColorDestinationBlend = Blend.Zero,
ColorWriteChannels = ColorWriteChannels.Red | ColorWriteChannels.Green | ColorWriteChannels.Blue,
AlphaDestinationBlend = Blend.Zero,
AlphaSourceBlend = Blend.SourceAlpha,
ColorSourceBlend = Blend.SourceAlpha
});
spriteBatch.Draw(texture, texture.Bounds, Color.White);
spriteBatch.End();
// copy the alpha values from the source texture into the final one without multiplying them
spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState
{
ColorWriteChannels = ColorWriteChannels.Alpha,
AlphaDestinationBlend = Blend.Zero,
ColorDestinationBlend = Blend.Zero,
AlphaSourceBlend = Blend.One,
ColorSourceBlend = Blend.One
});
spriteBatch.Draw(texture, texture.Bounds, Color.White);
spriteBatch.End();
// release GPU
gpu.SetRenderTarget(null);
// extract premultiplied data
Color[] data = new Color[texture.Width * texture.Height];
renderTarget.GetData(data);
// unset texture from GPU to regain control
gpu.Textures[0] = null;
// update texture with premultiplied data
texture.SetData(data);
}
return texture;
}
/****
** Concurrency logic
****/
/// Acquire a read lock which prevents concurrent writes to the cache while it's open.
/// The action's return value.
/// The action to perform.
private T WithReadLock(Func action)
{
try
{
this.Lock.EnterReadLock();
return action();
}
finally
{
this.Lock.ExitReadLock();
}
}
/// Acquire a write lock which prevents concurrent reads or writes to the cache while it's open.
/// The action to perform.
private void WithWriteLock(Action action)
{
try
{
this.Lock.EnterWriteLock();
action();
}
finally
{
this.Lock.ExitWriteLock();
}
}
/// Acquire a write lock which prevents concurrent reads or writes to the cache while it's open.
/// The action's return value.
/// The action to perform.
private T WithWriteLock(Func action)
{
try
{
this.Lock.EnterWriteLock();
return action();
}
finally
{
this.Lock.ExitWriteLock();
}
}
}
}