506 lines
17 KiB
C#
506 lines
17 KiB
C#
// MonoGame - Copyright (C) The MonoGame Team
|
|
// This file is subject to the terms and conditions defined in
|
|
// file 'LICENSE.txt', which is part of this source code package.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Reflection;
|
|
using MonoGame.Utilities;
|
|
using Microsoft.Xna.Framework.Graphics;
|
|
using System.Globalization;
|
|
|
|
namespace Microsoft.Xna.Framework.Content
|
|
{
|
|
public partial class ContentManager : IDisposable
|
|
{
|
|
const byte ContentCompressedLzx = 0x80;
|
|
const byte ContentCompressedLz4 = 0x40;
|
|
|
|
private string _rootDirectory = string.Empty;
|
|
private IServiceProvider serviceProvider;
|
|
private IGraphicsDeviceService graphicsDeviceService;
|
|
private Dictionary<string, object> loadedAssets = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
|
private List<IDisposable> disposableAssets = new List<IDisposable>();
|
|
private bool disposed;
|
|
private byte[] scratchBuffer;
|
|
|
|
private static object ContentManagerLock = new object();
|
|
private static List<WeakReference> ContentManagers = new List<WeakReference>();
|
|
|
|
private static readonly List<char> targetPlatformIdentifiers = new List<char>()
|
|
{
|
|
'w', // Windows (XNA & DirectX)
|
|
'x', // Xbox360 (XNA)
|
|
'm', // WindowsPhone7.0 (XNA)
|
|
'i', // iOS
|
|
'a', // Android
|
|
'd', // DesktopGL
|
|
'X', // MacOSX
|
|
'W', // WindowsStoreApp
|
|
'n', // NativeClient
|
|
'M', // WindowsPhone8
|
|
'r', // RaspberryPi
|
|
'P', // PlayStation4
|
|
'v', // PSVita
|
|
'O', // XboxOne
|
|
'S', // Nintendo Switch
|
|
|
|
// NOTE: There are additional idenfiers for consoles that
|
|
// are not defined in this repository. Be sure to ask the
|
|
// console port maintainers to ensure no collisions occur.
|
|
|
|
|
|
// Legacy identifiers... these could be reused in the
|
|
// future if we feel enough time has passed.
|
|
|
|
'p', // PlayStationMobile
|
|
'g', // Windows (OpenGL)
|
|
'l', // Linux
|
|
};
|
|
|
|
|
|
static partial void PlatformStaticInit();
|
|
|
|
static ContentManager()
|
|
{
|
|
// Allow any per-platform static initialization to occur.
|
|
PlatformStaticInit();
|
|
}
|
|
|
|
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)
|
|
{
|
|
var 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)
|
|
{
|
|
var contentRef = ContentManagers[i];
|
|
if (!contentRef.IsAlive || ReferenceEquals(contentRef.Target, contentManager))
|
|
ContentManagers.RemoveAt(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal static void ReloadGraphicsContent()
|
|
{
|
|
lock (ContentManagerLock)
|
|
{
|
|
// Reload the graphic assets of each content manager. Also take the
|
|
// opportunity to prune the list of any finalized content managers.
|
|
for (int i = ContentManagers.Count - 1; i >= 0; --i)
|
|
{
|
|
var contentRef = ContentManagers[i];
|
|
if (contentRef.IsAlive)
|
|
{
|
|
var contentManager = (ContentManager)contentRef.Target;
|
|
if (contentManager != null)
|
|
contentManager.ReloadGraphicsAssets();
|
|
}
|
|
else
|
|
{
|
|
ContentManagers.RemoveAt(i);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
public ContentManager(IServiceProvider serviceProvider)
|
|
{
|
|
if (serviceProvider == null)
|
|
{
|
|
throw new ArgumentNullException("serviceProvider");
|
|
}
|
|
this.serviceProvider = serviceProvider;
|
|
AddContentManager(this);
|
|
}
|
|
|
|
public ContentManager(IServiceProvider serviceProvider, string rootDirectory)
|
|
{
|
|
if (serviceProvider == null)
|
|
{
|
|
throw new ArgumentNullException("serviceProvider");
|
|
}
|
|
if (rootDirectory == null)
|
|
{
|
|
throw new ArgumentNullException("rootDirectory");
|
|
}
|
|
this.RootDirectory = rootDirectory;
|
|
this.serviceProvider = serviceProvider;
|
|
AddContentManager(this);
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
scratchBuffer = null;
|
|
disposed = true;
|
|
}
|
|
}
|
|
|
|
public virtual T LoadLocalized<T> (string assetName)
|
|
{
|
|
string [] cultureNames =
|
|
{
|
|
CultureInfo.CurrentCulture.Name, // eg. "en-US"
|
|
CultureInfo.CurrentCulture.TwoLetterISOLanguageName // eg. "en"
|
|
};
|
|
|
|
// Look first for a specialized language-country version of the asset,
|
|
// then if that fails, loop back around to see if we can find one that
|
|
// specifies just the language without the country part.
|
|
foreach (string cultureName in cultureNames) {
|
|
string localizedAssetName = assetName + '.' + cultureName;
|
|
|
|
try {
|
|
return Load<T> (localizedAssetName);
|
|
} catch (ContentLoadException) { }
|
|
}
|
|
|
|
// If we didn't find any localized asset, fall back to the default name.
|
|
return Load<T> (assetName);
|
|
}
|
|
|
|
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 /-seperating 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
|
|
var 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;
|
|
}
|
|
|
|
protected virtual Stream OpenStream(string assetName)
|
|
{
|
|
Stream stream;
|
|
try
|
|
{
|
|
var assetPath = Path.Combine(RootDirectory, assetName) + ".xnb";
|
|
|
|
// This is primarily for editor support.
|
|
// Setting the RootDirectory to an absolute path is useful in editor
|
|
// situations, but TitleContainer can ONLY be passed relative paths.
|
|
#if DESKTOPGL || WINDOWS
|
|
if (Path.IsPathRooted(assetPath))
|
|
stream = File.OpenRead(assetPath);
|
|
else
|
|
#endif
|
|
stream = TitleContainer.OpenStream(assetPath);
|
|
#if ANDROID
|
|
// Read the asset into memory in one go. This results in a ~50% reduction
|
|
// in load times on Android due to slow Android asset streams.
|
|
MemoryStream memStream = new MemoryStream();
|
|
stream.CopyTo(memStream);
|
|
memStream.Seek(0, SeekOrigin.Begin);
|
|
stream.Close();
|
|
stream = memStream;
|
|
#endif
|
|
}
|
|
catch (FileNotFoundException fileNotFound)
|
|
{
|
|
throw new ContentLoadException("The content file was not found.", fileNotFound);
|
|
}
|
|
#if !WINDOWS_UAP
|
|
catch (DirectoryNotFoundException directoryNotFound)
|
|
{
|
|
throw new ContentLoadException("The directory was not found.", directoryNotFound);
|
|
}
|
|
#endif
|
|
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");
|
|
}
|
|
|
|
string originalAssetName = assetName;
|
|
object result = null;
|
|
|
|
if (this.graphicsDeviceService == null)
|
|
{
|
|
this.graphicsDeviceService = serviceProvider.GetService(typeof(IGraphicsDeviceService)) as IGraphicsDeviceService;
|
|
if (this.graphicsDeviceService == null)
|
|
{
|
|
throw new InvalidOperationException("No Graphics Device Service");
|
|
}
|
|
}
|
|
|
|
// Try to load as XNB file
|
|
var stream = OpenStream(assetName);
|
|
using (var xnbReader = new BinaryReader(stream))
|
|
{
|
|
using (var reader = GetContentReaderFromXnb(assetName, stream, xnbReader, recordDisposableObject))
|
|
{
|
|
result = reader.ReadAsset<T>();
|
|
if (result is GraphicsResource)
|
|
((GraphicsResource)result).Name = originalAssetName;
|
|
}
|
|
}
|
|
|
|
if (result == null)
|
|
throw new ContentLoadException("Could not load " + originalAssetName + " asset!");
|
|
|
|
return (T)result;
|
|
}
|
|
|
|
private ContentReader GetContentReaderFromXnb(string originalAssetName, Stream stream, BinaryReader xnbReader, Action<IDisposable> recordDisposableObject)
|
|
{
|
|
// The first 4 bytes should be the "XNB" header. i use that to detect an invalid file
|
|
byte x = xnbReader.ReadByte();
|
|
byte n = xnbReader.ReadByte();
|
|
byte b = xnbReader.ReadByte();
|
|
byte platform = xnbReader.ReadByte();
|
|
|
|
if (x != 'X' || n != 'N' || b != 'B' ||
|
|
!(targetPlatformIdentifiers.Contains((char)platform)))
|
|
{
|
|
throw new ContentLoadException("Asset does not appear to be a valid XNB file. Did you process your content for Windows?");
|
|
}
|
|
|
|
byte version = xnbReader.ReadByte();
|
|
byte flags = xnbReader.ReadByte();
|
|
|
|
bool compressedLzx = (flags & ContentCompressedLzx) != 0;
|
|
bool compressedLz4 = (flags & ContentCompressedLz4) != 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();
|
|
|
|
Stream decompressedStream = null;
|
|
if (compressedLzx || compressedLz4)
|
|
{
|
|
// Decompress the xnb
|
|
int decompressedSize = xnbReader.ReadInt32();
|
|
|
|
if (compressedLzx)
|
|
{
|
|
int compressedSize = xnbLength - 14;
|
|
decompressedStream = new LzxDecoderStream(stream, decompressedSize, compressedSize);
|
|
}
|
|
else if (compressedLz4)
|
|
{
|
|
decompressedStream = new Lz4DecoderStream(stream);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
decompressedStream = stream;
|
|
}
|
|
|
|
var reader = new ContentReader(this, decompressedStream, this.graphicsDeviceService.GraphicsDevice,
|
|
originalAssetName, version, recordDisposableObject);
|
|
|
|
return reader;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Virtual property to allow a derived ContentManager to have it's assets reloaded
|
|
/// </summary>
|
|
protected virtual Dictionary<string, object> LoadedAssets
|
|
{
|
|
get { return loadedAssets; }
|
|
}
|
|
|
|
protected virtual void ReloadGraphicsAssets()
|
|
{
|
|
foreach (var asset in LoadedAssets)
|
|
{
|
|
// This never executes as asset.Key is never null. This just forces the
|
|
// linker to include the ReloadAsset function when AOT compiled.
|
|
if (asset.Key == null)
|
|
ReloadAsset(asset.Key, Convert.ChangeType(asset.Value, asset.Value.GetType()));
|
|
|
|
var methodInfo = ReflectionHelpers.GetMethodInfo(typeof(ContentManager), "ReloadAsset");
|
|
var genericMethod = methodInfo.MakeGenericMethod(asset.Value.GetType());
|
|
genericMethod.Invoke(this, new object[] { asset.Key, Convert.ChangeType(asset.Value, asset.Value.GetType()) });
|
|
}
|
|
}
|
|
|
|
protected virtual void ReloadAsset<T>(string originalAssetName, T currentAsset)
|
|
{
|
|
string assetName = originalAssetName;
|
|
if (string.IsNullOrEmpty(assetName))
|
|
{
|
|
throw new ArgumentNullException("assetName");
|
|
}
|
|
if (disposed)
|
|
{
|
|
throw new ObjectDisposedException("ContentManager");
|
|
}
|
|
|
|
if (this.graphicsDeviceService == null)
|
|
{
|
|
this.graphicsDeviceService = serviceProvider.GetService(typeof(IGraphicsDeviceService)) as IGraphicsDeviceService;
|
|
if (this.graphicsDeviceService == null)
|
|
{
|
|
throw new InvalidOperationException("No Graphics Device Service");
|
|
}
|
|
}
|
|
|
|
var stream = OpenStream(assetName);
|
|
using (var xnbReader = new BinaryReader(stream))
|
|
{
|
|
using (var reader = GetContentReaderFromXnb(assetName, stream, xnbReader, null))
|
|
{
|
|
reader.ReadAsset<T>(currentAsset);
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual void Unload()
|
|
{
|
|
// Look for disposable assets.
|
|
foreach (var disposable in disposableAssets)
|
|
{
|
|
if (disposable != null)
|
|
disposable.Dispose();
|
|
}
|
|
disposableAssets.Clear();
|
|
loadedAssets.Clear();
|
|
}
|
|
|
|
public string RootDirectory
|
|
{
|
|
get
|
|
{
|
|
return _rootDirectory;
|
|
}
|
|
set
|
|
{
|
|
_rootDirectory = value;
|
|
}
|
|
}
|
|
|
|
internal string RootDirectoryFullPath
|
|
{
|
|
get
|
|
{
|
|
return Path.Combine(TitleContainer.Location, RootDirectory);
|
|
}
|
|
}
|
|
|
|
public IServiceProvider ServiceProvider
|
|
{
|
|
get
|
|
{
|
|
return this.serviceProvider;
|
|
}
|
|
}
|
|
|
|
internal byte[] GetScratchBuffer(int size)
|
|
{
|
|
size = Math.Max(size, 1024 * 1024);
|
|
if (scratchBuffer == null || scratchBuffer.Length < size)
|
|
scratchBuffer = new byte[size];
|
|
return scratchBuffer;
|
|
}
|
|
}
|
|
}
|