using System; using System.Collections; using System.Collections.Specialized; using System.IO; using System.Runtime.InteropServices; using System.Text; using NWN.FileTypes.Tools; namespace NWN.FileTypes.BIF { /// /// The class encapsulates a NWN BIF file, allowing access to the files /// within the BIF. /// internal class Bif { #region public properties/methods /// /// Class constructor. It caches the list of files in the BIF for quick /// searching. /// /// The name of the BIF public Bif(string name) { // Delay loading the cache until needed. this.name = name; entries = null; } /// /// Attempts to extract the specified file ID from the BIF, returning /// the file's data in a Stream. /// /// The id of the file /// A stream for the file or null if the BIF does not contain /// the file. public Stream GetFile(uint id) { id = id & 0xfffff; // If we haven't populated the cache then do so now. if (null == entries) Cache(); // If the file isn't ours then return null. BifEntry entry = entries[id] as BifEntry; if (null == entry) return null; // Read the raw data into a memory stream. NWNLogger.Log(1, "Bif.GetFile({0}) found file in BIF {1}", id, name); using (FileStream reader = new FileStream(name, FileMode.Open, FileAccess.Read, FileShare.Read)) { // Read the file's data. byte[] buffer = new byte[entry.Size]; reader.Seek(entry.Offset, SeekOrigin.Begin); reader.Read(buffer, 0, buffer.Length); // Create a memory stream for the data and return it. MemoryStream s = new MemoryStream(buffer, false); return s; } } #endregion #region private raw structures for reading bif file data [StructLayout(LayoutKind.Sequential, Pack=1, CharSet=CharSet.Ansi)] private struct RawHeader { #region members public UInt32 FileType; public UInt32 FilerVersion; public UInt32 VariableResourceCount; public UInt32 FixedResourceCount; public UInt32 VariableTableOffset; #endregion } [StructLayout(LayoutKind.Sequential, Pack=1, CharSet=CharSet.Ansi)] private struct RawEntry { #region members public UInt32 ID; public UInt32 Offset; public UInt32 FileSize; public UInt32 ResourceType; #endregion } #endregion #region private nested classes /// /// Class that contains the data for a BIF file entry /// private class BifEntry { public uint Offset; public uint Size; } #endregion #region private fields/properties/methods private string name; private Hashtable entries; /// /// Caches the biff's file entries in our hashtable. /// private void Cache() { entries = new Hashtable(); using (FileStream reader = new FileStream(name, FileMode.Open, FileAccess.Read, FileShare.Read)) { // Read the file header. RawHeader header = (RawHeader) RawSerializer.Deserialize(typeof(RawHeader), reader); // Read all of the variable sized resources from the BIF, storing // their size/offset entries in our hash table. Fixed resources // are not implemented we don't have to worry about them. reader.Seek(header.VariableTableOffset, SeekOrigin.Begin); for (int i = 0; i < header.VariableResourceCount; i++) { // Read the raw entry data. RawEntry rawEntry = (RawEntry) RawSerializer.Deserialize(typeof(RawEntry), reader); // Create an entry and add it to our hash table. BifEntry entry = new BifEntry(); entry.Offset = rawEntry.Offset; entry.Size = rawEntry.FileSize; entries.Add((uint) (rawEntry.ID & 0xfffff), entry); } } } #endregion } /// /// This class encapsulates a NWN KEY file, which contains information /// about files stored in BIF files. /// internal class Key { /// /// Class constructor /// /// The name of the key file to load public Key(string name) { keyFileName = name; bifs = new ArrayList(); keys = new Hashtable(); // Glue the NWN install dir onto the file name and open it. string fileName = Path.Combine(NWN.NWNInfo.InstallPath, name); using (FileStream reader = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read)) { // Read the file header. RawHeader header = (RawHeader) RawSerializer.Deserialize(typeof(RawHeader), reader); // Read all of the BIF file entries from the key file. reader.Seek(header.OffsetToFileTable, SeekOrigin.Begin); RawFileEntry[] files = new RawFileEntry[header.BIFCount]; for (int i = 0; i < header.BIFCount; i++) files[i] = (RawFileEntry) RawSerializer.Deserialize(typeof(RawFileEntry), reader); // Loop through the BIF file entries making a list of all of // the BIFs this key file covers. byte[] fileNameBuffer = new byte[2048]; for (int i = 0; i < header.BIFCount; i++) { // Read the raw name data and convert it to a string. reader.Seek(files[i].FilenameOffset, SeekOrigin.Begin); reader.Read(fileNameBuffer, 0, files[i].FilenameSize); string s = RawSerializer.DeserializeString(fileNameBuffer, 0, files[i].FilenameSize); // The path is relative to the install directory, so glue that on before adding // the BIF to our string collection. //if (0 != (1 & files[i].Drives)) s = Path.Combine(NWN.NWNInfo.InstallPath, s); //else // throw new NWNException("Uknown data in file entry block in key file {0}", name); bifs.Add(new Bif(s)); } // Read all of the key entries, i.e. what files are in the BIFs. reader.Seek(header.OffsetToKeyTable, SeekOrigin.Begin); for (int i = 0; i < header.KeyCount; i++) { // Read the raw data. RawKeyEntry key = (RawKeyEntry) RawSerializer.Deserialize(typeof(RawKeyEntry), reader); // Build the file name from the ResRef and resource type. Then // add the file name and ID to our hash table. If this booms then // that is because of a resource type we don't know about, for // now we ignore that. ResType resType = (ResType) key.ResourceType; try { string ext = resType.ToString().Substring(3, 3).ToLower(); string s = RawSerializer.DeserializeString(key.ResRef, 0, 16); s = string.Format("{0}.{1}", s, ext); keys.Add(s, key.ResID); } catch (Exception) {} } } } /// /// Checks to see if the key contains the file, and if it does extracts /// it from it's BIF and returns it's data in a Stream. /// /// The file to extract /// A Stream containing the file's data or null if the key /// does not contain the file. public Stream GetFile(string file) { if (!keys.Contains(file)) return null; // The file is ours, check each of our BIFS for the file. // The bottom 20 bits are the index in the bif and the top // 12 bits are the bif index in our bif array. uint id = (uint) keys[file]; uint index = id >> 20; Bif bif = (Bif) bifs[(int) index]; return bif.GetFile(id & 0xfffff); /* * Not totally sure the above code is right so saving this foreach (Bif bif in bifs) { // Try to load the file from the BIF, if successful return it. Stream s = bif.GetFile(id); if (null != s) return s; } // If we get here something really bad has happened. throw new NWNException("Cannot extract file {0} from BIFs", file); */ } #region private raw structures for reading key file data [StructLayout(LayoutKind.Sequential, Pack=1, CharSet=CharSet.Ansi)] private struct RawHeader { #region members public UInt32 FileType; public UInt32 FilerVersion; public UInt32 BIFCount; public UInt32 KeyCount; public UInt32 OffsetToFileTable; public UInt32 OffsetToKeyTable; public UInt32 BuildYear; public UInt32 BuildDay; [MarshalAs(UnmanagedType.ByValArray, SizeConst=32)] private Byte[] Reserved; #endregion } [StructLayout(LayoutKind.Sequential, Pack=1, CharSet=CharSet.Ansi)] private struct RawFileEntry { #region members public UInt32 FileSize; public UInt32 FilenameOffset; public UInt16 FilenameSize; public UInt16 Drives; #endregion } [StructLayout(LayoutKind.Sequential, Pack=1, CharSet=CharSet.Ansi)] private struct RawKeyEntry { #region members [MarshalAs(UnmanagedType.ByValArray, SizeConst=16)] public byte[] ResRef; public UInt16 ResourceType; public UInt32 ResID; #endregion } #endregion #region private fields/properties/methods private string keyFileName; private ArrayList bifs; private Hashtable keys; #endregion } /// /// This class implements a collection of all of the KEY files installed /// on the user's system. It can be used to extract files from those /// keys. /// public class KeyCollection { #region public static methods /// /// Looks in all of the key files for the given file, extracting it /// from it's BIF and returning it's data in a stream. /// /// The file to extract /// A Stream containing the file data or null if the file /// cannot be found public static Stream GetFile(string file) { // Get a lower case copy of just the file name. file = Path.GetFileName(file).ToLower(); // Loop through all of the keys looking for the file. foreach (Key key in Singleton.keys) { // Ask the key for the file if we get it return it. Stream s = key.GetFile(file); if (null != s) return s; } // We couldn't find the file return null. return null; } #endregion #region private static methods private static KeyCollection singleton; /// /// Gets the singleton instance /// private static KeyCollection Singleton { get { if (null == singleton) singleton = new KeyCollection(); return singleton; } } #endregion #region private fields/properties/methods private ArrayList keys; /// /// Default constructor /// private KeyCollection() { keys = new ArrayList(); // Get a list of the key files for the NWN install and add the files // to a StringCollection, forcing the names to lower case. string[] filesArray = Directory.GetFiles(NWN.NWNInfo.InstallPath, "*.key"); StringCollection files = new StringCollection(); foreach (string file in filesArray) files.Add(Path.GetFileName(file).ToLower()); // We need to do the files in a certain order, as we want to check the xp's in // reverse order, then the main game last. ProcessFile(files, "xp3.key"); ProcessFile(files, "xp2patch.key"); ProcessFile(files, "xp2.key"); ProcessFile(files, "xp1patch.key"); ProcessFile(files, "xp1.key"); // Now process whatever is left. foreach (string file in files) { Key key = new Key(file); keys.Add(key); } } /// /// Checks to see if the string collection contains the key file, and if it does /// loads it and removes it from the collection. /// /// The list of files /// The key file to process private void ProcessFile(StringCollection files, string file) { if (files.Contains(file)) { // Create the key, add it to our collection, and remove the // file from the list as we've loaded it. Key key = new Key(file); keys.Add(key); files.Remove(file); } } #endregion } }