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