using System; using System.Collections; using System.Collections.Specialized; using System.IO; using System.Runtime.InteropServices; namespace NWN.FileTypes { /// /// This class represents a tlk file. It contains all of the functionality necessary to /// merge tlk files. /// public class Tlk { #region public nested classes /// /// Flags for the ResRef data. /// [Flags] public enum ResRefFlags: int { None = 0x0000, TextPresent = 0x0001, SoundPresent = 0x0002, SoundLengthPresent = 0x0004, } /// /// This class defines an entry in the tlk file. It contains all of the data for /// the entry. /// public class TlkEntry { #region public properties/methods /// /// Gets/sets the entrie's flags /// public ResRefFlags Flags { get { return flags; } set { flags = value; } } /// /// Gets/sets the sound ResRef /// public string SoundResRef { get { return soundResRef; } set { soundResRef = value; if (string.Empty != soundResRef) flags |= ResRefFlags.SoundPresent; else flags &= ~ResRefFlags.SoundPresent; } } /// /// Gets/sets the volume variance. /// public int VolumnVariance { get { return volumeVariance; } set { volumeVariance = value; } } /// /// Gets/sets the pitch variance. /// public int PitchVariance { get { return pitchVariance; } set { pitchVariance = value; } } /// /// Gets/sets the sound length. /// public float SoundLength { get { return soundLength; } set { soundLength = value; if (0 != soundLength) flags |= ResRefFlags.SoundLengthPresent; else flags &= ~ResRefFlags.SoundLengthPresent; } } /// /// Gets/sets the entry text. /// public string Text { get { return text; } set { text = value; if (string.Empty != text) flags |= ResRefFlags.TextPresent; else flags &= ~ResRefFlags.TextPresent; } } /// /// Returns true if the tlk entry is empty. /// public bool IsEmpty { get { return flags == ResRefFlags.None; } } /// /// Default constructor /// public TlkEntry() { flags = ResRefFlags.None; volumeVariance = 0; pitchVariance = 0; soundLength = 0; soundResRef = string.Empty;; this.text = string.Empty; } /// /// Constuctor to create an entry for a string. /// /// The text. public TlkEntry(string text) { flags = ResRefFlags.TextPresent; volumeVariance = 0; pitchVariance = 0; soundLength = 0; soundResRef = string.Empty; this.text = text; } #endregion #region private fields/properties/methods private ResRefFlags flags; public int volumeVariance; public int pitchVariance; public float soundLength; public string soundResRef; public string text; #endregion } #endregion #region public properties /// /// Gets the tlk key to be used in dictionary lookups, this is a lower case version /// of the name. /// public string Key { get { return name.ToLower(); } } /// /// Gets the name of the tlk file, the name does not include any path information. /// public string Name { get { return name; } } /// /// Gets the number of entries in the tlk file. /// public int Count { get { return header.stringCount; } } /// /// Does a lookup in the tlk file, returning the entry for the specified index. /// public TlkEntry this[int index] { get { // If the index is out of range return null. if (index >= header.stringCount || index < 0) return null; // Create a TlkEntry for the entry TlkEntry entry = new TlkEntry(); entry.PitchVariance = resRefs[index].pitchVariance; entry.SoundLength = resRefs[index].soundLength; entry.VolumnVariance = resRefs[index].volumeVariance; entry.SoundResRef = resRefs[index].soundResRef; entry.Text = strings[index]; entry.Flags = (ResRefFlags) resRefs[index].flags; // Return the created entry. return entry; } set { // Make sure the index is within range. if (index >= header.stringCount || index < 0) throw new IndexOutOfRangeException(); resRefs[index].pitchVariance = value.PitchVariance; resRefs[index].soundLength = value.SoundLength; resRefs[index].volumeVariance = value.VolumnVariance; resRefs[index].soundResRef = value.SoundResRef; resRefs[index].flags = (int) value.Flags; strings[index] = value.Text; } } #endregion #region public methods /// /// Default constructor. /// /// The initial number of entries in the tlk file. public Tlk(int count) { name = string.Empty; header = new TlkHeader(); resRefs = new RawResRef[count]; strings = new string[count]; header.fileType = tlkFile; header.fileVersion = tlkVersion; header.stringCount = count; header.stringOffset = 0; for (int i = 0; i < count; i++) { resRefs[i] = new RawResRef(); resRefs[i].flags = (int) ResRefFlags.None; resRefs[i].offsetToString = 0; resRefs[i].pitchVariance = 0; resRefs[i].soundLength = 0; resRefs[i].soundResRef = string.Empty; resRefs[i].stringSize = 0; resRefs[i].volumeVariance = 0; strings[i] = string.Empty; } } /// /// Returns true if the index'th entry is empty. /// /// The index of the entry to test /// True if the entry is empty false if it is not public bool IsEmpty(int index) { if (index >= header.stringCount || index < 0) throw new ArgumentOutOfRangeException(); return (int) ResRefFlags.None == resRefs[index].flags || string.Empty == strings[index]; } /// /// Pads the tlk to have at least the specified number of entries. If the tlk file /// has less entries than what is given, blank entries are inserted to pad. /// /// The new number of entries public void Pad(int length) { // If the tlk file is larger than the pad count then do nothing. if (header.stringCount >= length) return; // Add blank entries to the tlk file to pad. RawResRef[] padded = new RawResRef[length]; resRefs.CopyTo(padded, 0); for (int i = resRefs.Length; i < padded.Length; i++) { padded[i].flags = 0; padded[i].offsetToString = 0; padded[i].pitchVariance = 0; padded[i].stringSize = 0; padded[i].volumeVariance = 0; padded[i].soundLength = 0.0f; padded[i].soundResRef = string.Empty; } string[] paddedStrings = new string[length]; paddedStrings.CopyTo(strings, 0); for (int i = strings.Length; i < paddedStrings.Length; i++) paddedStrings[i] = string.Empty; // Save the new RawResRef array and update the number of entries in // the header. header.stringCount = length; resRefs = padded; strings = paddedStrings; } /// /// Saves the tlk file, the tlk file must have been given a name or this will /// throw an InvalidOperationException. /// public void Save() { if (string.Empty == name) throw new InvalidOperationException(); SaveAs(name); } /// /// Saves the tlk file under the specified file name. /// /// The name in which to save the file. public void SaveAs(string fileName) { using (FileStream writer = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None)) { // Figure out how big of a buffer we need to store all of the string data. int count = 0; foreach (string s in strings) count += s.Length; // Copy all of the string data to a byte array. int stringDataIndex = 0; byte[] stringData = new byte[count]; for (int i = 0; i < resRefs.Length; i++) { // Ignore entries w/o strings. if (0 == ((int) ResRefFlags.TextPresent & resRefs[i].flags) || string.Empty == strings[i]) { // Blank the string size and offset just in case to keep // the tlk file clean. resRefs[i].stringSize = 0; resRefs[i].offsetToString = 0; continue; } // Copy the string bytes to the byte array. for (int j = 0; j < strings[i].Length; j++) stringData[stringDataIndex + j] = (byte) strings[i][j]; // Save the string offset and size in the ResRef structure. resRefs[i].offsetToString = stringDataIndex; resRefs[i].stringSize = strings[i].Length; // Increment the buffer index to the next free byte. stringDataIndex += strings[i].Length; } // Set the offset to the string data in the header and write the header out. header.stringOffset = headerSize + (resRefs.Length * RawResRefSize); byte[] buffer = RawSerialize(header); writer.Write(buffer, 0, buffer.Length); // Write all of the ResRef entries out. for (int i = 0; i < resRefs.Length; i++) { buffer = RawSerialize(resRefs[i]); writer.Write(buffer, 0, buffer.Length); } // Write the raw string data out. writer.Write(stringData, 0, stringData.Length); } this.name = fileName; } #endregion #region public static methods /// /// Creates a Tlk object for the specified tlk file. /// /// The tlk file /// A Tlk object representing the tlk file public static Tlk LoadTlk(string fileName) { // Open the tlk file. Tlk tlk = new Tlk(); using (FileStream reader = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read)) { // Save the name of the tlk file. FileInfo info = new FileInfo(fileName); tlk.name = info.Name; // Read the header and decode it. byte[] buffer = new byte[Tlk.headerSize]; if (reader.Read(buffer, 0, buffer.Length) != buffer.Length) ThrowException("Tlk file {0} is corrupt", fileName); tlk.DeserializeHeader(buffer); // Do a reality check on the tlk file. if (tlk.header.fileType != tlkFile) ThrowException("{0} is not a tlk file", fileName); if (tlk.header.fileVersion != tlkVersion) ThrowException("{0} is an unsupported tlk file", fileName); // Read the RawResRef array and decode it. int size = tlk.header.stringCount * Tlk.RawResRefSize; buffer = new byte[size]; if (reader.Read(buffer, 0, buffer.Length) != buffer.Length) ThrowException("Tlk file {0} is corrupt", fileName); tlk.DeserializeRawResRefs(buffer); // Read the raw string data. buffer = new byte[reader.Length - tlk.header.stringOffset]; if (reader.Read(buffer, 0, buffer.Length) != buffer.Length) ThrowException("Tlk file {0} is corrupt", fileName); // Load the strings from the raw bytes into our string array. tlk.strings = new string[tlk.header.stringCount]; for (int i = 0; i < tlk.header.stringCount; i++) tlk.strings[i] = tlk.GetStringFromBuffer(buffer, i); } return tlk; } /// /// Merges 2 tlk objects, saving the results in the specified tlk file. The merge tlk file /// is just added to the end of the source tlk file. Unlike 2da merging, the merge tlk /// does not overwrite any entries in the source tlk. Tlk entries are not row critical like /// 2da rows (since tlk strings are not saved in character files), so exact positioning of /// them is not as critical. /// /// The source tlk /// The merge tlk /// The name of the output tlk file /// The offset of the first entry of the merge tlk in the output file. This offset /// can be used to fixup 2da entries that refer to the merge tlk. public static int MergeTlk(Tlk source, Tlk merge, string outFile) { /* // Open the output tlk. using (FileStream writer = new FileStream(outFile, FileMode.Create, FileAccess.Write, FileShare.None)) { // Build a RawResRef array containing both the source and merge RawResRef arrays. Then // loop through all of the merge entries and fixup the string offsets to point // past the source data to the merge data, which we will glue on the end of the // source data. RawResRef[] outResRefs = new RawResRef[source.resRefs.Length + merge.resRefs.Length]; source.resRefs.CopyTo(outResRefs, 0); merge.resRefs.CopyTo(outResRefs, source.resRefs.Length); for (int i = source.resRefs.Length; i < outResRefs.Length; i++) { if (0 != (outResRefs[i].flags & (int) ResRefFlags.textPresent)) outResRefs[i].offsetToString += source.stringBytes.Length; } // Build a header with a string count of all of the source + merge strings, then // write it out. TlkHeader headerOut = source.header; headerOut.stringCount += merge.header.stringCount; headerOut.stringOffset = headerSize + (outResRefs.Length * RawResRefSize); byte[] headerBytes = RawSerialize(headerOut); writer.Write(headerBytes, 0, headerBytes.Length); // Write the RawResRef data. for (int i = 0; i < outResRefs.Length; i++) { byte[] bytes = RawSerialize(outResRefs[i]); writer.Write(bytes, 0, bytes.Length); } // Write the source and merge string data out to the file. writer.Write(source.stringBytes, 0, source.stringBytes.Length); writer.Write(merge.stringBytes, 0, merge.stringBytes.Length); writer.Flush(); writer.Close(); } // Return the number of strings in the source tlk. Since we glued the merge // tlk onto the end of the source tlk, this value will be the fixup value we need // to correct 2da tlk references. return source.header.stringCount; */ return 0; } #endregion #region private methods /// /// Private constructor, instances of this class must /// private Tlk() { header = new TlkHeader(); resRefs = null; strings = null; } /// /// Gets the index'th string from the raw string buffer. /// /// The raw string buffer /// Index of the string to get /// The index'th string private string GetStringFromBuffer(byte[] buffer, int index) { // If the index is out of range or the text present flag is not set then // return an empty string. if (index >= header.stringCount || 0 == (resRefs[index].flags & (int) ResRefFlags.TextPresent)) return string.Empty;; // Can't find a converter to build a string from a byte array, so we // need a local char array to do the dirty work. int offset = resRefs[index].offsetToString; char[] chars = new char[resRefs[index].stringSize]; for (int i = 0; i < chars.Length; i++) chars[i] = (char) buffer[i + offset]; return new string(chars); } /// /// Deserializes the header from the given byte array. /// /// The byte array containing the header private void DeserializeHeader(byte[] bytes) { // Alloc a hglobal to store the bytes. IntPtr buffer = Marshal.AllocHGlobal(bytes.Length); try { // Copy the data to unprotected memory, then convert it to a TlkHeader // structure Marshal.Copy(bytes, 0, buffer, bytes.Length); object o = Marshal.PtrToStructure(buffer, typeof(TlkHeader)); header = (TlkHeader) o; } finally { // Free the hglobal before exiting. Marshal.FreeHGlobal(buffer); } } /// /// Deserializes the RawResRef array from the given byte array. /// /// private void DeserializeRawResRefs(byte[] bytes) { // Alloc a hglobal to store the bytes. IntPtr buffer = Marshal.AllocHGlobal(RawResRefSize); try { // Create a RawResRef array for all of the entries and loop // through the array populating it. resRefs = new RawResRef[header.stringCount]; for (int i = 0; i < resRefs.Length; i++) { // Copy the bytes of the i'th RawResRef to unprotected memory // and convert it to a RawResRef structure. Marshal.Copy(bytes, i * RawResRefSize, buffer, RawResRefSize); object o = Marshal.PtrToStructure(buffer, typeof(RawResRef)); resRefs[i] = (RawResRef) o; } } finally { // Free the hglobal before exiting. Marshal.FreeHGlobal(buffer); } } #endregion #region private static methods /// /// Throws an NWNException exception /// /// The message format string /// Message arguments private static void ThrowException(string format, params object[] args) { throw new NWNException(format, args); } /// /// This method serializes an arbitrary object to a byte array for storage in /// a tlk file. The object should have it's data mapped to proper positions /// or the serialization won't work. /// /// The object to serialize /// private static byte[] RawSerialize(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); } } #endregion #region private nested structures/classes /// /// Structure to store the header of the tlk file. This is mapped directly over the /// bytes loaded from the tlk file, so we need to declare the mapping explicitly. /// Using LayoutKind.Sequential should work, but I had problems with it so used /// Explicit to force the issue. fileType and fileVersions are really strings, but /// .NET insists on null terminating strings, and these strings are not null terminated. /// They are both 4 bytes so using Int32 works. /// [StructLayout(LayoutKind.Explicit, Pack=1, CharSet=CharSet.Ansi)] private struct TlkHeader { [FieldOffsetAttribute(0)] public Int32 fileType; [FieldOffsetAttribute(4)] public Int32 fileVersion; [FieldOffsetAttribute(8)] public Int32 language; [FieldOffsetAttribute(12)] public Int32 stringCount; [FieldOffsetAttribute(16)] public Int32 stringOffset; } /// /// Structure to represent a RawResRef entry in a tlk file. This is mapped directly over the /// bytes loaded from the tlk file, so we need to declare the mapping explicitly. /// [StructLayout(LayoutKind.Sequential, Pack=1, CharSet=CharSet.Ansi)] private struct RawResRef { public Int32 flags; [MarshalAs(UnmanagedType.ByValTStr, SizeConst=16)] public String soundResRef; public Int32 volumeVariance; public Int32 pitchVariance; public Int32 offsetToString; public Int32 stringSize; public float soundLength; } #endregion #region private fields private const int headerSize = 20; private const int RawResRefSize = 40; private const Int32 tlkFile = 0x204b4c54; private const Int32 tlkVersion = 0x302e3356; private string name; private TlkHeader header; private RawResRef[] resRefs; private string[] strings; #endregion } }