using System;
using System.IO;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using StardewValley;
namespace StardewModdingAPI.Framework
{
/// Provides an API for loading content assets.
internal class ContentHelper : IContentHelper
{
/*********
** Properties
*********/
/// SMAPI's underlying content manager.
private readonly SContentManager ContentManager;
/// The absolute path to the mod folder.
private readonly string ModFolderPath;
/// The path to the mod's folder, relative to the game's content folder (e.g. "../Mods/ModName").
private readonly string RelativeContentFolder;
/// The friendly mod name for use in errors.
private readonly string ModName;
/*********
** Public methods
*********/
/// Construct an instance.
/// SMAPI's underlying content manager.
/// The absolute path to the mod folder.
/// The friendly mod name for use in errors.
public ContentHelper(SContentManager contentManager, string modFolderPath, string modName)
{
this.ContentManager = contentManager;
this.ModFolderPath = modFolderPath;
this.ModName = modName;
this.RelativeContentFolder = this.GetRelativePath(contentManager.FullRootDirectory, modFolderPath);
}
/// 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 asset key to fetch (if the is ), or the local path to an XNB file relative to the mod folder.
/// Where to search for a matching content asset.
/// 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 key, ContentSource source)
{
// validate
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.");
// load content
try
{
switch (source)
{
case ContentSource.GameContent:
return this.ContentManager.Load(key);
case ContentSource.ModFolder:
// find content file
FileInfo file = new FileInfo(Path.Combine(this.ModFolderPath, key));
if (!file.Exists && file.Extension == "")
file = new FileInfo(Path.Combine(this.ModFolderPath, key + ".xnb"));
if (!file.Exists)
throw new ContentLoadException($"There is no file at path '{file.FullName}'.");
// get content-relative path
string contentPath = Path.Combine(this.RelativeContentFolder, key);
if (contentPath.EndsWith(".xnb"))
contentPath = contentPath.Substring(0, contentPath.Length - 4);
// load content
switch (file.Extension.ToLower())
{
case ".xnb":
return this.ContentManager.Load(contentPath);
case ".png":
// validate
if (typeof(T) != typeof(Texture2D))
throw new ContentLoadException($"Can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'.");
// try cache
if (this.ContentManager.IsLoaded(contentPath))
return this.ContentManager.Load(contentPath);
// fetch & cache
using (FileStream stream = File.OpenRead(file.FullName))
{
var texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream);
texture = this.PremultiplyTransparency(texture);
this.ContentManager.Inject(contentPath, texture);
return (T)(object)texture;
}
default:
throw new ContentLoadException($"Unknown file extension '{file.Extension}'; must be '.xnb' or '.png'.");
}
default:
throw new NotSupportedException($"Unknown content source '{source}'.");
}
}
catch (Exception ex)
{
throw new ContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}.", ex);
}
}
/*********
** Private methods
*********/
/// 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
using (RenderTarget2D renderTarget = new RenderTarget2D(Game1.graphics.GraphicsDevice, texture.Width, texture.Height))
using (SpriteBatch spriteBatch = new SpriteBatch(Game1.graphics.GraphicsDevice))
{
//Viewport originalViewport = Game1.graphics.GraphicsDevice.Viewport;
// create blank slate in render target
Game1.graphics.GraphicsDevice.SetRenderTarget(renderTarget);
Game1.graphics.GraphicsDevice.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 the GPU
Game1.graphics.GraphicsDevice.SetRenderTarget(null);
//Game1.graphics.GraphicsDevice.Viewport = originalViewport;
// store data from render target because the RenderTarget2D is volatile
var data = new Color[texture.Width * texture.Height];
renderTarget.GetData(data);
// unset texture from graphic device and set modified data back to it
Game1.graphics.GraphicsDevice.Textures[0] = null;
texture.SetData(data);
}
return texture;
}
}
}