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; } } }