// Copyright (c) Craftwork Games. All rights reserved. // Licensed under the MIT license. // See LICENSE file in the project root for full license information. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using MonoGame.Extended.BitmapFonts; using MonoGame.Extended.Content.BitmapFonts; using MonoGame.Extended.Content.TexturePacker; using MonoGame.Extended.Graphics; namespace MonoGame.Extended.Content; /// /// Extends the to provide additional loading functionality and additional asset types /// public class ExtendedContentManager : ContentManager { private List _disposableAssets; private readonly IGraphicsDeviceService _graphicsDeviceService; /// /// Gets the collection of loaded disposable assets that will be disposed when this content manager is disposed. /// public List DisposeableAssets { get { if (_disposableAssets is null) { // MonoGame please make this protected so subclass have access plz FieldInfo field = typeof(ContentManager).GetField(nameof(_disposableAssets), BindingFlags.NonPublic | BindingFlags.Instance); if (field is null) { throw new InvalidOperationException("Unable to get source disposable assets field"); } _disposableAssets = field.GetValue(this) as List; } return _disposableAssets; } } #if KNI || FNA private Dictionary _loadedAssets; /// /// Gets the dictionary of loaded assets keyed by asset name. /// public Dictionary LoadedAssets { get { if(_loadedAssets is null) { // KNI please make this public so I don't have to use reflection FieldInfo field = typeof(ContentManager).GetField(nameof(_loadedAssets), BindingFlags.NonPublic | BindingFlags.Instance); if (field is null) { throw new InvalidOperationException("Unable to get source loaded assets field"); } _loadedAssets = field.GetValue(this) as Dictionary; } return _loadedAssets; } } #endif /// /// Initializes a new instance of the class. /// /// /// By default, the content manager searches for content in the directory where the executable is located. /// /// The service provided used for resolving services. /// /// Thrown when is . /// public ExtendedContentManager(IServiceProvider serviceProvider) : base(serviceProvider) { _graphicsDeviceService = serviceProvider.GetService(typeof(IGraphicsDeviceService)) as IGraphicsDeviceService; } /// /// Initializes a new instance of the class with a specified root directory. /// /// The service provider to use for resolving services. /// The root directory the content manager will search for content in. public ExtendedContentManager(IServiceProvider serviceProvider, string rootDirectory) : base(serviceProvider, rootDirectory) { _graphicsDeviceService = serviceProvider.GetService(typeof(IGraphicsDeviceService)) as IGraphicsDeviceService; } #if KNI || FNA /// /// Loads a asset. /// /// /// If the parameter is a relative path, it must be relative to the /// path. /// /// The path to the asset to load /// /// Specifies whether the color data of the texture should be premultiplied by its alpha value. /// /// The loaded. public Texture2D LoadTexture2D(string path) { if (TryGetCachedAsset(path, out Texture2D texture)) { return texture; } if (NoExtension(path)) { return Load(path); } using Stream stream = GetStream(path); texture = Texture2D.FromStream(_graphicsDeviceService.GraphicsDevice, stream); texture.Name = path; CacheAsset(path, texture); return texture; } #else /// /// Loads a asset. /// /// /// If the parameter is a relative path, it must be relative to the /// path. /// /// The path to the asset to load /// The loaded. public Texture2D LoadTexture2D(string path) => LoadTexture2D(path, true); /// /// Loads a asset. /// /// /// If the parameter is a relative path, it must be relative to the /// path. /// /// The path to the asset to load /// /// Specifies whether the color data of the texture should be premultiplied by its alpha value. /// /// The loaded. public Texture2D LoadTexture2D(string path, bool premultiplyAlpha) { if (TryGetCachedAsset(path, out Texture2D texture)) { return texture; } if (NoExtension(path)) { return Load(path); } using Stream stream = GetStream(path); texture = premultiplyAlpha ? Texture2D.FromStream(_graphicsDeviceService.GraphicsDevice, stream) : Texture2D.FromStream(_graphicsDeviceService.GraphicsDevice, stream, DefaultColorProcessors.PremultiplyAlpha); texture.Name = path; CacheAsset(path, texture); return texture; } #endif /// /// Loads a asset. /// /// /// If the parameter is a relative path, it must be relative to the /// path. /// /// The path to the asset to load /// The loaded. public SoundEffect LoadSoundEffect(string path) { if (TryGetCachedAsset(path, out SoundEffect soundEffect)) { return soundEffect; } if (NoExtension(path)) { return Load(path); } using Stream stream = GetStream(path); soundEffect = SoundEffect.FromStream(stream); soundEffect.Name = path; CacheAsset(path, soundEffect); return soundEffect; } /// /// Loads a asset. /// /// /// If the parameter is a relative path, it must be relative to the /// path. /// /// The path to the asset to load. /// The loaded. public BitmapFont LoadBitmapFont(string path) { if (TryGetCachedAsset(path, out BitmapFont font)) { return font; } if (NoExtension(path)) { return Load(path); } using FileStream stream = GetStream(path); var bmfFile = BitmapFontFileReader.Read(stream); var textures = bmfFile.Pages.Select(page => LoadTexture2D(Path.GetRelativePath(path, page))) .ToArray(); var characters = new Dictionary(); foreach (var charBlock in bmfFile.Characters) { var texture = textures[charBlock.Page]; var region = new Texture2DRegion(texture, charBlock.X, charBlock.Y, charBlock.Width, charBlock.Height); var character = new BitmapFontCharacter((int)charBlock.ID, region, charBlock.XOffset, charBlock.YOffset, charBlock.XAdvance); characters.Add(character.Character, character); } foreach (var kerningBlock in bmfFile.Kernings) { if (characters.TryGetValue((int)kerningBlock.First, out var character)) { character.Kernings.Add((int)kerningBlock.Second, kerningBlock.Amount); } } var bmfFont = new BitmapFont(bmfFile.FontName, bmfFile.Info.FontSize, bmfFile.Common.LineHeight, characters.Values); CacheAsset(path, font); return font; } #if KNI || FNA /// /// Loads a from a TexturePacker JSON file. /// /// The path to the TexturePacker JSON file /// The created from the TexturePacker JSON file content. public Texture2DAtlas LoadTexturePacker(string path) #else /// /// Loads a from a TexturePacker JSON file. /// /// The path to the TexturePacker JSON file /// /// Specifies whether the color data of the texture should be premultiplied by its alpha value. /// /// The created from the TexturePacker JSON file content. public Texture2DAtlas LoadTexturePacker(string path, bool premultiplyAlpha) #endif { if (TryGetCachedAsset(path, out var atlas)) { return atlas; } if (NoExtension(path)) { return Load(path); } using var stream = GetStream(path); var tpFile = TexturePackerFileReader.Read(stream); var dir = Path.GetDirectoryName(path); var imageAssetPath = Path.Combine(dir, tpFile.Meta.Image); #if KNI || FNA var texture = LoadTexture2D(imageAssetPath); #else var texture = LoadTexture2D(imageAssetPath, premultiplyAlpha); #endif atlas = new Texture2DAtlas(Path.GetFileNameWithoutExtension(tpFile.Meta.Image), texture); foreach (var region in tpFile.Regions) { var frame = region.Frame; atlas.CreateRegion(frame.X, frame.Y, frame.Width, frame.Height, Path.GetFileNameWithoutExtension(region.FileName)); } CacheAsset(path, atlas); return atlas; } /// /// Opens a file stream for the specified asset path. /// /// /// The argument can be either an absolute or relative path. When it is a relative path, it /// is resolved using . /// /// The path to the asset file. /// A for reading the asset file. protected FileStream GetStream(string path) { if (Path.IsPathRooted(path)) { return File.OpenRead(path); } return (FileStream)TitleContainer.OpenStream(path); } /// /// Caches a loaded asset in the loaded assets dictionary/ /// /// /// Assets that implement are registered with the /// collection for automatic disposal. /// /// The cache key for the asset, typically the asset path. /// The asset instance to cache. protected void CacheAsset(string name, object obj) { LoadedAssets.Add(name, obj); if (obj is IDisposable disposable) { DisposeableAssets.Add(disposable); } } /// /// Determines whether the specified asset name has no file extension. /// /// The asset name to check. /// /// if the name has no extension or the extension is empty; otherwise, . /// protected bool NoExtension(string name) => string.IsNullOrEmpty(Path.GetExtension(name)); /// /// Attempts to retrieve a previously cached asset of the specified type. /// /// The expected type of the cached asset. /// The cache key for the asset, typically the asset path. /// /// When this method returns, contains the cached asset if found and of type ; otherwise, /// the default value for . /// /// /// if a cached asset of type was found; otherwise, . /// protected bool TryGetCachedAsset(string name, out T asset) { asset = default; if (LoadedAssets.TryGetValue(name, out object value)) { if (value is T) { asset = (T)value; return true; } } return false; } }