Show More
Commit Description:
Various UI improvements.
Commit Description:
Various UI improvements.
References:
File last commit:
Show/Diff file:
Action:
FNA/src/Content/ContentManager.cs
639 lines | 16.2 KiB | text/x-csharp | CSharpLexer
639 lines | 16.2 KiB | text/x-csharp | CSharpLexer
r0 | #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 | ||||
} | ||||
} | ||||