#region CPL License /* Nuclex Framework Copyright (C) 2002-2011 Nuclex Development Labs This library is free software; you can redistribute it and/or modify it under the terms of the IBM Common Public License as published by the IBM Corporation; either version 1.0 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the IBM Common Public License for more details. You should have received a copy of the IBM Common Public License along with this library */ #endregion using System; using System.Collections.Generic; using System.IO; using SevenZip.Compression.LZMA; using Microsoft.Xna.Framework.Content; namespace Nuclex.Game.Content { /// Content manager that can read LZMA-compressed game assets public class LzmaContentManager : ContentManager { #region struct CompressedFileInfo /// Stores the informations of a compressed file private struct CompressedFileInfo { /// Where in the LZMA package the file is stored public long Offset; /// Length of the compressed data public int CompressedLength; /// Length the data will have when it is uncompressed public int UncompressedLength; } #endregion // struct CompressedFileInfo /// Initializes a new LZMA-decompressing content manager /// /// Service provider to use for accessing additional services that the /// individual content readers require for creating their resources. /// public LzmaContentManager(IServiceProvider serviceProvider) : this(serviceProvider, true) { } /// Initializes a new LZMA-decompressing content manager /// /// Service provider to use for accessing additional services that the /// individual content readers require for creating their resources. /// /// /// Is the user allowed to replace individual files by placing the .xnb files /// in the game's folders as Visual Studio would normally do? /// /// /// It is recommended to leave the allowReplacement argument set to true at least /// during development. Otherwise you will be forced to rerun the content compression /// utility each time you modify one of your assets. Forgetting to run the utility /// might further lead to strange problems because the game would then be working /// with an older version of the assets than you think it does. /// public LzmaContentManager(IServiceProvider serviceProvider, bool allowReplacement) : base(serviceProvider) { this.replacementAllowed = allowReplacement; } /// Initializes a new LZMA-decompressing content manager /// /// Path of the package to read content asset files from. This package needs to /// be created using the Nuclex content compression utility. /// /// /// Service provider to use for accessing additional services that the /// individual content readers require for creating their resources. /// public LzmaContentManager(IServiceProvider serviceProvider, string packagePath) : this(serviceProvider, packagePath, true) { } /// Initializes a new LZMA-decompressing content manager /// /// Service provider to use for accessing additional services that the /// individual content readers require for creating their resources. /// /// /// Path of the package to read content asset files from. This package needs to /// be created using the Nuclex content compression utility. /// /// /// Is the user allowed to replace individual files by placing the .xnb files /// in the game's folders as Visual Studio would normally do? /// /// /// It is recommended to leave the allowReplacement argument set to true at least /// during development. Otherwise you will be forced to rerun the Nuclex content /// compression utility each time you modify one of your assets. Forgetting to run /// the utility might further lead to strange problems because the game would then /// be working with an older version of the assets than you think it does. /// public LzmaContentManager( IServiceProvider serviceProvider, string packagePath, bool allowReplacement ) : this(serviceProvider, allowReplacement) { openPackage(packagePath); } /// Immediately releases all resources used the content manager /// Whether the call was initiated by user code /// /// If the call wasn't initiated by user code, the call comes from the .NET /// garbage collector, meaning the content manager must not access any other /// classes for risk of them having been reclaimed already. /// protected override void Dispose(bool calledByUser) { if(calledByUser) { if(this.lzmaPackageStream != null) { this.lzmaPackageStream.Dispose(); this.lzmaPackageStream = null; } } base.Dispose(calledByUser); } /// Opens a stream to the named asset /// Asset to open a stream for /// The opened stream protected override Stream OpenStream(string assetName) { // If overriding resources is allowed, check whether a normal file with // the .xnb extension already exists. If it does, we will use the .xnb file // instead of the compressed content. This prevents nasty surprises when people // run the content compressor on their build directory and then continue working. if(this.replacementAllowed) { string xnbAssetFilename = Path.ChangeExtension(assetName, ".xnb"); string xnbAssetPath = Path.Combine(RootDirectory, xnbAssetFilename); if(File.Exists(xnbAssetPath)) { return new FileStream( xnbAssetPath, FileMode.Open, FileAccess.Read, FileShare.Read ); } } // Are we configured to use a package? if(this.files != null) { return uncompressFromPackage(assetName); } else { // Nope, look for an individually compressed file! return uncompressIndividualFile(assetName); } } /// Uncompresses an LZMA-compressed file from an LZMA package /// Name of the packaged file to decompress /// A stream by which the uncompressed data can be read private Stream uncompressFromPackage(string assetName) { // Look for the compressed asset in the LZMA package CompressedFileInfo fileInfo; if(!this.files.TryGetValue(assetName.ToLower(), out fileInfo)) { throw new ArgumentException( string.Format("Asset '{0}' not found in package", assetName), "assetName" ); } // We have found the entry, jump to the indicated position and // uncompress the asset lock(this.lzmaPackageStream) { this.lzmaPackageStream.Position = fileInfo.Offset; return uncompress( this.lzmaPackageStream, fileInfo.CompressedLength, fileInfo.UncompressedLength ); } } /// Uncompresses an invididually compressed LZMA file /// Name of the asset to uncompress as an LZMA file /// A stream by which the uncompressed data can be read private Stream uncompressIndividualFile(string assetName) { // Look for a compressed .lzma asset string lzmaAssetFilename = Path.ChangeExtension(assetName, ".lzma"); string lzmaAssetPath = Path.Combine(RootDirectory, lzmaAssetFilename); if(!File.Exists(lzmaAssetPath)) { throw new ArgumentException( string.Format("Asset '{0}' not found", assetName), "assetName" ); } // Try to open and decompress the .lzma asset as an individual file using( FileStream compressedFile = new FileStream( lzmaAssetPath, FileMode.Open, FileAccess.Read, FileShare.Read ) ) { BinaryReader reader = new BinaryReader(compressedFile); int uncompressedLength = reader.ReadInt32(); return uncompress( compressedFile, (int)(compressedFile.Length - 4), uncompressedLength ); } } /// Uncompresses a stream of LZMA-compressed data into a memory stream /// Source stream containing the LZMA-compressed data /// Length of the compressed data /// Length the uncompressed data will have /// A memory stream containing the uncompressed data private Stream uncompress(Stream source, int compressedLength, int uncompressedLength) { BinaryReader reader = new BinaryReader(source); // Build a memory chunk we can uncompress into MemoryStream uncompressedMemory = new MemoryStream(uncompressedLength); // Set up the LZMA decoder and decode the compressed asset into the memory chunk Decoder decoder = new Decoder(); decoder.SetDecoderProperties(reader.ReadBytes(5)); decoder.Code( source, uncompressedMemory, compressedLength - 5, uncompressedLength, null ); // Done, set the file pointer to the beginning of the memory chunk and return it uncompressedMemory.Position = 0; return uncompressedMemory; } /// Opens the specified package for access by the LZMA content manager /// Path of the package that will be opened private void openPackage(string packagePath) { // If file replacement is allowed, there might not even be a package and all the // assets are stored uncompressed in their respective folders. if(this.replacementAllowed) { if(!File.Exists(packagePath)) { this.files = new Dictionary(); return; } } FileStream lzmaPackageStream = new FileStream( packagePath, FileMode.Open, FileAccess.Read, FileShare.Read ); try { BinaryReader reader = new BinaryReader(lzmaPackageStream); // Obtain the number of assets that are stored in this package int fileCount = reader.ReadInt32(); this.files = new Dictionary(fileCount); // Read the file headers and all assets stored in the LZMA package for(int fileIndex = 0; fileIndex < fileCount; ++fileIndex) { string name = reader.ReadString(); CompressedFileInfo fileInfo; fileInfo.Offset = reader.ReadInt64(); fileInfo.CompressedLength = reader.ReadInt32(); fileInfo.UncompressedLength = reader.ReadInt32(); this.files.Add(name.ToLower(), fileInfo); } } catch(Exception) { lzmaPackageStream.Dispose(); throw; } // Successfully parsed, take over the package stream this.lzmaPackageStream = lzmaPackageStream; } /// /// Whether compressed files can be overridden by placing an uncompressed file /// at the same location the compressed file is in. /// private bool replacementAllowed; /// File stream for the LZMA package opened by this content manager private FileStream lzmaPackageStream; /// Starting offsets for the files contained private Dictionary files; } } // namespace Nuclex.Game.Content