diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
index 4f5bd2f0..2dd8a2e3 100644
--- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
@@ -1,10 +1,8 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
-using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
-using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.Framework.Exceptions;
@@ -74,12 +72,12 @@ namespace StardewModdingAPI.Framework.ModHelpers
this.ContentManager = contentManager;
this.ModFolderPath = modFolderPath;
this.ModName = modName;
- this.ModFolderPathFromContent = this.GetRelativePath(contentManager.FullRootDirectory, modFolderPath);
+ this.ModFolderPathFromContent = this.ContentManager.GetRelativePath(modFolderPath);
this.Monitor = monitor;
}
/// Load content from the game folder or mod folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop.
- /// The expected data type. The main supported types are and dictionaries; other types may be supported by the game's content pipeline.
+ /// The expected data type. The main supported types are , , and dictionaries; other types may be supported by the game's content pipeline.
/// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder.
/// Where to search for a matching content asset.
/// The is empty or contains invalid characters.
@@ -88,9 +86,9 @@ namespace StardewModdingAPI.Framework.ModHelpers
{
SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}: {reasonPhrase}.");
- this.AssertValidAssetKeyFormat(key);
try
{
+ this.ContentManager.AssertValidAssetKeyFormat(key);
switch (source)
{
case ContentSource.GameContent:
@@ -103,60 +101,32 @@ namespace StardewModdingAPI.Framework.ModHelpers
throw GetContentError($"there's no matching file at path '{file.FullName}'.");
// get asset path
- string assetPath = this.GetModAssetPath(key, file.FullName);
+ string assetName = this.GetModAssetPath(key, file.FullName);
// try cache
- if (this.ContentManager.IsLoaded(assetPath))
- return this.ContentManager.Load(assetPath);
+ if (this.ContentManager.IsLoaded(assetName))
+ return this.ContentManager.Load(assetName);
- // load content
- switch (file.Extension.ToLower())
+ // fix map tilesheets
+ if (file.Extension.ToLower() == ".tbin")
{
- // XNB file
- case ".xnb":
- {
- T asset = this.ContentManager.Load(assetPath);
- if (asset is Map)
- this.FixLocalMapTilesheets(asset as Map, key);
- return asset;
- }
+ // validate
+ if (typeof(T) != typeof(Map))
+ throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'.");
- // unpacked map
- case ".tbin":
- {
- // validate
- if (typeof(T) != typeof(Map))
- throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'.");
+ // fetch & cache
+ FormatManager formatManager = FormatManager.Instance;
+ Map map = formatManager.LoadMap(file.FullName);
+ this.FixLocalMapTilesheets(map, key);
- // fetch & cache
- FormatManager formatManager = FormatManager.Instance;
- Map map = formatManager.LoadMap(file.FullName);
- this.FixLocalMapTilesheets(map, key);
-
- // inject map
- this.ContentManager.Inject(assetPath, map);
- return (T)(object)map;
- }
-
- // 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.ContentManager.Inject(assetPath, texture);
- return (T)(object)texture;
- }
-
- default:
- throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.png', '.tbin', or '.xnb'.");
+ // inject map
+ this.ContentManager.Inject(assetName, map, this.ContentManager);
+ return (T)(object)map;
}
+ // load through content manager
+ return this.ContentManager.Load(assetName);
+
default:
throw GetContentError($"unknown content source '{source}'.");
}
@@ -264,8 +234,8 @@ namespace StardewModdingAPI.Framework.ModHelpers
try
{
string key =
- this.TryLoadTilesheetImageSource(relativeMapFolder, seasonalImageSource)
- ?? this.TryLoadTilesheetImageSource(relativeMapFolder, imageSource);
+ this.GetTilesheetAssetName(relativeMapFolder, seasonalImageSource)
+ ?? this.GetTilesheetAssetName(relativeMapFolder, imageSource);
if (key != null)
{
tilesheet.ImageSource = key;
@@ -282,33 +252,22 @@ namespace StardewModdingAPI.Framework.ModHelpers
}
}
- /// Load a tilesheet image source if the file exists.
- /// The folder path containing the map, relative to the mod folder.
+ /// Get the actual asset name for a tilesheet.
+ /// The folder path containing the map, relative to the mod folder.
/// The tilesheet image source to load.
- /// Returns the loaded asset key (if it was loaded successfully).
+ /// Returns the asset name.
/// See remarks on .
- private string TryLoadTilesheetImageSource(string relativeMapFolder, string imageSource)
+ private string GetTilesheetAssetName(string modRelativeMapFolder, string imageSource)
{
if (imageSource == null)
return null;
// check relative to map file
{
- string localKey = Path.Combine(relativeMapFolder, imageSource);
+ string localKey = Path.Combine(modRelativeMapFolder, imageSource);
FileInfo localFile = this.GetModFile(localKey);
if (localFile.Exists)
- {
- try
- {
- this.Load(localKey);
- }
- catch (Exception ex)
- {
- throw new ContentLoadException($"The local '{imageSource}' tilesheet couldn't be loaded.", ex);
- }
-
return this.GetActualAssetKey(localKey);
- }
}
// check relative to content folder
@@ -343,18 +302,6 @@ namespace StardewModdingAPI.Framework.ModHelpers
return null;
}
- /// 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", "UnusedParameter.Local", Justification = "Parameter is only used for assertion checks by design.")]
- private 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.");
- }
-
/// Get a file from the mod folder.
/// The asset path relative to the mod folder.
private FileInfo GetModFile(string path)
@@ -400,81 +347,5 @@ namespace StardewModdingAPI.Framework.ModHelpers
return absolutePath;
#endif
}
-
- /// Get a directory path relative to a given root.
- /// The root path from which the path should be relative.
- /// The target file path.
- private string GetRelativePath(string rootPath, string targetPath)
- {
- // convert to URIs
- Uri from = new Uri(rootPath + "/");
- Uri to = new Uri(targetPath + "/");
- if (from.Scheme != to.Scheme)
- throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{rootPath}'.");
-
- // get relative path
- return Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString())
- .Replace(Path.DirectorySeparatorChar == '/' ? '\\' : '/', Path.DirectorySeparatorChar); // use correct separator for platform
- }
-
- /// 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;
- }
}
}
diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs
index 0b6daaa6..10d854d9 100644
--- a/src/SMAPI/Framework/SContentManager.cs
+++ b/src/SMAPI/Framework/SContentManager.cs
@@ -1,13 +1,17 @@
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;
@@ -55,6 +59,9 @@ namespace StardewModdingAPI.Framework
/// 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);
@@ -78,6 +85,9 @@ namespace StardewModdingAPI.Framework
/*********
** Public methods
*********/
+ /****
+ ** Constructor
+ ****/
/// Construct an instance.
/// The service provider to use to locate services.
/// The root directory to search for content.
@@ -92,12 +102,16 @@ namespace StardewModdingAPI.Framework
this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor));
this.Cache = new ContentCache(this, reflection, SContentManager.PossiblePathSeparators, SContentManager.PreferredPathSeparator);
this.GetKeyLocale = reflection.GetPrivateMethod(this, "languageCode");
+ this.ModContentPrefix = this.GetRelativePath(Constants.ModPath);
// get asset data
this.CoreAssets = new CoreAssets(this.NormaliseAssetName);
this.KeyLocales = this.GetKeyLocales(reflection);
}
+ /****
+ ** Asset key/name handling
+ ****/
/// Normalise path separators in a file path. For asset keys, see instead.
/// The file path to normalise.
[Pure]
@@ -114,6 +128,42 @@ namespace StardewModdingAPI.Framework
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.");
+ }
+
+ /// Get a directory path relative to the content root.
+ /// The target file path.
+ public string GetRelativePath(string targetPath)
+ {
+ // convert to URIs
+ Uri from = new Uri(this.FullRootDirectory + "/");
+ Uri to = new Uri(targetPath + "/");
+ if (from.Scheme != to.Scheme)
+ throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{this.FullRootDirectory}'.");
+
+ // get relative path
+ return Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString())
+ .Replace(Path.DirectorySeparatorChar == '/' ? '\\' : '/', Path.DirectorySeparatorChar); // use correct separator for platform
+ }
+
+ /****
+ ** Content loading
+ ****/
+ /// Get the current content locale.
+ public string GetLocale()
+ {
+ return this.GetKeyLocale.Invoke();
+ }
+
/// 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)
@@ -122,76 +172,6 @@ namespace StardewModdingAPI.Framework
return this.WithReadLock(() => this.IsNormalisedKeyLoaded(assetName));
}
- /// Load an asset that has been processed by the content pipeline.
- /// The type of asset to load.
- /// The asset path relative to the loader root directory, not including the .xnb extension.
- public override T Load(string assetName)
- {
- return this.LoadFor(assetName, this);
- }
-
- /// Load an asset that has been processed by the content pipeline.
- /// 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.
- public T LoadFor(string assetName, ContentManager instance)
- {
- assetName = this.NormaliseAssetName(assetName);
- return this.WithWriteLock(() =>
- {
- // skip if already loaded
- if (this.IsNormalisedKeyLoaded(assetName))
- {
- this.TrackAssetLoader(assetName, instance);
- return base.Load(assetName);
- }
-
- // 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 = base.Load(assetName);
- }
- else
- {
- data = this.AssetsBeingLoaded.Track(assetName, () =>
- {
- IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, typeof(T), this.NormaliseAssetName);
- IAssetData asset = this.ApplyLoader(info) ?? new AssetDataForObject(info, base.Load(assetName), this.NormaliseAssetName);
- asset = this.ApplyEditors(info, asset);
- return (T)asset.Data;
- });
- }
-
- // update cache & return data
- this.Cache[assetName] = data;
- this.TrackAssetLoader(assetName, instance);
- return data;
- });
- }
-
- /// 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.
- public void Inject(string assetName, T value)
- {
- this.WithWriteLock(() =>
- {
- assetName = this.NormaliseAssetName(assetName);
- this.Cache[assetName] = value;
- this.TrackAssetLoader(assetName, this);
- });
- }
-
- /// Get the current content locale.
- public string GetLocale()
- {
- return this.GetKeyLocale.Invoke();
- }
-
/// Get the cached asset keys.
public IEnumerable GetAssetKeys()
{
@@ -202,6 +182,95 @@ namespace StardewModdingAPI.Framework
);
}
+ /// 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.
+ public override T Load(string assetName)
+ {
+ return this.LoadFor(assetName, this);
+ }
+
+ /// 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 is empty or contains invalid characters.
+ /// The content asset couldn't be loaded (e.g. because it doesn't exist).
+ public T LoadFor(string assetName, ContentManager instance)
+ {
+ // normalise asset key
+ this.AssertValidAssetKeyFormat(assetName);
+ assetName = this.NormaliseAssetName(assetName);
+
+ // load game content
+ if (!assetName.StartsWith(this.ModContentPrefix))
+ return this.LoadImpl(assetName, instance);
+
+ // 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))
+ return this.LoadImpl(assetName, instance);
+
+ // 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":
+ return this.LoadImpl(assetName, instance);
+
+ // 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))
+ {
+ 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.
@@ -279,6 +348,9 @@ namespace StardewModdingAPI.Framework
});
}
+ /****
+ ** Disposal
+ ****/
/// Dispose assets for the given content manager shim.
/// The content manager whose assets to dispose.
internal void DisposeFor(ContentManagerShim shim)
@@ -297,6 +369,9 @@ namespace StardewModdingAPI.Framework
/*********
** Private methods
*********/
+ /****
+ ** Disposal
+ ****/
/// Dispose held resources.
/// Whether the content manager is disposing (rather than finalising).
protected override void Dispose(bool disposing)
@@ -305,24 +380,9 @@ namespace StardewModdingAPI.Framework
base.Dispose(disposing);
}
- /// Get whether an asset has already been loaded.
- /// The normalised asset name.
- private bool IsNormalisedKeyLoaded(string normalisedAssetName)
- {
- return this.Cache.ContainsKey(normalisedAssetName)
- || this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke()}"); // translated asset
- }
-
- /// 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);
- }
-
+ /****
+ ** Asset name/key handling
+ ****/
/// Get the locale codes (like ja-JP) used in asset keys.
/// Simplifies access to private game code.
private IDictionary GetKeyLocales(Reflector reflection)
@@ -385,6 +445,113 @@ namespace StardewModdingAPI.Framework
localeCode = null;
}
+ /****
+ ** Cache handling
+ ****/
+ /// Get whether an asset has already been loaded.
+ /// The normalised asset name.
+ private bool IsNormalisedKeyLoaded(string normalisedAssetName)
+ {
+ return this.Cache.ContainsKey(normalisedAssetName)
+ || this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke()}"); // translated asset
+ }
+
+ /// 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.
+ private T LoadImpl(string assetName, ContentManager instance)
+ {
+ return this.WithWriteLock(() =>
+ {
+ // skip if already loaded
+ if (this.IsNormalisedKeyLoaded(assetName))
+ {
+ this.TrackAssetLoader(assetName, instance);
+ return base.Load(assetName);
+ }
+
+ // 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 = base.Load(assetName);
+ }
+ else
+ {
+ data = this.AssetsBeingLoaded.Track(assetName, () =>
+ {
+ IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, typeof(T), this.NormaliseAssetName);
+ IAssetData asset = this.ApplyLoader(info) ?? new AssetDataForObject(info, base.Load(assetName), this.NormaliseAssetName);
+ 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(path + ".xnb");
+ if (result.Exists)
+ file = result;
+ }
+
+ return file;
+ }
+
+ /// Get a file from the game's content folder.
+ /// The asset key.
+ private FileInfo GetContentFolderFile(string key)
+ {
+ // get file path
+ string path = Path.Combine(this.FullRootDirectory, key);
+ if (!path.EndsWith(".xnb"))
+ path += ".xnb";
+
+ // get file
+ return new FileInfo(path);
+ }
+
/// Load the initial asset from the registered .
/// The basic asset metadata.
/// Returns the loaded asset metadata, or null if no loader matched.
@@ -510,6 +677,69 @@ namespace StardewModdingAPI.Framework
}
}
+ /// 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.
diff --git a/src/SMAPI/IContentHelper.cs b/src/SMAPI/IContentHelper.cs
index b78b165b..7900809f 100644
--- a/src/SMAPI/IContentHelper.cs
+++ b/src/SMAPI/IContentHelper.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using StardewValley;
+using xTile;
namespace StardewModdingAPI
{
@@ -29,7 +30,7 @@ namespace StardewModdingAPI
** Public methods
*********/
/// Load content from the game folder or mod folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop.
- /// The expected data type. The main supported types are and dictionaries; other types may be supported by the game's content pipeline.
+ /// The expected data type. The main supported types are , , and dictionaries; other types may be supported by the game's content pipeline.
/// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder.
/// Where to search for a matching content asset.
/// The is empty or contains invalid characters.