centralise most content-loading logic to fix map tilesheet edge case (#373)
This commit is contained in:
parent
fd2b9c72a8
commit
a1eeece49b
|
@ -1,10 +1,8 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.Xna.Framework;
|
|
||||||
using Microsoft.Xna.Framework.Content;
|
using Microsoft.Xna.Framework.Content;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
using StardewModdingAPI.Framework.Exceptions;
|
using StardewModdingAPI.Framework.Exceptions;
|
||||||
|
@ -74,12 +72,12 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||||
this.ContentManager = contentManager;
|
this.ContentManager = contentManager;
|
||||||
this.ModFolderPath = modFolderPath;
|
this.ModFolderPath = modFolderPath;
|
||||||
this.ModName = modName;
|
this.ModName = modName;
|
||||||
this.ModFolderPathFromContent = this.GetRelativePath(contentManager.FullRootDirectory, modFolderPath);
|
this.ModFolderPathFromContent = this.ContentManager.GetRelativePath(modFolderPath);
|
||||||
this.Monitor = monitor;
|
this.Monitor = monitor;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Load content from the game folder or mod folder (if not already cached), and return it. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary>
|
/// <summary>Load content from the game folder or mod folder (if not already cached), and return it. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary>
|
||||||
/// <typeparam name="T">The expected data type. The main supported types are <see cref="Texture2D"/> and dictionaries; other types may be supported by the game's content pipeline.</typeparam>
|
/// <typeparam name="T">The expected data type. The main supported types are <see cref="Map"/>, <see cref="Texture2D"/>, and dictionaries; other types may be supported by the game's content pipeline.</typeparam>
|
||||||
/// <param name="key">The asset key to fetch (if the <paramref name="source"/> is <see cref="ContentSource.GameContent"/>), or the local path to a content file relative to the mod folder.</param>
|
/// <param name="key">The asset key to fetch (if the <paramref name="source"/> is <see cref="ContentSource.GameContent"/>), or the local path to a content file relative to the mod folder.</param>
|
||||||
/// <param name="source">Where to search for a matching content asset.</param>
|
/// <param name="source">Where to search for a matching content asset.</param>
|
||||||
/// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception>
|
/// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception>
|
||||||
|
@ -88,9 +86,9 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||||
{
|
{
|
||||||
SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}: {reasonPhrase}.");
|
SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}: {reasonPhrase}.");
|
||||||
|
|
||||||
this.AssertValidAssetKeyFormat(key);
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
this.ContentManager.AssertValidAssetKeyFormat(key);
|
||||||
switch (source)
|
switch (source)
|
||||||
{
|
{
|
||||||
case ContentSource.GameContent:
|
case ContentSource.GameContent:
|
||||||
|
@ -103,60 +101,32 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||||
throw GetContentError($"there's no matching file at path '{file.FullName}'.");
|
throw GetContentError($"there's no matching file at path '{file.FullName}'.");
|
||||||
|
|
||||||
// get asset path
|
// get asset path
|
||||||
string assetPath = this.GetModAssetPath(key, file.FullName);
|
string assetName = this.GetModAssetPath(key, file.FullName);
|
||||||
|
|
||||||
// try cache
|
// try cache
|
||||||
if (this.ContentManager.IsLoaded(assetPath))
|
if (this.ContentManager.IsLoaded(assetName))
|
||||||
return this.ContentManager.Load<T>(assetPath);
|
return this.ContentManager.Load<T>(assetName);
|
||||||
|
|
||||||
// load content
|
// fix map tilesheets
|
||||||
switch (file.Extension.ToLower())
|
if (file.Extension.ToLower() == ".tbin")
|
||||||
{
|
{
|
||||||
// XNB file
|
// validate
|
||||||
case ".xnb":
|
if (typeof(T) != typeof(Map))
|
||||||
{
|
throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'.");
|
||||||
T asset = this.ContentManager.Load<T>(assetPath);
|
|
||||||
if (asset is Map)
|
|
||||||
this.FixLocalMapTilesheets(asset as Map, key);
|
|
||||||
return asset;
|
|
||||||
}
|
|
||||||
|
|
||||||
// unpacked map
|
// fetch & cache
|
||||||
case ".tbin":
|
FormatManager formatManager = FormatManager.Instance;
|
||||||
{
|
Map map = formatManager.LoadMap(file.FullName);
|
||||||
// validate
|
this.FixLocalMapTilesheets(map, key);
|
||||||
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
|
// inject map
|
||||||
FormatManager formatManager = FormatManager.Instance;
|
this.ContentManager.Inject(assetName, map, this.ContentManager);
|
||||||
Map map = formatManager.LoadMap(file.FullName);
|
return (T)(object)map;
|
||||||
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'.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// load through content manager
|
||||||
|
return this.ContentManager.Load<T>(assetName);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw GetContentError($"unknown content source '{source}'.");
|
throw GetContentError($"unknown content source '{source}'.");
|
||||||
}
|
}
|
||||||
|
@ -264,8 +234,8 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string key =
|
string key =
|
||||||
this.TryLoadTilesheetImageSource(relativeMapFolder, seasonalImageSource)
|
this.GetTilesheetAssetName(relativeMapFolder, seasonalImageSource)
|
||||||
?? this.TryLoadTilesheetImageSource(relativeMapFolder, imageSource);
|
?? this.GetTilesheetAssetName(relativeMapFolder, imageSource);
|
||||||
if (key != null)
|
if (key != null)
|
||||||
{
|
{
|
||||||
tilesheet.ImageSource = key;
|
tilesheet.ImageSource = key;
|
||||||
|
@ -282,33 +252,22 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Load a tilesheet image source if the file exists.</summary>
|
/// <summary>Get the actual asset name for a tilesheet.</summary>
|
||||||
/// <param name="relativeMapFolder">The folder path containing the map, relative to the mod folder.</param>
|
/// <param name="modRelativeMapFolder">The folder path containing the map, relative to the mod folder.</param>
|
||||||
/// <param name="imageSource">The tilesheet image source to load.</param>
|
/// <param name="imageSource">The tilesheet image source to load.</param>
|
||||||
/// <returns>Returns the loaded asset key (if it was loaded successfully).</returns>
|
/// <returns>Returns the asset name.</returns>
|
||||||
/// <remarks>See remarks on <see cref="FixLocalMapTilesheets"/>.</remarks>
|
/// <remarks>See remarks on <see cref="FixLocalMapTilesheets"/>.</remarks>
|
||||||
private string TryLoadTilesheetImageSource(string relativeMapFolder, string imageSource)
|
private string GetTilesheetAssetName(string modRelativeMapFolder, string imageSource)
|
||||||
{
|
{
|
||||||
if (imageSource == null)
|
if (imageSource == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
// check relative to map file
|
// check relative to map file
|
||||||
{
|
{
|
||||||
string localKey = Path.Combine(relativeMapFolder, imageSource);
|
string localKey = Path.Combine(modRelativeMapFolder, imageSource);
|
||||||
FileInfo localFile = this.GetModFile(localKey);
|
FileInfo localFile = this.GetModFile(localKey);
|
||||||
if (localFile.Exists)
|
if (localFile.Exists)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
this.Load<Texture2D>(localKey);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw new ContentLoadException($"The local '{imageSource}' tilesheet couldn't be loaded.", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.GetActualAssetKey(localKey);
|
return this.GetActualAssetKey(localKey);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check relative to content folder
|
// check relative to content folder
|
||||||
|
@ -343,18 +302,6 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Assert that the given key has a valid format.</summary>
|
|
||||||
/// <param name="key">The asset key to check.</param>
|
|
||||||
/// <exception cref="ArgumentException">The asset key is empty or contains invalid characters.</exception>
|
|
||||||
[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.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Get a file from the mod folder.</summary>
|
/// <summary>Get a file from the mod folder.</summary>
|
||||||
/// <param name="path">The asset path relative to the mod folder.</param>
|
/// <param name="path">The asset path relative to the mod folder.</param>
|
||||||
private FileInfo GetModFile(string path)
|
private FileInfo GetModFile(string path)
|
||||||
|
@ -400,81 +347,5 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||||
return absolutePath;
|
return absolutePath;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Get a directory path relative to a given root.</summary>
|
|
||||||
/// <param name="rootPath">The root path from which the path should be relative.</param>
|
|
||||||
/// <param name="targetPath">The target file path.</param>
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Premultiply a texture's alpha values to avoid transparency issues in the game. This is only possible if the game isn't currently drawing.</summary>
|
|
||||||
/// <param name="texture">The texture to premultiply.</param>
|
|
||||||
/// <returns>Returns a premultiplied texture.</returns>
|
|
||||||
/// <remarks>Based on <a href="https://gist.github.com/Layoric/6255384">code by Layoric</a>.</remarks>
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Diagnostics.Contracts;
|
using System.Diagnostics.Contracts;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
using Microsoft.Xna.Framework.Content;
|
using Microsoft.Xna.Framework.Content;
|
||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
using StardewModdingAPI.Framework.Content;
|
using StardewModdingAPI.Framework.Content;
|
||||||
|
using StardewModdingAPI.Framework.Exceptions;
|
||||||
using StardewModdingAPI.Framework.Reflection;
|
using StardewModdingAPI.Framework.Reflection;
|
||||||
using StardewModdingAPI.Framework.Utilities;
|
using StardewModdingAPI.Framework.Utilities;
|
||||||
using StardewModdingAPI.Metadata;
|
using StardewModdingAPI.Metadata;
|
||||||
|
@ -55,6 +59,9 @@ namespace StardewModdingAPI.Framework
|
||||||
/// <summary>A lookup of the content managers which loaded each asset.</summary>
|
/// <summary>A lookup of the content managers which loaded each asset.</summary>
|
||||||
private readonly IDictionary<string, HashSet<ContentManager>> ContentManagersByAssetKey = new Dictionary<string, HashSet<ContentManager>>();
|
private readonly IDictionary<string, HashSet<ContentManager>> ContentManagersByAssetKey = new Dictionary<string, HashSet<ContentManager>>();
|
||||||
|
|
||||||
|
/// <summary>The path prefix for assets in mod folders.</summary>
|
||||||
|
private readonly string ModContentPrefix;
|
||||||
|
|
||||||
/// <summary>A lock used to prevents concurrent changes to the cache while data is being read.</summary>
|
/// <summary>A lock used to prevents concurrent changes to the cache while data is being read.</summary>
|
||||||
private readonly ReaderWriterLockSlim Lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
|
private readonly ReaderWriterLockSlim Lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
|
||||||
|
|
||||||
|
@ -78,6 +85,9 @@ namespace StardewModdingAPI.Framework
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
*********/
|
*********/
|
||||||
|
/****
|
||||||
|
** Constructor
|
||||||
|
****/
|
||||||
/// <summary>Construct an instance.</summary>
|
/// <summary>Construct an instance.</summary>
|
||||||
/// <param name="serviceProvider">The service provider to use to locate services.</param>
|
/// <param name="serviceProvider">The service provider to use to locate services.</param>
|
||||||
/// <param name="rootDirectory">The root directory to search for content.</param>
|
/// <param name="rootDirectory">The root directory to search for content.</param>
|
||||||
|
@ -92,12 +102,16 @@ namespace StardewModdingAPI.Framework
|
||||||
this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor));
|
this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor));
|
||||||
this.Cache = new ContentCache(this, reflection, SContentManager.PossiblePathSeparators, SContentManager.PreferredPathSeparator);
|
this.Cache = new ContentCache(this, reflection, SContentManager.PossiblePathSeparators, SContentManager.PreferredPathSeparator);
|
||||||
this.GetKeyLocale = reflection.GetPrivateMethod(this, "languageCode");
|
this.GetKeyLocale = reflection.GetPrivateMethod(this, "languageCode");
|
||||||
|
this.ModContentPrefix = this.GetRelativePath(Constants.ModPath);
|
||||||
|
|
||||||
// get asset data
|
// get asset data
|
||||||
this.CoreAssets = new CoreAssets(this.NormaliseAssetName);
|
this.CoreAssets = new CoreAssets(this.NormaliseAssetName);
|
||||||
this.KeyLocales = this.GetKeyLocales(reflection);
|
this.KeyLocales = this.GetKeyLocales(reflection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/****
|
||||||
|
** Asset key/name handling
|
||||||
|
****/
|
||||||
/// <summary>Normalise path separators in a file path. For asset keys, see <see cref="NormaliseAssetName"/> instead.</summary>
|
/// <summary>Normalise path separators in a file path. For asset keys, see <see cref="NormaliseAssetName"/> instead.</summary>
|
||||||
/// <param name="path">The file path to normalise.</param>
|
/// <param name="path">The file path to normalise.</param>
|
||||||
[Pure]
|
[Pure]
|
||||||
|
@ -114,6 +128,42 @@ namespace StardewModdingAPI.Framework
|
||||||
return this.Cache.NormaliseKey(assetName);
|
return this.Cache.NormaliseKey(assetName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Assert that the given key has a valid format.</summary>
|
||||||
|
/// <param name="key">The asset key to check.</param>
|
||||||
|
/// <exception cref="ArgumentException">The asset key is empty or contains invalid characters.</exception>
|
||||||
|
[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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get a directory path relative to the content root.</summary>
|
||||||
|
/// <param name="targetPath">The target file path.</param>
|
||||||
|
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
|
||||||
|
****/
|
||||||
|
/// <summary>Get the current content locale.</summary>
|
||||||
|
public string GetLocale()
|
||||||
|
{
|
||||||
|
return this.GetKeyLocale.Invoke<string>();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Get whether the content manager has already loaded and cached the given asset.</summary>
|
/// <summary>Get whether the content manager has already loaded and cached the given asset.</summary>
|
||||||
/// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
|
/// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
|
||||||
public bool IsLoaded(string assetName)
|
public bool IsLoaded(string assetName)
|
||||||
|
@ -122,76 +172,6 @@ namespace StardewModdingAPI.Framework
|
||||||
return this.WithReadLock(() => this.IsNormalisedKeyLoaded(assetName));
|
return this.WithReadLock(() => this.IsNormalisedKeyLoaded(assetName));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Load an asset that has been processed by the content pipeline.</summary>
|
|
||||||
/// <typeparam name="T">The type of asset to load.</typeparam>
|
|
||||||
/// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
|
|
||||||
public override T Load<T>(string assetName)
|
|
||||||
{
|
|
||||||
return this.LoadFor<T>(assetName, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Load an asset that has been processed by the content pipeline.</summary>
|
|
||||||
/// <typeparam name="T">The type of asset to load.</typeparam>
|
|
||||||
/// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
|
|
||||||
/// <param name="instance">The content manager instance for which to load the asset.</param>
|
|
||||||
public T LoadFor<T>(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<T>(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<T>(assetName);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
data = this.AssetsBeingLoaded.Track(assetName, () =>
|
|
||||||
{
|
|
||||||
IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, typeof(T), this.NormaliseAssetName);
|
|
||||||
IAssetData asset = this.ApplyLoader<T>(info) ?? new AssetDataForObject(info, base.Load<T>(assetName), this.NormaliseAssetName);
|
|
||||||
asset = this.ApplyEditors<T>(info, asset);
|
|
||||||
return (T)asset.Data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// update cache & return data
|
|
||||||
this.Cache[assetName] = data;
|
|
||||||
this.TrackAssetLoader(assetName, instance);
|
|
||||||
return data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Inject an asset into the cache.</summary>
|
|
||||||
/// <typeparam name="T">The type of asset to inject.</typeparam>
|
|
||||||
/// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
|
|
||||||
/// <param name="value">The asset value.</param>
|
|
||||||
public void Inject<T>(string assetName, T value)
|
|
||||||
{
|
|
||||||
this.WithWriteLock(() =>
|
|
||||||
{
|
|
||||||
assetName = this.NormaliseAssetName(assetName);
|
|
||||||
this.Cache[assetName] = value;
|
|
||||||
this.TrackAssetLoader(assetName, this);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Get the current content locale.</summary>
|
|
||||||
public string GetLocale()
|
|
||||||
{
|
|
||||||
return this.GetKeyLocale.Invoke<string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Get the cached asset keys.</summary>
|
/// <summary>Get the cached asset keys.</summary>
|
||||||
public IEnumerable<string> GetAssetKeys()
|
public IEnumerable<string> GetAssetKeys()
|
||||||
{
|
{
|
||||||
|
@ -202,6 +182,95 @@ namespace StardewModdingAPI.Framework
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Load an asset through the content pipeline. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary>
|
||||||
|
/// <typeparam name="T">The expected asset type.</typeparam>
|
||||||
|
/// <param name="assetName">The asset path relative to the content directory.</param>
|
||||||
|
public override T Load<T>(string assetName)
|
||||||
|
{
|
||||||
|
return this.LoadFor<T>(assetName, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Load an asset through the content pipeline. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary>
|
||||||
|
/// <typeparam name="T">The expected asset type.</typeparam>
|
||||||
|
/// <param name="assetName">The asset path relative to the content directory.</param>
|
||||||
|
/// <param name="instance">The content manager instance for which to load the asset.</param>
|
||||||
|
/// <exception cref="ArgumentException">The <paramref name="assetName"/> is empty or contains invalid characters.</exception>
|
||||||
|
/// <exception cref="ContentLoadException">The content asset couldn't be loaded (e.g. because it doesn't exist).</exception>
|
||||||
|
public T LoadFor<T>(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<T>(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<T>(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<T>(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Inject an asset into the cache.</summary>
|
||||||
|
/// <typeparam name="T">The type of asset to inject.</typeparam>
|
||||||
|
/// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
|
||||||
|
/// <param name="value">The asset value.</param>
|
||||||
|
/// <param name="instance">The content manager instance for which to load the asset.</param>
|
||||||
|
public void Inject<T>(string assetName, T value, ContentManager instance)
|
||||||
|
{
|
||||||
|
this.WithWriteLock(() => this.InjectWithoutLock(assetName, value, instance));
|
||||||
|
}
|
||||||
|
|
||||||
|
/****
|
||||||
|
** Cache invalidation
|
||||||
|
****/
|
||||||
/// <summary>Purge assets from the cache that match one of the interceptors.</summary>
|
/// <summary>Purge assets from the cache that match one of the interceptors.</summary>
|
||||||
/// <param name="editors">The asset editors for which to purge matching assets.</param>
|
/// <param name="editors">The asset editors for which to purge matching assets.</param>
|
||||||
/// <param name="loaders">The asset loaders for which to purge matching assets.</param>
|
/// <param name="loaders">The asset loaders for which to purge matching assets.</param>
|
||||||
|
@ -279,6 +348,9 @@ namespace StardewModdingAPI.Framework
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/****
|
||||||
|
** Disposal
|
||||||
|
****/
|
||||||
/// <summary>Dispose assets for the given content manager shim.</summary>
|
/// <summary>Dispose assets for the given content manager shim.</summary>
|
||||||
/// <param name="shim">The content manager whose assets to dispose.</param>
|
/// <param name="shim">The content manager whose assets to dispose.</param>
|
||||||
internal void DisposeFor(ContentManagerShim shim)
|
internal void DisposeFor(ContentManagerShim shim)
|
||||||
|
@ -297,6 +369,9 @@ namespace StardewModdingAPI.Framework
|
||||||
/*********
|
/*********
|
||||||
** Private methods
|
** Private methods
|
||||||
*********/
|
*********/
|
||||||
|
/****
|
||||||
|
** Disposal
|
||||||
|
****/
|
||||||
/// <summary>Dispose held resources.</summary>
|
/// <summary>Dispose held resources.</summary>
|
||||||
/// <param name="disposing">Whether the content manager is disposing (rather than finalising).</param>
|
/// <param name="disposing">Whether the content manager is disposing (rather than finalising).</param>
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
|
@ -305,24 +380,9 @@ namespace StardewModdingAPI.Framework
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Get whether an asset has already been loaded.</summary>
|
/****
|
||||||
/// <param name="normalisedAssetName">The normalised asset name.</param>
|
** Asset name/key handling
|
||||||
private bool IsNormalisedKeyLoaded(string normalisedAssetName)
|
****/
|
||||||
{
|
|
||||||
return this.Cache.ContainsKey(normalisedAssetName)
|
|
||||||
|| this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke<string>()}"); // translated asset
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Track that a content manager loaded an asset.</summary>
|
|
||||||
/// <param name="key">The asset key that was loaded.</param>
|
|
||||||
/// <param name="manager">The content manager that loaded the asset.</param>
|
|
||||||
private void TrackAssetLoader(string key, ContentManager manager)
|
|
||||||
{
|
|
||||||
if (!this.ContentManagersByAssetKey.TryGetValue(key, out HashSet<ContentManager> hash))
|
|
||||||
hash = this.ContentManagersByAssetKey[key] = new HashSet<ContentManager>();
|
|
||||||
hash.Add(manager);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Get the locale codes (like <c>ja-JP</c>) used in asset keys.</summary>
|
/// <summary>Get the locale codes (like <c>ja-JP</c>) used in asset keys.</summary>
|
||||||
/// <param name="reflection">Simplifies access to private game code.</param>
|
/// <param name="reflection">Simplifies access to private game code.</param>
|
||||||
private IDictionary<string, LanguageCode> GetKeyLocales(Reflector reflection)
|
private IDictionary<string, LanguageCode> GetKeyLocales(Reflector reflection)
|
||||||
|
@ -385,6 +445,113 @@ namespace StardewModdingAPI.Framework
|
||||||
localeCode = null;
|
localeCode = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/****
|
||||||
|
** Cache handling
|
||||||
|
****/
|
||||||
|
/// <summary>Get whether an asset has already been loaded.</summary>
|
||||||
|
/// <param name="normalisedAssetName">The normalised asset name.</param>
|
||||||
|
private bool IsNormalisedKeyLoaded(string normalisedAssetName)
|
||||||
|
{
|
||||||
|
return this.Cache.ContainsKey(normalisedAssetName)
|
||||||
|
|| this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke<string>()}"); // translated asset
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Track that a content manager loaded an asset.</summary>
|
||||||
|
/// <param name="key">The asset key that was loaded.</param>
|
||||||
|
/// <param name="manager">The content manager that loaded the asset.</param>
|
||||||
|
private void TrackAssetLoader(string key, ContentManager manager)
|
||||||
|
{
|
||||||
|
if (!this.ContentManagersByAssetKey.TryGetValue(key, out HashSet<ContentManager> hash))
|
||||||
|
hash = this.ContentManagersByAssetKey[key] = new HashSet<ContentManager>();
|
||||||
|
hash.Add(manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
/****
|
||||||
|
** Content loading
|
||||||
|
****/
|
||||||
|
/// <summary>Load an asset name without heuristics to support mod content.</summary>
|
||||||
|
/// <typeparam name="T">The type of asset to load.</typeparam>
|
||||||
|
/// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
|
||||||
|
/// <param name="instance">The content manager instance for which to load the asset.</param>
|
||||||
|
private T LoadImpl<T>(string assetName, ContentManager instance)
|
||||||
|
{
|
||||||
|
return this.WithWriteLock(() =>
|
||||||
|
{
|
||||||
|
// skip if already loaded
|
||||||
|
if (this.IsNormalisedKeyLoaded(assetName))
|
||||||
|
{
|
||||||
|
this.TrackAssetLoader(assetName, instance);
|
||||||
|
return base.Load<T>(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<T>(assetName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
data = this.AssetsBeingLoaded.Track(assetName, () =>
|
||||||
|
{
|
||||||
|
IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, typeof(T), this.NormaliseAssetName);
|
||||||
|
IAssetData asset = this.ApplyLoader<T>(info) ?? new AssetDataForObject(info, base.Load<T>(assetName), this.NormaliseAssetName);
|
||||||
|
asset = this.ApplyEditors<T>(info, asset);
|
||||||
|
return (T)asset.Data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// update cache & return data
|
||||||
|
this.InjectWithoutLock(assetName, data, instance);
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Inject an asset into the cache without acquiring a write lock. This should only be called from within a write lock.</summary>
|
||||||
|
/// <typeparam name="T">The type of asset to inject.</typeparam>
|
||||||
|
/// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
|
||||||
|
/// <param name="value">The asset value.</param>
|
||||||
|
/// <param name="instance">The content manager instance for which to load the asset.</param>
|
||||||
|
private void InjectWithoutLock<T>(string assetName, T value, ContentManager instance)
|
||||||
|
{
|
||||||
|
assetName = this.NormaliseAssetName(assetName);
|
||||||
|
this.Cache[assetName] = value;
|
||||||
|
this.TrackAssetLoader(assetName, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get a file from the mod folder.</summary>
|
||||||
|
/// <param name="path">The asset path relative to the content folder.</param>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get a file from the game's content folder.</summary>
|
||||||
|
/// <param name="key">The asset key.</param>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Load the initial asset from the registered <see cref="Loaders"/>.</summary>
|
/// <summary>Load the initial asset from the registered <see cref="Loaders"/>.</summary>
|
||||||
/// <param name="info">The basic asset metadata.</param>
|
/// <param name="info">The basic asset metadata.</param>
|
||||||
/// <returns>Returns the loaded asset metadata, or <c>null</c> if no loader matched.</returns>
|
/// <returns>Returns the loaded asset metadata, or <c>null</c> if no loader matched.</returns>
|
||||||
|
@ -510,6 +677,69 @@ namespace StardewModdingAPI.Framework
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Premultiply a texture's alpha values to avoid transparency issues in the game. This is only possible if the game isn't currently drawing.</summary>
|
||||||
|
/// <param name="texture">The texture to premultiply.</param>
|
||||||
|
/// <returns>Returns a premultiplied texture.</returns>
|
||||||
|
/// <remarks>Based on <a href="https://gist.github.com/Layoric/6255384">code by Layoric</a>.</remarks>
|
||||||
|
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
|
||||||
|
****/
|
||||||
/// <summary>Acquire a read lock which prevents concurrent writes to the cache while it's open.</summary>
|
/// <summary>Acquire a read lock which prevents concurrent writes to the cache while it's open.</summary>
|
||||||
/// <typeparam name="T">The action's return value.</typeparam>
|
/// <typeparam name="T">The action's return value.</typeparam>
|
||||||
/// <param name="action">The action to perform.</param>
|
/// <param name="action">The action to perform.</param>
|
||||||
|
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||||
using Microsoft.Xna.Framework.Content;
|
using Microsoft.Xna.Framework.Content;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
using StardewValley;
|
using StardewValley;
|
||||||
|
using xTile;
|
||||||
|
|
||||||
namespace StardewModdingAPI
|
namespace StardewModdingAPI
|
||||||
{
|
{
|
||||||
|
@ -29,7 +30,7 @@ namespace StardewModdingAPI
|
||||||
** Public methods
|
** Public methods
|
||||||
*********/
|
*********/
|
||||||
/// <summary>Load content from the game folder or mod folder (if not already cached), and return it. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary>
|
/// <summary>Load content from the game folder or mod folder (if not already cached), and return it. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary>
|
||||||
/// <typeparam name="T">The expected data type. The main supported types are <see cref="Texture2D"/> and dictionaries; other types may be supported by the game's content pipeline.</typeparam>
|
/// <typeparam name="T">The expected data type. The main supported types are <see cref="Map"/>, <see cref="Texture2D"/>, and dictionaries; other types may be supported by the game's content pipeline.</typeparam>
|
||||||
/// <param name="key">The asset key to fetch (if the <paramref name="source"/> is <see cref="ContentSource.GameContent"/>), or the local path to a content file relative to the mod folder.</param>
|
/// <param name="key">The asset key to fetch (if the <paramref name="source"/> is <see cref="ContentSource.GameContent"/>), or the local path to a content file relative to the mod folder.</param>
|
||||||
/// <param name="source">Where to search for a matching content asset.</param>
|
/// <param name="source">Where to search for a matching content asset.</param>
|
||||||
/// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception>
|
/// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception>
|
||||||
|
|
Loading…
Reference in New Issue