Show More
Commit Description:
Various UI improvements.
Commit Description:
Various UI improvements.
File last commit:
Show/Diff file:
Action:
FNA/src/Content/ContentManager.cs
639 lines | 16.2 KiB | text/x-csharp | CSharpLexer
#region License
/* FNA - XNA4 Reimplementation for Desktop Platforms
* Copyright 2009-2020 Ethan Lee and the MonoGame Team
*
* Released under the Microsoft Public License.
* See LICENSE for details.
*/
#endregion
#region Using Statements
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Media;
#endregion
namespace Microsoft.Xna.Framework.Content
{
public partial class ContentManager : IDisposable
{
#region Public ServiceProvider Property
public IServiceProvider ServiceProvider
{
get;
private set;
}
#endregion
#region Public RootDirectory Property
public string RootDirectory
{
get;
set;
}
#endregion
#region Internal Root Directory Path Property
internal string RootDirectoryFullPath
{
get
{
if (Path.IsPathRooted(RootDirectory))
{
return RootDirectory;
}
return Path.Combine(TitleLocation.Path, RootDirectory);
}
}
#endregion
#region Private Variables
private GraphicsDevice graphicsDevice;
private Dictionary<string, object> loadedAssets = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
private List<IDisposable> disposableAssets = new List<IDisposable>();
private bool disposed;
#endregion
#region Private Static Variables
private static object ContentManagerLock = new object();
private static List<WeakReference> ContentManagers = new List<WeakReference>();
private static readonly byte[] xnbHeader = new byte[4];
private static List<char> targetPlatformIdentifiers = new List<char>()
{
'w', // Windows (DirectX)
'x', // Xbox360
'm', // WindowsPhone
'i', // iOS
'a', // Android
'd', // DesktopGL
'X', // MacOSX
'W', // WindowsStoreApp
'n', // NativeClient
'u', // Ouya
'p', // PlayStationMobile
'M', // WindowsPhone8
'r', // RaspberryPi
'P', // Playstation 4
'g', // WindowsGL (deprecated for DesktopGL)
'l', // Linux (deprecated for DesktopGL)
};
#endregion
#region Public Constructors
public ContentManager(IServiceProvider serviceProvider)
{
if (serviceProvider == null)
{
throw new ArgumentNullException("serviceProvider");
}
ServiceProvider = serviceProvider;
RootDirectory = string.Empty;
AddContentManager(this);
}
public ContentManager(IServiceProvider serviceProvider, string rootDirectory)
{
if (serviceProvider == null)
{
throw new ArgumentNullException("serviceProvider");
}
if (rootDirectory == null)
{
throw new ArgumentNullException("rootDirectory");
}
ServiceProvider = serviceProvider;
RootDirectory = rootDirectory;
AddContentManager(this);
}
#endregion
#region Destructor
/* Use C# destructor syntax for finalization code.
* This destructor will run only if the Dispose method
* does not get called.
* It gives your base class the opportunity to finalize.
* Do not provide destructors in types derived from this class.
*/
~ContentManager()
{
/* Do not re-create Dispose clean-up code here.
* Calling Dispose(false) is optimal in terms of
* readability and maintainability.
*/
Dispose(false);
}
#endregion
#region Dispose Methods
public void Dispose()
{
Dispose(true);
/* Tell the garbage collector not to call the finalizer
* since all the cleanup will already be done.
*/
GC.SuppressFinalize(this);
// Once disposed, content manager wont be used again
RemoveContentManager(this);
}
/* If disposing is true, it was called explicitly and we should dispose managed
* objects. If disposing is false, it was called by the finalizer and managed
* objects should not be disposed.
*/
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
Unload();
}
disposed = true;
}
}
#endregion
#region Public Methods
public virtual T Load<T>(string assetName)
{
if (string.IsNullOrEmpty(assetName))
{
throw new ArgumentNullException("assetName");
}
if (disposed)
{
throw new ObjectDisposedException("ContentManager");
}
T result = default(T);
/* On some platforms, name and slash direction matter.
* We store the asset by a /-separating key rather than
* how the path to the file was passed to us to avoid
* loading "content/asset1.xnb" and "content\\ASSET1.xnb"
* as if they were two different files. this matches
* stock XNA behavior. The Dictionary will ignore case
* differences.
*/
string key = assetName.Replace('\\', '/');
// Check for a previously loaded asset first
object asset = null;
if (loadedAssets.TryGetValue(key, out asset))
{
if (asset is T)
{
return (T) asset;
}
}
// Load the asset.
result = ReadAsset<T>(assetName, null);
loadedAssets[key] = result;
return result;
}
public virtual void Unload()
{
// Look for disposable assets.
foreach (IDisposable disposable in disposableAssets)
{
if (disposable != null)
{
disposable.Dispose();
}
}
disposableAssets.Clear();
loadedAssets.Clear();
}
#endregion
#region Protected Methods
protected virtual Stream OpenStream(string assetName)
{
Stream stream;
try
{
stream = TitleContainer.OpenStream(
Path.Combine(RootDirectory, assetName) + ".xnb"
);
}
catch (FileNotFoundException fileNotFound)
{
throw new ContentLoadException("The content file was not found.", fileNotFound);
}
catch (DirectoryNotFoundException directoryNotFound)
{
throw new ContentLoadException("The directory was not found.", directoryNotFound);
}
catch (Exception exception)
{
throw new ContentLoadException("Opening stream error.", exception);
}
return stream;
}
protected T ReadAsset<T>(string assetName, Action<IDisposable> recordDisposableObject)
{
if (string.IsNullOrEmpty(assetName))
{
throw new ArgumentNullException("assetName");
}
if (disposed)
{
throw new ObjectDisposedException("ContentManager");
}
object result = null;
Stream stream = null;
string modifiedAssetName = String.Empty; // Will be used if we have to guess a filename
try
{
stream = OpenStream(assetName);
}
catch (Exception e)
{
// Okay, so we couldn't open it. Maybe it needs a different extension?
// FIXME: This only works for files on the disk, what about custom streams? -flibit
modifiedAssetName = MonoGame.Utilities.FileHelpers.NormalizeFilePathSeparators(
Path.Combine(RootDirectoryFullPath, assetName)
);
if (typeof(T) == typeof(Texture2D) || typeof(T) == typeof(Texture))
{
modifiedAssetName = Texture2DReader.Normalize(modifiedAssetName);
}
else if ((typeof(T) == typeof(SoundEffect)))
{
modifiedAssetName = SoundEffectReader.Normalize(modifiedAssetName);
}
else if ((typeof(T) == typeof(Effect)))
{
modifiedAssetName = EffectReader.Normalize(modifiedAssetName);
}
else if ((typeof(T) == typeof(Song)))
{
modifiedAssetName = SongReader.Normalize(modifiedAssetName);
}
else if ((typeof(T) == typeof(Video)))
{
modifiedAssetName = VideoReader.Normalize(modifiedAssetName);
}
else
{
// No raw format available, disregard!
modifiedAssetName = null;
}
// Did we get anything...?
if (String.IsNullOrEmpty(modifiedAssetName))
{
// Nope, nothing we're aware of!
throw new ContentLoadException(
"Could not load asset " + assetName + "! Error: " + e.Message,
e
);
}
stream = TitleContainer.OpenStream(modifiedAssetName);
}
// Check for XNB header
stream.Read(xnbHeader, 0, xnbHeader.Length);
if ( xnbHeader[0] == 'X' &&
xnbHeader[1] == 'N' &&
xnbHeader[2] == 'B' &&
targetPlatformIdentifiers.Contains((char) xnbHeader[3]) )
{
using (BinaryReader xnbReader = new BinaryReader(stream))
using (ContentReader reader = GetContentReaderFromXnb(assetName, ref stream, xnbReader, (char) xnbHeader[3], recordDisposableObject))
{
result = reader.ReadAsset<T>();
GraphicsResource resource = result as GraphicsResource;
if (resource != null)
{
resource.Name = assetName;
}
}
}
else
{
// It's not an XNB file. Try to load as a raw asset instead.
// FIXME: Assuming seekable streams! -flibit
stream.Seek(0, SeekOrigin.Begin);
if (typeof(T) == typeof(Texture2D) || typeof(T) == typeof(Texture))
{
Texture2D texture;
if ( xnbHeader[0] == 'D' &&
xnbHeader[1] == 'D' &&
xnbHeader[2] == 'S' &&
xnbHeader[3] == ' ' )
{
texture = Texture2D.DDSFromStreamEXT(
GetGraphicsDevice(),
stream
);
}
else
{
texture = Texture2D.FromStream(
GetGraphicsDevice(),
stream
);
}
texture.Name = assetName;
result = texture;
}
else if ((typeof(T) == typeof(SoundEffect)))
{
result = SoundEffect.FromStream(stream);
}
else if ((typeof(T) == typeof(Effect)))
{
byte[] data = new byte[stream.Length];
stream.Read(data, 0, (int) stream.Length);
result = new Effect(GetGraphicsDevice(), data);
}
else if ((typeof(T) == typeof(Song)))
{
// FIXME: Not using the stream! -flibit
result = new Song(modifiedAssetName);
}
else if ((typeof(T) == typeof(Video)))
{
// FIXME: Not using the stream! -flibit
result = new Video(modifiedAssetName, GetGraphicsDevice());
FNALoggerEXT.LogWarn(
"Video " +
modifiedAssetName +
" does not have an XNB file! Hacking Duration property!"
);
}
else
{
stream.Close();
throw new ContentLoadException("Could not load " + assetName + " asset!");
}
/* Because Raw Assets skip the ContentReader step, they need to have their
* disposables recorded here. Doing it outside of this catch will
* result in disposables being logged twice.
*/
IDisposable disposableResult = result as IDisposable;
if (disposableResult != null)
{
if (recordDisposableObject != null)
{
recordDisposableObject(disposableResult);
}
else
{
disposableAssets.Add(disposableResult);
}
}
/* Because we're not using a BinaryReader for raw assets, we
* need to close the stream ourselves.
* -flibit
*/
stream.Close();
}
return (T) result;
}
#endregion
#region Internal Methods
internal void RecordDisposable(IDisposable disposable)
{
Debug.Assert(disposable != null, "The disposable is null!");
/* Avoid recording disposable objects twice. ReloadAsset will try to record
* the disposables again. We don't know which asset recorded which
* disposable so just guard against storing multiple of the same instance.
*/
if (!disposableAssets.Contains(disposable))
{
disposableAssets.Add(disposable);
}
}
internal GraphicsDevice GetGraphicsDevice()
{
if (graphicsDevice == null)
{
IGraphicsDeviceService result = ServiceProvider.GetService(
typeof(IGraphicsDeviceService)
) as IGraphicsDeviceService;
if (result == null)
{
throw new ContentLoadException("No Graphics Device Service");
}
graphicsDevice = result.GraphicsDevice;
}
return graphicsDevice;
}
#endregion
#region Private Methods
private ContentReader GetContentReaderFromXnb(string originalAssetName, ref Stream stream, BinaryReader xnbReader, char platform, Action<IDisposable> recordDisposableObject)
{
byte version = xnbReader.ReadByte();
byte flags = xnbReader.ReadByte();
bool compressed = (flags & 0x80) != 0;
if (version != 5 && version != 4)
{
throw new ContentLoadException("Invalid XNB version");
}
// The next int32 is the length of the XNB file
int xnbLength = xnbReader.ReadInt32();
ContentReader reader;
if (compressed)
{
/* Decompress the XNB
* Thanks to ShinAli (https://bitbucket.org/alisci01/xnbdecompressor)
*/
int compressedSize = xnbLength - 14;
int decompressedSize = xnbReader.ReadInt32();
// This will replace the XNB stream at the end
MemoryStream decompressedStream = new MemoryStream(
new byte[decompressedSize],
0,
decompressedSize,
true,
true // This MUST be true! Readers may need GetBuffer()!
);
/* Read in the whole XNB file at once, into a temp buffer.
* For slow disks, the extra malloc is more than worth the
* performance improvement from not constantly fread()ing!
*/
MemoryStream compressedStream = new MemoryStream(
new byte[compressedSize],
0,
compressedSize,
true,
true
);
stream.Read(compressedStream.GetBuffer(), 0, compressedSize);
// Default window size for XNB encoded files is 64Kb (need 16 bits to represent it)
LzxDecoder dec = new LzxDecoder(16);
int decodedBytes = 0;
long pos = 0;
while (pos < compressedSize)
{
/* The compressed stream is separated into blocks that will
* decompress into 32kB or some other size if specified.
* Normal, 32kB output blocks will have a short indicating
* the size of the block before the block starts. Blocks
* that have a defined output will be preceded by a byte of
* value 0xFF (255), then a short indicating the output size
* and another for the block size. All shorts for these
* cases are encoded in big endian order.
*/
int hi = compressedStream.ReadByte();
int lo = compressedStream.ReadByte();
int block_size = (hi << 8) | lo;
int frame_size = 0x8000; // Frame size is 32kB by default
// Does this block define a frame size?
if (hi == 0xFF)
{
hi = lo;
lo = (byte) compressedStream.ReadByte();
frame_size = (hi << 8) | lo;
hi = (byte) compressedStream.ReadByte();
lo = (byte) compressedStream.ReadByte();
block_size = (hi << 8) | lo;
pos += 5;
}
else
{
pos += 2;
}
// Either says there is nothing to decode
if (block_size == 0 || frame_size == 0)
{
break;
}
dec.Decompress(compressedStream, block_size, decompressedStream, frame_size);
pos += block_size;
decodedBytes += frame_size;
/* Reset the position of the input just in case the bit
* buffer read in some unused bytes.
*/
compressedStream.Seek(pos, SeekOrigin.Begin);
}
if (decompressedStream.Position != decompressedSize)
{
throw new ContentLoadException(
"Decompression of " + originalAssetName + " failed. "
);
}
decompressedStream.Seek(0, SeekOrigin.Begin);
reader = new ContentReader(
this,
decompressedStream,
originalAssetName,
version,
platform,
recordDisposableObject
);
}
else
{
reader = new ContentReader(
this,
stream,
originalAssetName,
version,
platform,
recordDisposableObject
);
}
return reader;
}
#endregion
#region Private Static Methods
private static void AddContentManager(ContentManager contentManager)
{
lock (ContentManagerLock)
{
/* Check if the list contains this content manager already. Also take
* the opportunity to prune the list of any finalized content managers.
*/
bool contains = false;
for (int i = ContentManagers.Count - 1; i >= 0; i -= 1)
{
WeakReference contentRef = ContentManagers[i];
if (ReferenceEquals(contentRef.Target, contentManager))
{
contains = true;
}
if (!contentRef.IsAlive)
{
ContentManagers.RemoveAt(i);
}
}
if (!contains)
{
ContentManagers.Add(new WeakReference(contentManager));
}
}
}
private static void RemoveContentManager(ContentManager contentManager)
{
lock (ContentManagerLock)
{
/* Check if the list contains this content manager and remove it. Also
* take the opportunity to prune the list of any finalized content managers.
*/
for (int i = ContentManagers.Count - 1; i >= 0; i -= 1)
{
WeakReference contentRef = ContentManagers[i];
if (!contentRef.IsAlive || ReferenceEquals(contentRef.Target, contentManager))
{
ContentManagers.RemoveAt(i);
}
}
}
}
#endregion
}
}