using System;
using System.Collections;
using System.Collections.Specialized;
using System.IO;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text;
using NWN.FileTypes.Tools;
namespace NWN.FileTypes.Tools
{
///
/// This enum defines all of the different resources that can be stored
/// in an ERF file.
///
public enum ResType : ushort
{
#region values
Invalid = 0xffff,
ResBMP = 1,
ResTGA = 3,
ResWAV = 4,
ResPLT = 6,
ResINI = 7,
ResBMU = 8,
ResTXT = 10,
ResMDL = 2002,
ResNSS = 2009,
ResNCS = 2010,
ResARE = 2012,
ResSET = 2013,
ResIFO = 2014,
ResBIC = 2015,
ResWOK = 2016,
Res2DA = 2017,
ResTXI = 2022,
ResGIT = 2023,
ResUTI = 2025,
ResUTC = 2027,
ResDLG = 2029,
ResITP = 2030,
ResUTT = 2032,
ResDDS = 2033,
ResUTS = 2035,
ResLTR = 2036,
ResGFF = 2037,
ResFAC = 2038,
ResUTE = 2040,
ResUTD = 2042,
ResUTP = 2044,
ResDFT = 2045,
ResGIC = 2046,
ResGUI = 2047,
ResUTM = 2051,
ResDWK = 2052,
ResPWK = 2053,
ResJRL = 2056,
ResUTW = 2058,
ResSSF = 2060,
ResNDB = 2064,
ResPTM = 2065,
ResPTT = 2066
#endregion
}
///
/// Class that facilitates debug logging. It provides a way for all of the NWN tools
/// to do logging via a single object. Implementation is a singleton.
///
public class NWNLogger
{
#region public static properties/methods
///
/// Gets/sets the name of the log file. This should not have any path information
/// it should just be a file name. The log is automatically created in the NWN logs
/// folder.
///
public static string LogFile
{
get { return logFile; }
set { logFile = value; }
}
///
/// Gets/sets the minimum severity level that is logged. If this is set to
/// 0 then all messages are logged, if set to a number higher than 0, only messages
/// with that severity or higher are logged.
///
public static int MinimumLogLevel
{
get { return minLevel; }
set { minLevel = value; }
}
///
/// Enables/disables logging.
///
public static bool Logging
{
get { return null != stream; }
set
{
// If we're not logging and logging is turned on then open the
// log file.
if (value && null == stream)
{
// Get the full name of the log file.
string logPath = Path.Combine(NWNInfo.InstallPath, "logs");
string logFullName = Path.Combine(logPath, logFile);
// Create/append the log file and add a header to indicate a new log session.
stream = new StreamWriter(logFullName, true, Encoding.ASCII);
stream.WriteLine("");
stream.WriteLine("");
stream.WriteLine("");
stream.WriteLine("*********************************************************");
stream.WriteLine("Logging started at {0}", DateTime.Now);
stream.WriteLine("*********************************************************");
stream.WriteLine("");
}
// If we're logging and logging is turned off then flush and close
// the log file and null our object reference.
if (!value && null != stream)
{
stream.Flush();
stream.Close();
stream = null;
}
}
}
///
/// Logs the given format string and arguments to the log file if logging is enabled.
///
/// The importance level of the message, the higher the number
/// the more important the message.
/// The format string
/// The format string's data
public static void Log(int level, string format, params object[] args)
{
// If we are logging and the message is important enough then log it.
if (null != stream && level >= minLevel)
{
stream.WriteLine(format, args);
stream.Flush();
}
}
#endregion
#region private static fields/properties/methods
private static int minLevel = 0;
private static string logFile = "NWNLogger.txt";
private static StreamWriter stream;
#endregion
}
///
/// This class contains functionality to serialize/deserialize objects
/// to streams and byte arrays.
///
internal sealed class RawSerializer
{
#region public static methods to deserialize raw data
///
/// Deserializes an object of the given type from a stream. The stream
/// is assumed to contain the object's raw data at the current seek
/// position.
///
/// The type of object to deserialize
/// The stream
/// The deserialized object
public static object Deserialize (Type t, Stream stream)
{
// Allocate a buffer to hold an object of the given type, and
// read the raw object data from the file.
//NWNLogger.Log(0, "RawSerializer.Deserialize entering");
if (null == t) NWNLogger.Log(10, "t is null!!!");
if (null == stream) NWNLogger.Log(10, "stream is null!!!");
//NWNLogger.Log(0, "RawSerializer.Deserialize({0}, {1})", t.Name, stream.GetType().Name);
int size = Marshal.SizeOf(t);
//NWNLogger.Log(0, "RawSerializer.Deserialize sizeof(t) = {0}, allocing byte array", size);
byte[] buffer = new Byte[size];
//NWNLogger.Log(0, "RawSerializer.Deserialize reading {0} bytes from stream", buffer.Length);
if (stream.Read(buffer, 0, buffer.Length) != buffer.Length) return null;
// Deserialize from the raw data.
//NWNLogger.Log(0, "RawSerializer.Deserialize calling Deserialize overload");
return Deserialize(t, buffer);
}
///
/// Deserializes an object of the given type from a byte array. The byte
/// array is assumed to contain the object's raw data.
///
/// The type of object to deserialize
/// The byte array containing the object's
/// raw data
/// The deserialized object
public static object Deserialize (Type t, byte[] buffer)
{
// Alloc a hglobal to store the bytes.
//NWNLogger.Log(0, "RawSerializer.Deserialize Marshal.AllocHGlobal({0})", buffer.Length);
IntPtr ptr = Marshal.AllocHGlobal(buffer.Length);
try
{
// Copy the data to unprotected memory, then convert it to a STlkHeader
// structure
//NWNLogger.Log(0, "RawSerializer.Deserialize calling Marshal.Copy()");
Marshal.Copy(buffer, 0, ptr, buffer.Length);
//NWNLogger.Log(0, "RawSerializer.Deserialize calling Marshal.PtrToStructure()");
object o = Marshal.PtrToStructure(ptr, t);
//NWNLogger.Log(0, "RawSerializer.Deserialize created object of type {0}",
// null == o ? "null" : o.GetType().Name);
return o;
}
finally
{
// Free the hglobal before exiting.
//NWNLogger.Log(0, "RawSerializer.Deserialize calling Marshal.FreeHGlobal()");
Marshal.FreeHGlobal(ptr);
}
}
///
/// Deserializes an ANSI string from the passed byte array.
///
/// The byte array
/// The deserialized string.
public static string DeserializeString(byte[] buffer)
{
return DeserializeString(buffer, 0, buffer.Length);
}
///
/// Deserializes an ANSI string from the passed byte array.
///
/// The byte array
/// The offset into the byte array of the
/// start of the string
/// The length of the string in the array
/// The deserialized string.
public static string DeserializeString(byte[] buffer, int offset, int length)
{
// figure out how many chars in the string are really used. If we
// don't do this then the extra null bytes at the end get included
// in the string length which messes up .NET internally.
int used = 0;
for (; used < length; used++)
if (0 == buffer[offset + used]) break;
// If the string is empty then just return that.
if (0 == used) return string.Empty;
// Alloc a hglobal to store the bytes.
IntPtr ptr = Marshal.AllocHGlobal(used);
try
{
// Copy the data to unprotected memory, then convert it to a STlkHeader
// structure
Marshal.Copy(buffer, offset, ptr, used);
object o = Marshal.PtrToStringAnsi(ptr, used);
return (string) o;
}
finally
{
// Free the hglobal before exiting.
Marshal.FreeHGlobal(ptr);
}
}
#endregion
#region public static methods to serialize raw data
///
/// Serializes the passed object to the stream.
///
/// The stream to serialize the object to
/// The object to serialize
public static void Serialize(Stream s, object o)
{
byte[] buffer = Serialize(o);
s.Write(buffer, 0, buffer.Length);
}
///
/// Serializes the passed object to a byte array.
///
/// The object to serialize
/// A byte array containing the object's raw data
public static byte[] Serialize(object o)
{
// Allocate a hglobal to store the object's data.
int rawsize = Marshal.SizeOf(o);
IntPtr buffer = Marshal.AllocHGlobal(rawsize);
try
{
// Copy the object to unprotected memory, then copy that to a byte array.
Marshal.StructureToPtr(o, buffer, false);
byte[] rawdata = new byte[rawsize];
Marshal.Copy(buffer, rawdata, 0, rawsize);
return rawdata;
}
finally
{
// Free the hglobal before exiting
Marshal.FreeHGlobal(buffer);
}
}
///
/// Serializes a string to a fixed length byte array.
///
/// The string to serialize
/// The length of the resultant byte array. The
/// string will be truncated or nulls will be added as necessary to
/// make the byte array be this length
/// Indicates whether the string's trailing null
/// byte should be included in length.
/// A byte array of length length containing the serialized string
public static byte[] SerializeString(string s, int length, bool includeNull)
{
// Figure out how many real characters we can have in the string, if
// we are saving the null in the buffer we have to account for it.
int adjustedLen = includeNull ? length - 1 : length;
// If the string is too long then trim it, then
// convert it to
if (s.Length > adjustedLen) s = s.Substring(0, adjustedLen);
IntPtr ptr = Marshal.StringToHGlobalAnsi(s);
try
{
// Allocate a buffer for the data with the proper length, copy
// the string data, then pad with nulls if necessary.
byte[] buffer = new byte[length];
Marshal.Copy(ptr, buffer, 0, s.Length);
for (int i = s.Length; i < length; i++) buffer[i] = 0;
return buffer;
}
finally
{
Marshal.FreeHGlobal(ptr);
}
}
#endregion
}
}
namespace NWN.FileTypes
{
///
/// Enum defining the various languages that strings may be.
///
public enum LanguageID
{
#region values
English = 0,
French = 1,
German = 2,
Italian = 3,
Spanish = 4,
Polish = 5,
Korean = 128,
ChineseTrad = 129,
ChineseSimple = 130,
Japanese = 131
#endregion
}
///
/// Class for all NWN exceptions.
///
public class NWNException: Exception
{
#region public properties/methods
///
/// Constructor to build the exception from just a string
///
/// Error message
public NWNException (string s) : base(s)
{
}
///
/// Constructor to build the exception from a formatted message.
///
/// Format string
/// Message arguments for the format string
public NWNException (string format, params object[] args) :
base(Format(format, args))
{
}
#endregion
#region private fields/properties/methods
///
/// Method to format a string from a format string and arguments.
///
/// Format string
/// Argument list
/// Formatted string
private static string Format(string format, params object[] args)
{
StringBuilder b = new StringBuilder();
b.AppendFormat(format, args);
return b.ToString();
}
#endregion
}
///
/// This class is used to manipulate an ERF file. The ERF file format is used
/// for ERF, MOD, SAV, and HAK files. This class allows any of those files to
/// be decompressed and modified.
///
public class Erf
{
#region public nested enums/structs/classes
///
/// Enum for the different types of ERF files.
///
public enum ErfType { HAK, MOD, ERF, SAV };
///
/// This structure defines a string value as stored in an ERF file.
/// It provides functionality to serialize/deserialize the raw data.
///
public struct ErfString
{
#region public properties/methods
///
/// Gets the number of bytes the string will be when saved in
/// the stream.
///
public int SizeInStream
{
get
{
// The size of the string in the stream is 8 bytes (4 for
// the language ID, 4 for the string size) plus the
// string length.
return 8 + val.Length + (IncludeNull ? 1 : 0);
}
}
///
/// Gets the language ID for the string.
///
public LanguageID Language { get { return (LanguageID) languageID; } }
///
/// Gets/sets the string value.
///
public string Value
{
get { return val; }
set { val = value; }
}
///
/// Class constructor
///
/// The string value
/// The type of ERF the string is coming from, some ERFs
/// have null terminated strings and some do not
public ErfString(string s, ErfType type)
{
// 0 is English.
languageID = (Int32) LanguageID.English;
val = s;
this.type = type;
}
///
/// Class constructor to deserialize an ErfString.
///
/// The stream containing the raw data.
/// The type of ERF the string is coming from, some ERFs
/// have null terminated strings and some do not
public ErfString(Stream s, ErfType type)
{
// Read the language ID from the stream.
byte[] buffer = new Byte[4];
if (buffer.Length != s.Read(buffer, 0, buffer.Length))
throw new NWNException("Invalid erf string in stream");
languageID = BitConverter.ToInt32(buffer, 0);
// Read the number of bytes in the string from the stream.
if (buffer.Length != s.Read(buffer, 0, buffer.Length))
throw new NWNException("Invalid erf string in stream");
Int32 size = BitConverter.ToInt32(buffer, 0);
// Read the string bytes from the stream.
buffer = new byte[size];
if (buffer.Length != s.Read(buffer, 0, buffer.Length))
throw new NWNException("Invalid erf string in stream");
val = RawSerializer.DeserializeString(buffer);
this.type = type;
}
///
/// Method to serialize the ErfString to a stream.
///
///
public void Serialize(Stream s)
{
// Write the structure's data to the stream.
RawSerializer.Serialize(s, languageID);
int count = val.Length + (IncludeNull ? 1 : 0);
RawSerializer.Serialize(s, (Int32) count);
byte[] buffer = RawSerializer.SerializeString(val, val.Length, false);
s.Write(buffer, 0, buffer.Length);
// Write a null byte if needed.
if (IncludeNull)
{
buffer[0] = 0;
s.Write(buffer, 0, 1);
}
}
#endregion
#region public static methods
///
/// Deserializes a number of ErfString objects from the passed stream,
/// placing them into an array.
///
/// The stream
/// The number of strings to deserialize
/// The type of ERF the string is coming from, some ERFs
/// have null terminated strings and some do not
/// An ErfString array with the strings
public static ErfString[] Deserialize(Stream s, int count, ErfType type)
{
ErfString[] estrings = new ErfString[count];
for (int i = 0; i < count; i++)
estrings[i] = new ErfString(s, type);
return estrings;
}
///
/// Serializes a number of ErfStrings from the passed array.
///
/// The stream
/// The ErfString structures to serialize
public static void Serialize(Stream s, ErfString[] estrings)
{
foreach (ErfString estring in estrings)
estring.Serialize(s);
}
#endregion
#region private fields/properties/methods
private Int32 languageID;
private string val;
private ErfType type;
///
/// Returns true if the null terminator should be included in
/// the string's length.
///
private bool IncludeNull { get { return ErfType.ERF == type || ErfType.HAK == type; } }
#endregion
}
#endregion
#region public properties/methods
///
/// Gets the number of files in the ERF.
///
public int FileCount
{
get { return header.EntryCount + addedFileHash.Count - removedFiles.Count; }
}
///
/// Gets the name of the ERF file if it represents a file on disk.
///
public string FileName
{ get { return null == fileInfo ? string.Empty : fileInfo.FullName; } }
///
/// Gets the list of files in the erf.
///
public StringCollection Files
{
get
{
StringCollection files = new StringCollection();
// Add all of the files that were in the erf to start with.
foreach (ErfKey key in keys)
files.Add(key.FileName);
// Add any added files.
string[] strings = new string[addedFileHash.Count];
addedFileHash.Values.CopyTo(strings, 0);
files.AddRange(strings);
return files;
}
}
///
/// Gets the collection of replaced files.
///
public StringCollection ReplacedFiles
{
get
{
// Get all of the strings from the hash table.
string[] strings = new string[replacedFileHash.Count];
replacedFileHash.Values.CopyTo(strings, 0);
// Copy the strings to a string array and return it.
StringCollection collection = new StringCollection();
collection.AddRange(strings);
return collection;
}
}
///
/// Gets the collection of added files.
///
public StringCollection AddedFiles
{
get
{
// Get all of the strings from the hash table.
string[] strings = new string[addedFileHash.Count];
addedFileHash.Values.CopyTo(strings, 0);
// Copy the strings to a string array and return it.
StringCollection collection = new StringCollection();
collection.AddRange(strings);
return collection;
}
}
///
/// Default constructor
///
private Erf()
{
removedFiles = new StringCollection();
keyHash = new Hashtable(5000);
addedFileHash = new Hashtable(1000);
replacedFileHash = new Hashtable(1000);
decompressedPath = string.Empty;
}
///
/// Returns true if the ERF contains a file with the given file name.
///
/// The file to look for
/// True if the ERF contains the file, false if it does not.
public bool Contains(string fileName)
{
// Get the key for the file name.
string key = GetKey(fileName);
// Check to see if the file is in the ERF key list.
if (keyHash.Contains(key)) return true;
// Check to see if the file is in the added file list.
if (addedFileHash.Contains(key)) return true;
return false;
}
///
/// Adds an existing file to the ERF. The actual erf file is
/// not modified until RecreateFile() is called, the reference
/// to the file is merely saved.
///
/// The name of the file to add
/// Indicates whether to overwrite the file
/// if it already exists
public void AddFile(string fileName, bool overwrite)
{
// Ignore ExportInfo.GFF, a file in all ERF's.
if ("exportinfo.gff" == Path.GetFileName(fileName).ToLower()) return;
// Make sure the file really exists.
if (!File.Exists(fileName))
throw new NWNException("Cannot add non-existant file {0}", fileName);
// Make sure that the file isn't already in the ERF.
bool contains = Contains(fileName);
if (contains && !overwrite)
throw new NWNException("File {0} is already in the erf", fileName);
// Just add the file to our added/replaced files collection, depending
// on whether it's already in the erf or not.
if (contains)
replacedFileHash.Add(GetKey(fileName), fileName);
else
addedFileHash.Add(GetKey(fileName), fileName);
}
///
/// Removes a file from the added/replaced file lists.
///
/// The name of the file to remove
public void RemoveFileFromAddedList(string fileName)
{
// Get the key for the file name.
string key = GetKey(fileName);
// Check to see if the file is in the added file list.
if (addedFileHash.Contains(key))
addedFileHash.Remove(key);
else if (replacedFileHash.Contains(key))
replacedFileHash.Remove(key);
}
///
/// Saves the ERF file under the specified name.
///
/// The name of the file.
public void SaveAs(string fileName)
{
NWNLogger.Log(0, "module.SaveAs entering [{0}]", fileName);
// The ERF must be decompressed first unless it is a new ERF.
if (keys.Length > 0 && string.Empty == decompressedPath)
throw new NWNException("ERF must be decompressed to recreate");
// Copy all of the modified files into the temp directory
StringCollection replacedFiles = ReplacedFiles;
NWNLogger.Log(0, "module.SaveAs copying {0} modified files into temp directory", replacedFiles.Count);
foreach (string file in replacedFiles)
File.Copy(file, Path.Combine(decompressedPath,
Path.GetFileName(file)), true);
// Figure out the new number of files in the ERF and create new
// key/resource arrays of the proper size.
int fileCount = keys.Length + addedFileHash.Count - removedFiles.Count;
NWNLogger.Log(0, "module.SaveAs {0} total files, allocating key/resource arrays", fileCount);
ErfKey[] newKeys = new ErfKey[fileCount];
ErfResource[] newResources = new ErfResource[fileCount];
// Create a buffer to store the data.
NWNLogger.Log(0, "module.SaveAs creating memory stream");
MemoryStream buffer = new MemoryStream();
// Copy all of the existing not-removed files into the new key/resource
// arrays.
int index = 0;
for (int i = 0; i < keys.Length; i++)
{
string file = keys[i].FileName;
if (string.Empty == file || removedFiles.Contains(file)) continue;
// Copy the key/resource pair over.
NWNLogger.Log(1, "module.SaveAs copying file[{0}] '{1}'", i, file);
newKeys[index] = keys[i];
newResources[index] = resources[i];
// Read the file into the buffer.
ReadFileIntoStream(Path.Combine(decompressedPath, file),
ref newResources[index], buffer);
index++;
}
// Add all of the new files to the key/resource arrays.
StringCollection addedFiles = AddedFiles;
foreach (string file in addedFiles)
{
NWNLogger.Log(1, "module.SaveAs adding new file '{0}'", file);
newKeys[index] = new ErfKey(file);
newResources[index] = new ErfResource();
// Read the file into the buffer.
ReadFileIntoStream(file, ref newResources[index], buffer);
index++;
}
// Figure out how big our descriptions are going to be.
NWNLogger.Log(0, "module.SaveAs calcing description size");
int descriptionsCount = 0;
for (int i = 0; i < descriptions.Length; i++)
descriptionsCount += descriptions[i].SizeInStream;
// Create a new resource header and calculate the new offsets.
NWNLogger.Log(0, "module.SaveAs creating header");
ErfHeader newHeader = header;
newHeader.OffsetToLocalizedString = Marshal.SizeOf(typeof(ErfHeader));
newHeader.OffsetToKeyList = newHeader.OffsetToLocalizedString + descriptionsCount;
newHeader.EntryCount = fileCount;
newHeader.OffsetToResourceList = newHeader.OffsetToKeyList +
(fileCount * Marshal.SizeOf(typeof(ErfKey)));
// Calculate the offset to the beginning of the resource data and adjust
// the offsets in the resource array to take this into account.
NWNLogger.Log(0, "module.SaveAs calcing offsets");
int offsetToData = newHeader.OffsetToResourceList +
(fileCount * Marshal.SizeOf(typeof(ErfResource)));
for (int i = 0; i < newResources.Length; i++)
newResources[i].OffsetToResource += offsetToData;
// Create the new file and write the data to it.
NWNLogger.Log(0, "module.SaveAs creating output file");
string newName = fileName + ".New";
using (FileStream writer = new FileStream(newName, FileMode.Create,
FileAccess.Write, FileShare.Write))
{
NWNLogger.Log(0, "module.SaveAs writing header");
newHeader.Serialize(writer);
NWNLogger.Log(0, "module.SaveAs writing strings");
ErfString.Serialize(writer, descriptions);
NWNLogger.Log(0, "module.SaveAs writing keys");
ErfKey.Serlialize(writer, newKeys);
NWNLogger.Log(0, "module.SaveAs writing resources");
ErfResource.Serlialize(writer, newResources);
NWNLogger.Log(0, "module.SaveAs writing raw data");
writer.Write(buffer.GetBuffer(), 0, (int) buffer.Length);
NWNLogger.Log(0, "module.SaveAs flushing and closing");
writer.Flush();
writer.Close();
}
// Delete the old file and rename the new file to the proper name.
NWNLogger.Log(0, "module.SaveAs copying over current file");
File.Copy(newName, fileName, true);
NWNLogger.Log(0, "module.SaveAs deleting");
File.Delete(newName);
// Update the ERF's field's with the new values.
NWNLogger.Log(0, "module.SaveAs updating object definition");
header = newHeader;
keys = newKeys;
resources = newResources;
// Clear our string collections.
replacedFileHash.Clear();
addedFileHash.Clear();
removedFiles.Clear();
fileInfo = new FileInfo(fileName);
}
///
/// Rebuilds the ERF disk file. Any added/removed/changed files will be
/// reflected in the new file.
///
public void RecreateFile()
{
SaveAs(fileInfo.FullName);
}
///
/// Decompresses the ERF to the specified directory.
///
/// The path to decompress the erf to
public void Decompress(string path)
{
try
{
// If the path doesn't exist then create it.
NWNLogger.Log(1, "Erf.Decompress creating path {0}", path);
if (!Directory.Exists(path)) Directory.CreateDirectory(path);
// If this is not a new blank ERF then decompress it.
if (null != fileInfo)
{
// Open the file.
NWNLogger.Log(1, "Erf.Decompress opening erf {0}", fileInfo.FullName);
using (FileStream reader = new FileStream(fileInfo.FullName, FileMode.Open,
FileAccess.Read, FileShare.Read))
{
// Loop through all of key/resource entries.
NWNLogger.Log(0, "Erf.Decompress reading keys");
for (int i = 0; i < header.EntryCount; i++)
{
// Ignore empty file names, why this can happen I don't know but
// it does.
if (string.Empty == keys[i].FileName)
{
NWNLogger.Log(2, "Erf.Decompress key[{0}] contains empty file name", i);
continue;
}
// Generate the full path to the output file and create it.
string outFile = Path.Combine(path, keys[i].FileName);
//NWNLogger.Log(1, "Erf.Decompress creating file {0}", outFile);
using (FileStream writer = new FileStream(outFile, FileMode.Create,
FileAccess.Write, FileShare.None))
{
// Read the file data from the ERF.
//NWNLogger.Log(0, "Erf.Decompress reading file data from erf");
byte[] buffer = new byte[resources[i].ResourceSize];
reader.Seek(resources[i].OffsetToResource, SeekOrigin.Begin);
if (buffer.Length != reader.Read(buffer, 0, buffer.Length))
throw new NWNException("Cannot read data for {0}", keys[i].FileName);
// Write the data to the output file.
NWNLogger.Log(0, "Erf.Decompress file data to decompressed file");
writer.Write(buffer, 0, buffer.Length);
writer.Flush();
writer.Close();
}
}
}
}
// Save the path that we decompressed the files to so that we know
// that the files have been decompressed and where they are.
decompressedPath = path;
}
catch (Exception)
{
// If we have a problem we have to delete any decompressed files.
Directory.Delete(path, true);
throw;
}
}
#endregion
#region public static methods
///
/// Gets the number of files contained in the specifed ERF file.
///
/// The name of the file
/// The number of files in the ERF
public static int GetFileCount(string fileName)
{
using (FileStream reader = new FileStream(fileName, FileMode.Open))
{
// Read the header from the ERF and return the number of files.
ErfHeader header = new ErfHeader(reader);
return header.EntryCount;
}
}
///
/// This method loads a single file from the specified erf, returning
/// a MemoryStream containing the file's contents.
///
/// The erf containing the file
/// The file to load
/// A MemoryStream containing the file's data
public static MemoryStream GetFile(string erf, string file)
{
// Open the erf file.
NWNLogger.Log(0, "Erf.GetFile({0}, {1}) entering", erf, file);
using (FileStream reader =
new FileStream(erf, FileMode.Open, FileAccess.Read, FileShare.Read))
{
// Read the header from the ERF
ErfHeader header = new ErfHeader(reader);
NWNLogger.Log(0, "Erf.GetFile({0}, {1}) has {2} files", erf, file, header.EntryCount);
// Read the key (file) list from the ERF.
reader.Seek(header.OffsetToKeyList, SeekOrigin.Begin);
ErfKey[] keys = ErfKey.Deserialize(reader, header.EntryCount);
NWNLogger.Log(0, "Erf.GetFile({0}, {1}) read {2} keys", erf, file, keys.Length);
// Read the resource (file) list from the ERF.
reader.Seek(header.OffsetToResourceList, SeekOrigin.Begin);
ErfResource[] resources = ErfResource.Deserialize(reader, header.EntryCount);
NWNLogger.Log(0, "Erf.GetFile({0}, {1}) read {2} resources", erf, file, resources.Length);
// Loop through all of the resources in the erf looking for the file.
for (int i = 0; i < keys.Length; i++)
{
// Check to see if this is the file we're looking for.
NWNLogger.Log(1, "Erf.GetFile('{0}', '{1}'), keys[{2}].FileName '{3}'", erf, file, i, keys[i].FileName);
//if (keys[i].FileName.ToLower() == file.ToLower())
if (0 == string.Compare(keys[i].FileName, file, true, CultureInfo.InvariantCulture))
{
NWNLogger.Log(1, "Erf.GetFile('{0}', '{1}'), match!", erf, file);
// We found our file, create a MemoryStream large enough to hold the file's
// data and load the data into the stream.
byte[] buffer = new Byte[resources[i].ResourceSize];
reader.Seek(resources[i].OffsetToResource, SeekOrigin.Begin);
reader.Read(buffer, 0, resources[i].ResourceSize);
NWNLogger.Log(1, "Erf.GetFile('{0}', '{1}'), creating MemoryStream from {2} bytes!", erf, file, buffer.Length);
return new MemoryStream(buffer, false);
}
}
return null;
}
}
///
/// Loads the specified ERF file, returning an instance to it.
///
/// The name of the ERF file to load
/// An Erf object for the file
public static Erf Load(string fileName)
{
// Open the erf file.
Erf erf = new Erf();
using (FileStream reader =
new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
{
erf.fileInfo = new FileInfo(fileName);
// Read the header from the ERF
erf.header = new ErfHeader(reader);
// Read the description(s) from the ERF
reader.Seek(erf.header.OffsetToLocalizedString, SeekOrigin.Begin);
erf.descriptions = ErfString.Deserialize(reader, erf.header.LanguageCount,
erf.header.ErfType);
// Read the key (file) list from the ERF.
reader.Seek(erf.header.OffsetToKeyList, SeekOrigin.Begin);
erf.keys = ErfKey.Deserialize(reader, erf.header.EntryCount);
// Build the keys's hash table for fast access to files.
foreach (ErfKey key in erf.keys)
try
{
erf.keyHash.Add(key.FileName.ToLower(), key);
}
catch (ArgumentException)
{}
// Read the resource (file) list from the ERF.
reader.Seek(erf.header.OffsetToResourceList, SeekOrigin.Begin);
erf.resources = ErfResource.Deserialize(reader, erf.header.EntryCount);
}
return erf;
}
///
/// Creates a new, empty ERF file of the specified type.
///
/// The type of the ERF
/// The ERF's description
///
public static Erf New(ErfType type, string description)
{
// Create the ERF file and it's header.
Erf erf = new Erf();
erf.header = new ErfHeader(type);
// Create empty key/resource files since an empty ERF contains 0 files.
erf.keys = new ErfKey[0];
erf.resources = new ErfResource[0];
// Create an ErfString for the description.
erf.descriptions = new ErfString[1];
erf.descriptions[0] = new ErfString(description, type);
// Setup the header to account for our description.
erf.header.LanguageCount = 1;
erf.header.LocalizedStringSize = erf.descriptions[0].SizeInStream;
erf.header.OffsetToLocalizedString = Marshal.SizeOf(typeof(ErfHeader));
erf.header.OffsetToKeyList = erf.header.OffsetToLocalizedString +
erf.header.LocalizedStringSize;
return erf;
}
#endregion
#region private nested structures
///
/// This structure is the header of the ERF file. It maps directly over
/// the ERF raw data in the file and provides functionality to
/// serialize/deserialize the raw data.
///
[StructLayout(LayoutKind.Sequential, Pack=1, CharSet=CharSet.Ansi)]
private struct ErfHeader
{
#region public properties
///
/// Gets/sets the type of the ERF
///
public ErfType ErfType
{
get
{
return (ErfType) System.Enum.Parse(typeof(ErfType), Type, true);
}
set
{
Type = value.ToString();
}
}
///
/// Gets/sets the type of the ERF. Valid types are
/// "ERF", "MOD", "SAV", "HAK".
///
public string Type
{
get
{
string s = RawSerializer.DeserializeString(type);
return s.Trim();
}
set
{
string s = value.ToUpper().PadRight(4, ' ');
}
}
///
/// Gets the file version as a string, in the format "V1.0".
///
public string VersionText
{
get { return RawSerializer.DeserializeString(version); }
}
///
/// Gets the file version as a double.
///
public double Version
{
get
{
string version = VersionText;
return System.Convert.ToDouble(version.Substring(1, version.Length - 1));
}
}
///
/// Gets the number of different languages that the module description
/// is stored in. There will be 1 description entry for each language.
///
public int LanguageCount
{
get { return languageCount; }
set { languageCount = value; }
}
///
/// Gets/sets the localized string size for the description.
///
public int LocalizedStringSize
{
get { return localizedStringSize; }
set { localizedStringSize = value; }
}
///
/// Gets/sets the number of files in the ERF
///
public int EntryCount
{
get { return entryCount; }
set { entryCount = value; }
}
///
/// Gets/sets the offset to the localized strings
///
public int OffsetToLocalizedString
{
get { return offsetToLocalizedString; }
set { offsetToLocalizedString = value; }
}
///
/// Gets/sets the offset to the key list
///
public int OffsetToKeyList
{
get { return offsetToKeyList; }
set { offsetToKeyList = value; }
}
///
/// Gets/sets the offset to the resource list
///
public int OffsetToResourceList
{
get { return offsetToResourceList; }
set { offsetToResourceList = value; }
}
///
/// Gets/sets the build year
///
public int BuildYear
{
get { return buildYear + 1900; }
set { buildYear = value - 1900; }
}
///
/// Gets/sets the build day
///
public int BuildDay
{
get { return buildDay; }
set { buildDay = value; }
}
///
/// Gets/sets the tlk strref for the file description
///
public int DescriptionStrRef
{
get { return descriptionStrRef; }
set { descriptionStrRef = value; }
}
#endregion
#region public methods
///
/// Constructur to deserialize the ErfHeader from a stream.
///
///
public ErfHeader(Stream s)
{
// Let the raw serializer do the real work then just convert the
// returned object to an ErfHeader.
object o = RawSerializer.Deserialize(typeof(ErfHeader), s);
if (null == o) throw new NWNException("Invalid Header in stream");
this = (ErfHeader) o;
}
///
/// Class constructor.
///
/// The type of the ERF file
public ErfHeader(ErfType erfType)
{
string s = erfType.ToString();
type = new byte[] { (byte) s[0], (byte) s[1], (byte) s[2], (byte) ' ' };
version = new byte[] { (byte) 'V', (byte) '1', (byte) '.', (byte) '0' };
languageCount = 0;
localizedStringSize = 0;
entryCount = 0;
offsetToLocalizedString = 0;
offsetToKeyList = 0;
offsetToResourceList = 0;
buildYear = DateTime.Today.Year;
buildDay = DateTime.Today.DayOfYear;
descriptionStrRef = 0;
pad = new byte[116];
}
///
/// Serializes the ErfHeader to a stream.
///
/// The stream to serialize to.
public void Serialize(Stream s)
{
RawSerializer.Serialize(s, this);
}
#endregion
#region private fields/properties/methods
[MarshalAs(UnmanagedType.ByValArray, SizeConst=4)] private byte[] type;
[MarshalAs(UnmanagedType.ByValArray, SizeConst=4)] private byte[] version;
private Int32 languageCount;
private Int32 localizedStringSize;
private Int32 entryCount;
private Int32 offsetToLocalizedString;
private Int32 offsetToKeyList;
private Int32 offsetToResourceList;
private Int32 buildYear;
private Int32 buildDay;
private Int32 descriptionStrRef;
[MarshalAs(UnmanagedType.ByValArray, SizeConst=116)] private byte[] pad;
#endregion
}
///
/// This structure is a key entry in the ERF file. A key defines a single
/// resource (i.e. file) in the ERF. It maps directly over
/// the ERF raw data in the file and provides functionality to
/// serialize/deserialize the raw data.
///
[StructLayout(LayoutKind.Sequential, Pack=1, CharSet=CharSet.Ansi)]
private struct ErfKey
{
#region public properties/methods
///
/// Gets the file name of the key.
///
public string FileName
{
get
{
// If the resource type is invalid then return an empty string.
if (ResType.Invalid == this.ResType) return string.Empty;
if (0 == ResRef.Length) return string.Empty;
// Convert the restype to a string to get the extension,
// if it is an unknown extension then arbitrarily use
// "ResUNK" to get "UNK" as the extension.
string restype = this.ResType.ToString();
if (restype.Length < 6) restype = "ResUNK";
System.Text.StringBuilder b = new System.Text.StringBuilder(32);
b.Append(ResRef);
b.Append(".");
b.Append(restype, 3, 3);
return b.ToString();
}
}
///
/// Gets the keys's ResRef
///
public string ResRef { get { return RawSerializer.DeserializeString(resRef); } }
///
/// Gets the key's ResType
///
public ResType ResType { get { return (ResType) resType; } }
///
/// Constructor to deserialize an ErfKey from a stream.
///
/// The stream
public ErfKey(Stream s)
{
// Let the raw serializer do the real work then just convert the
// returned object to an ErfHeader.
object o = RawSerializer.Deserialize(typeof(ErfKey), s);
if (null == o) throw new NWNException("Invalid key in stream");
this = (ErfKey) o;
}
///
/// Constructor to create a key from a file.
///
/// The name of the file
public ErfKey(string fileName)
{
unused = 0;
resourceID = 0;
// Generate the ResType of the file based on it's extension, then
// save the Int16 version of that value in resType.
FileInfo info = new FileInfo(fileName);
string resource = "Res" + info.Extension.Substring(1, info.Extension.Length - 1);
resType = (Int16) (ResType) Enum.Parse(typeof(ResType), resource, true);
// Strip the extension from the file name and that is the ResRef of the
// file.
resRef = RawSerializer.SerializeString(
Path.GetFileNameWithoutExtension(fileName).ToLower(), 16, false);
}
///
/// Serializes the ErfKey to a stream.
///
/// The stream to serialize to.
public void Serialize(Stream s)
{
RawSerializer.Serialize(s, this);
}
#endregion
#region public static methods
///
/// Deserializes an array of ErfKey structures from the stream.
///
/// The stream
/// The number of keys to deserialize
/// An array of ErfKey structures
public static ErfKey[] Deserialize(Stream s, int count)
{
// Create an array of ErfKeys from the stream.
ErfKey[] keys = new ErfKey[count];
for (int i = 0; i < count; i++)
keys[i] = new ErfKey(s);
return keys;
}
///
/// Serializes an ErfKey array to the stream.
///
/// The stream
/// The array to serialize
public static void Serlialize(Stream s, ErfKey[] keys)
{
// Loop through the keys, assigning them a resource ID
// (it's just the array index) and then serializing them.
for (int i = 0; i < keys.Length; i++)
{
keys[i].resourceID = i;
keys[i].Serialize(s);
}
}
#endregion
#region private fields/properties/methods
[MarshalAs(UnmanagedType.ByValArray, SizeConst=16)] private byte[] resRef;
private Int32 resourceID;
private Int16 resType;
private Int16 unused;
#endregion
}
///
/// This structure is a resource entry in the ERF file. A resource defines
/// the location and size of the file data within the ERF. It maps directly over
/// the ERF raw data in the file and provides functionality to
/// serialize/deserialize the raw data.
///
[StructLayout(LayoutKind.Sequential, Pack=1, CharSet=CharSet.Ansi)]
private struct ErfResource
{
#region public properties/methods
///
/// Gets/sets the offset to the resource data in the ERF.
///
public int OffsetToResource
{
get { return offsetToResource; }
set { offsetToResource = value; }
}
///
/// Gets/sets the size of the resource data in the ERF.
///
public int ResourceSize
{
get { return resourceSize; }
set { resourceSize = value; }
}
///
/// Constructor to deserialize an ErfResource from a stream.
///
/// The stream
public ErfResource(Stream s)
{
// Let the raw serializer do the real work then just convert the
// returned object to an ErfHeader.
object o = RawSerializer.Deserialize(typeof(ErfResource), s);
if (null == o) throw new NWNException("Invalid resource in stream");
this = (ErfResource) o;
}
///
/// Serializes the ErfResource to a stream.
///
/// The stream to serialize to.
public void Serialize(Stream s)
{
RawSerializer.Serialize(s, this);
}
#endregion
#region public static methods
///
/// Deserializes an array of ErfResource structures from the stream.
///
/// The stream
/// The number of resources to deserialize
/// An array of ErfResource structures
public static ErfResource[] Deserialize(Stream s, int count)
{
// Create an array of ErfKeys from the stream.
ErfResource[] resources = new ErfResource[count];
for (int i = 0; i < count; i++)
resources[i] = new ErfResource(s);
return resources;
}
///
/// Serializes an ErfResource array to the stream.
///
/// The stream
/// The array to serialize
public static void Serlialize(Stream s, ErfResource[] resources)
{
// Loop through the resources serializing them.
for (int i = 0; i < resources.Length; i++)
resources[i].Serialize(s);
}
#endregion
#region private fields/properties/methods
private Int32 offsetToResource;
private Int32 resourceSize;
#endregion
}
#endregion
#region private fields/properties/methods
private string decompressedPath;
private FileInfo fileInfo;
private ErfHeader header;
private ErfString[] descriptions;
private ErfKey[] keys;
private ErfResource[] resources;
private StringCollection removedFiles;
private Hashtable keyHash;
private Hashtable addedFileHash;
private Hashtable replacedFileHash;
///
/// Gets the key for a given file name.
///
/// The file name
/// The key for the file
private string GetKey(string fileName)
{
return Path.GetFileName(fileName).ToLower();
}
///
/// This method reads the given file into the passed stream,
/// setting the passed ErfResource's offset/and size as appropriate.
///
///
///
///
private void ReadFileIntoStream(string file, ref ErfResource resource,
Stream buffer)
{
using (FileStream reader = new FileStream(file, FileMode.Open))
{
// Read the source file.
byte[] bytes = new byte[reader.Length];
reader.Read(bytes, 0, bytes.Length);
// Write the bytes to the buffer
long pos = buffer.Position;
buffer.Write(bytes, 0, bytes.Length);
// Update the ErfResource with the offset and size
resource.OffsetToResource = (int) pos;
resource.ResourceSize = bytes.Length;
}
}
///
/// This method gets the file extension for a given ResType.
///
/// The type to get the extension for
/// The extension for the given type
private string GetFileExtension(ResType type)
{
// Invalid has no file extension
if (ResType.Invalid == type) return string.Empty;
// Convert the ResType to a string and return the last 3
// characters, this is the file extension.
return type.ToString().Substring(3, 3).ToLower();
}
///
/// This method gets the ResType of the given file.
///
/// The path/name of the file
/// The ResType of the file or ResType.Invalid if the
/// file's extension is unknown
private ResType GetTypeOfFile(string fileName)
{
try
{
// Get the file's extension and add "Res" to it and convert that text
// to the enum value.
string extension = Path.GetExtension(fileName).Substring(1, 3).ToUpper();
return (ResType) System.Enum.Parse(typeof(ResType), "Res" + extension, true);
}
catch (Exception)
{
// If we get an exception then the file is unsupported, return invalid.
return ResType.Invalid;
}
}
#endregion
}
}