using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; using Oni.Collections; using Oni.Metadata; namespace Oni { internal sealed class InstanceFileWriter { #region Private data private static readonly byte[] padding = new byte[512]; private static byte[] copyBuffer1 = new byte[32768]; private static byte[] copyBuffer2 = new byte[32768]; private StreamCache streamCache; private readonly bool bigEndian; private readonly Dictionary namedInstancedIdMap; private readonly FileHeader header; private readonly List descriptorTable; private NameDescriptorTable nameIndex; private TemplateDescriptorTable templateTable; private NameTable nameTable; private readonly Dictionary sharedMap; private readonly Dictionary linkMaps; private readonly Dictionary> rawOffsetMaps; private readonly Dictionary> sepOffsetMaps; private int rawOffset; private int sepOffset; private List rawParts; private List sepParts; #endregion #region private class FileHeader private class FileHeader { public const int Size = 64; public long TemplateChecksum; public int Version; public int InstanceCount; public int NameCount; public int TemplateCount; public int DataTableOffset; public int DataTableSize; public int NameTableOffset; public int NameTableSize; public int RawTableOffset; public int RawTableSize; public void Write(BinaryWriter writer) { writer.Write(TemplateChecksum); writer.Write(Version); writer.Write(InstanceFileHeader.Signature); writer.Write(InstanceCount); writer.Write(NameCount); writer.Write(TemplateCount); writer.Write(DataTableOffset); writer.Write(DataTableSize); writer.Write(NameTableOffset); writer.Write(NameTableSize); writer.Write(RawTableOffset); writer.Write(RawTableSize); writer.Write(0); writer.Write(0); } } #endregion #region private class DescriptorTableEntry private class DescriptorTableEntry { public const int Size = 20; public readonly InstanceDescriptor SourceDescriptor; public readonly int Id; public int DataOffset; public int NameOffset; public int DataSize; public bool AnimationPositionPointHack; public DescriptorTableEntry(int id, InstanceDescriptor descriptor) { Id = id; SourceDescriptor = descriptor; } public bool HasName => SourceDescriptor.HasName; public string Name => SourceDescriptor.FullName; public TemplateTag Code => SourceDescriptor.Template.Tag; public InstanceFile SourceFile => SourceDescriptor.File; public void Write(BinaryWriter writer, bool shared) { writer.Write((int)Code); writer.Write(DataOffset); writer.Write(NameOffset); writer.Write(DataSize); var flags = InstanceDescriptorFlags.None; if (!SourceDescriptor.HasName) flags |= InstanceDescriptorFlags.Private; if (DataOffset == 0) flags |= InstanceDescriptorFlags.Placeholder; if (shared) flags |= InstanceDescriptorFlags.Shared; writer.Write((int)flags); } } #endregion #region private class NameDescriptorTable private class NameDescriptorTable { private List entries; #region private class Entry private class Entry : IComparable { public const int Size = 8; public int InstanceNumber; public string Name; public void Write(BinaryWriter writer) { writer.Write(InstanceNumber); writer.Write(0); } #region IComparable Members int IComparable.CompareTo(Entry other) { // // Note: Oni is case sensitive so we need to sort names accordingly. // return string.CompareOrdinal(Name, other.Name); } #endregion } #endregion public static NameDescriptorTable CreateFromDescriptors(List descriptorTable) { NameDescriptorTable nameIndex = new NameDescriptorTable(); nameIndex.entries = new List(); for (int i = 0; i < descriptorTable.Count; i++) { DescriptorTableEntry descriptor = descriptorTable[i]; if (descriptor.HasName) { Entry entry = new Entry(); entry.Name = descriptor.Name; entry.InstanceNumber = i; nameIndex.entries.Add(entry); } } nameIndex.entries.Sort(); return nameIndex; } public int Count => entries.Count; public int Size => entries.Count * Entry.Size; public void Write(BinaryWriter writer) { foreach (Entry entry in entries) entry.Write(writer); } } #endregion #region private class TemplateDescriptorTable private class TemplateDescriptorTable { private List entries; #region private class Entry private class Entry : IComparable { public const int Size = 16; public long Checksum; public TemplateTag Code; public int Count; public void Write(BinaryWriter writer) { writer.Write(Checksum); writer.Write((int)Code); writer.Write(Count); } int IComparable.CompareTo(Entry other) => Code.CompareTo(other.Code); } #endregion public static TemplateDescriptorTable CreateFromDescriptors(InstanceMetadata metadata, List descriptorTable) { Dictionary templateCount = new Dictionary(); foreach (DescriptorTableEntry entry in descriptorTable) { int count; templateCount.TryGetValue(entry.Code, out count); templateCount[entry.Code] = count + 1; } TemplateDescriptorTable templateTable = new TemplateDescriptorTable(); templateTable.entries = new List(templateCount.Count); foreach (KeyValuePair pair in templateCount) { Entry entry = new Entry(); entry.Checksum = metadata.GetTemplate(pair.Key).Checksum; entry.Code = pair.Key; entry.Count = pair.Value; templateTable.entries.Add(entry); } templateTable.entries.Sort(); return templateTable; } public int Count => entries.Count; public int Size => entries.Count * Entry.Size; public void Write(BinaryWriter writer) { foreach (Entry entry in entries) entry.Write(writer); } } #endregion #region private class NameTable private class NameTable { private List names; private int size; public static NameTable CreateFromDescriptors(List descriptors) { NameTable nameTable = new NameTable(); nameTable.names = new List(); int nameTableSize = 0; foreach (DescriptorTableEntry descriptor in descriptors) { if (!descriptor.HasName) continue; string name = descriptor.Name; nameTable.names.Add(name); descriptor.NameOffset = nameTableSize; nameTableSize += name.Length + 1; if (name.Length > 63) Console.WriteLine("Warning: name '{0}' too long.", name); } nameTable.size = nameTableSize; return nameTable; } public int Size => size; public void Write(BinaryWriter writer) { byte[] copyBuffer = new byte[256]; foreach (string name in names) { int length = Encoding.UTF8.GetBytes(name, 0, name.Length, copyBuffer, 0); copyBuffer[length] = 0; writer.Write(copyBuffer, 0, length + 1); } } } #endregion #region private class BinaryPartEntry private class BinaryPartEntry : IComparable { public readonly int SourceOffset; public readonly string SourceFile; public readonly int DestinationOffset; public readonly int Size; public readonly BinaryPartField Field; public BinaryPartEntry(string sourceFile, int sourceOffset, int size, int destinationOffset, Field field) { SourceFile = sourceFile; SourceOffset = sourceOffset; Size = size; DestinationOffset = destinationOffset; Field = (BinaryPartField)field; } #region IComparable Members int IComparable.CompareTo(BinaryPartEntry other) { // // Sort the binary parts by destination offset in an attempt to streamline the write IO // return DestinationOffset.CompareTo(other.DestinationOffset); } #endregion } #endregion #region private class ChecksumStream private class ChecksumStream : Stream { private int checksum; private int position; public int Checksum => checksum; public override bool CanRead => false; public override bool CanSeek => false; public override bool CanWrite => true; public override void Flush() { } public override long Length => position; public override long Position { get { return position; } set { throw new NotSupportedException(); } } public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); } public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } public override void SetLength(long value) { throw new NotSupportedException(); } public override void Write(byte[] buffer, int offset, int count) { for (int i = offset; i < offset + count; i++) checksum += buffer[i] ^ (i + position); position += count; } } #endregion #region private class StreamCache private class StreamCache : IDisposable { private const int maxCacheSize = 32; private Dictionary cacheEntries = new Dictionary(); private class CacheEntry { public BinaryReader Stream; public long LastTimeUsed; } public BinaryReader GetReader(InstanceDescriptor descriptor) { CacheEntry entry; if (!cacheEntries.TryGetValue(descriptor.FilePath, out entry)) entry = OpenStream(descriptor); entry.LastTimeUsed = DateTime.Now.Ticks; entry.Stream.Position = descriptor.DataOffset; return entry.Stream; } private CacheEntry OpenStream(InstanceDescriptor descriptor) { CacheEntry oldestEntry = null; string oldestDescriptor = null; if (cacheEntries.Count >= maxCacheSize) { foreach (KeyValuePair pair in cacheEntries) { if (oldestEntry == null || pair.Value.LastTimeUsed < oldestEntry.LastTimeUsed) { oldestDescriptor = pair.Key; oldestEntry = pair.Value; } } } if (oldestEntry == null) { oldestEntry = new CacheEntry(); } else { oldestEntry.Stream.Dispose(); cacheEntries.Remove(oldestDescriptor); } oldestEntry.Stream = new BinaryReader(descriptor.FilePath); cacheEntries.Add(descriptor.FilePath, oldestEntry); return oldestEntry; } public void Dispose() { foreach (CacheEntry entry in cacheEntries.Values) entry.Stream.Dispose(); } } #endregion public static InstanceFileWriter CreateV31(long templateChecksum, bool bigEndian) { return new InstanceFileWriter(templateChecksum, InstanceFileHeader.Version31, bigEndian); } public static InstanceFileWriter CreateV32(List descriptors) { long templateChecksum; if (descriptors.Exists(x => x.Template.Tag == TemplateTag.SNDD && x.IsMacFile)) templateChecksum = InstanceFileHeader.OniMacTemplateChecksum; else templateChecksum = InstanceFileHeader.OniPCTemplateChecksum; var writer = new InstanceFileWriter(templateChecksum, InstanceFileHeader.Version32, false); writer.AddDescriptors(descriptors, false); return writer; } private InstanceFileWriter(long templateChecksum, int version, bool bigEndian) { if (templateChecksum != InstanceFileHeader.OniPCTemplateChecksum && templateChecksum != InstanceFileHeader.OniMacTemplateChecksum && templateChecksum != 0) { throw new ArgumentException("Unknown template checksum", "templateChecksum"); } this.bigEndian = bigEndian; header = new FileHeader { TemplateChecksum = templateChecksum, Version = version }; descriptorTable = new List(); namedInstancedIdMap = new Dictionary(); linkMaps = new Dictionary(); rawOffsetMaps = new Dictionary>(); sepOffsetMaps = new Dictionary>(); sharedMap = new Dictionary(); } public void AddDescriptors(List descriptors, bool removeDuplicates) { if (removeDuplicates) { Console.WriteLine("Removing duplicates"); using (streamCache = new StreamCache()) descriptors = RemoveDuplicates(descriptors); } // // Initialize LinkMap table of each source file. // var inputFiles = new Set(); foreach (var descriptor in descriptors) inputFiles.Add(descriptor.File); foreach (var inputFile in inputFiles) { linkMaps[inputFile] = new int[inputFile.Descriptors.Count]; rawOffsetMaps[inputFile] = new Dictionary(); sepOffsetMaps[inputFile] = new Dictionary(); } foreach (var descriptor in descriptors) { AddDescriptor(descriptor); } CreateHeader(); } private void AddDescriptor(InstanceDescriptor descriptor) { // // SNDD instances are special because they are different between PC // and Mac/PC Demo versions so we need to check if the instance type matches the // output file type. If the file type wasn't specified then we will set it when // the first SNDD instance is seen. // if (descriptor.Template.Tag == TemplateTag.SNDD) { if (header.TemplateChecksum == 0) { header.TemplateChecksum = descriptor.TemplateChecksum; } else if (header.TemplateChecksum != descriptor.TemplateChecksum) { if (header.TemplateChecksum == InstanceFileHeader.OniMacTemplateChecksum) throw new NotSupportedException(string.Format("File {0} cannot be imported due to conflicting template checksums", descriptor.FilePath)); } } // // Create a new id for this descriptor and remember it and the old one // in the LinkMap table of the source file. // int id = MakeInstanceId(descriptorTable.Count); linkMaps[descriptor.File][descriptor.Index] = id; // // If the descriptor has a name we will need to know later what is the new id // for its name. // if (descriptor.HasName) namedInstancedIdMap[descriptor.FullName] = id; // // Create and add new table entry for this descriptor. // var entry = new DescriptorTableEntry(id, descriptor); if (!descriptor.IsPlaceholder) { // // .oni files have only one non empty named descriptor. The rest are // forced to be empty and their contents stored in separate .oni files. // if (!IsV32 || !descriptor.HasName || descriptorTable.Count == 0 || descriptorTable[0].SourceDescriptor == descriptor) { int dataSize = descriptor.DataSize; if (descriptor.Template.Tag == TemplateTag.SNDD && header.TemplateChecksum == InstanceFileHeader.OniPCTemplateChecksum && descriptor.TemplateChecksum == InstanceFileHeader.OniMacTemplateChecksum) { // // HACK: when converting SNDD instances from PC Demo to PC Retail the resulting // data size differs from the original. // dataSize = 0x60; } else if (descriptor.Template.Tag == TemplateTag.AKDA) { dataSize = 0x20; } entry.DataSize = dataSize; entry.DataOffset = header.DataTableSize + 8; header.DataTableSize += entry.DataSize; } } descriptorTable.Add(entry); } private void CreateHeader() { if (header.TemplateChecksum == 0) throw new InvalidOperationException("Target file format was not specified and cannot be autodetected."); header.InstanceCount = descriptorTable.Count; int offset = FileHeader.Size + descriptorTable.Count * DescriptorTableEntry.Size; if (IsV31) { nameIndex = NameDescriptorTable.CreateFromDescriptors(descriptorTable); header.NameCount = nameIndex.Count; offset += nameIndex.Size; templateTable = TemplateDescriptorTable.CreateFromDescriptors( InstanceMetadata.GetMetadata(header.TemplateChecksum), descriptorTable); header.TemplateCount = templateTable.Count; offset += templateTable.Size; header.DataTableOffset = Utils.Align32(offset); nameTable = NameTable.CreateFromDescriptors(descriptorTable); header.NameTableSize = nameTable.Size; header.NameTableOffset = Utils.Align32(header.DataTableOffset + header.DataTableSize); } else { // // .oni files do not need the name index and the template table. // They consume space, complicate things and the information // contained in them can be recreated anyway. // nameTable = NameTable.CreateFromDescriptors(descriptorTable); header.NameTableSize = nameTable.Size; header.NameTableOffset = Utils.Align32(offset); header.DataTableOffset = Utils.Align32(header.NameTableOffset + nameTable.Size); header.RawTableOffset = Utils.Align32(header.DataTableOffset + header.DataTableSize); } } public void Write(string filePath) { string outputDirPath = Path.GetDirectoryName(filePath); Directory.CreateDirectory(outputDirPath); int fileId = IsV31 ? MakeFileId(filePath) : 0; using (streamCache = new StreamCache()) using (var outputStream = new FileStream(filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None, 65536)) using (var writer = new BinaryWriter(outputStream)) { outputStream.Position = FileHeader.Size; foreach (DescriptorTableEntry entry in descriptorTable) { entry.Write(writer, sharedMap.ContainsKey(entry.SourceDescriptor)); } if (IsV31) { nameIndex.Write(writer); templateTable.Write(writer); } else { // // For .oni files write the name table before the data table // for better reading performance at import time. // writer.Position = header.NameTableOffset; nameTable.Write(writer); } WriteDataTable(writer, fileId); if (IsV31) { writer.Position = header.NameTableOffset; nameTable.Write(writer); } WriteBinaryParts(writer, filePath); if (IsV32 && outputStream.Length > header.RawTableOffset) { // // The header was created with a RawTable size of 0 because // we don't know the size in advance. Fix that now. // header.RawTableSize = (int)outputStream.Length - header.RawTableOffset; } outputStream.Position = 0; header.Write(writer); } } private void WriteDataTable(BinaryWriter writer, int fileId) { writer.Position = header.DataTableOffset; // // Raw and sep parts will be added as they are found. The initial offset // is 32 because a 0 offset means "NULL". // rawOffset = 32; rawParts = new List(); sepOffset = 32; sepParts = new List(); var entries = descriptorTable.ToArray(); Array.Sort(entries, (x, y) => x.DataOffset.CompareTo(y.DataOffset)); foreach (var entry in entries) { if (entry.DataSize == 0) continue; int padSize = header.DataTableOffset + entry.DataOffset - 8 - writer.Position; if (padSize <= 512) writer.Write(padding, 0, padSize); else writer.Position = header.DataTableOffset + entry.DataOffset - 8; writer.Write(entry.Id); writer.Write(fileId); var template = entry.SourceDescriptor.Template; if (template.Tag == TemplateTag.SNDD && entry.SourceDescriptor.File.Header.TemplateChecksum == InstanceFileHeader.OniMacTemplateChecksum && header.TemplateChecksum == InstanceFileHeader.OniPCTemplateChecksum) { // // Special case to convert PC Demo SNDD files to PC Retail SNDD files. // ConvertSNDDHack(entry, writer); } else { template.Type.Copy(streamCache.GetReader(entry.SourceDescriptor), writer, state => { if (state.Type == MetaType.RawOffset) RemapRawOffset(entry, state); else if (state.Type == MetaType.SepOffset) RemapSepOffset(entry, state); else if (state.Type is MetaPointer) RemapLinkId(entry, state); }); if (entry.Code == TemplateTag.TXMP) { // // HACK: All .oni files use the PC format except the SNDD ones. Most // differences between PC and Mac formats are handled by the metadata // but the TXMP is special because the raw/sep offset field is at a // different offset. // ConvertTXMPHack(entry, writer.BaseStream); } } } } private void ConvertSNDDHack(DescriptorTableEntry entry, BinaryWriter writer) { var reader = streamCache.GetReader(entry.SourceDescriptor); int flags = reader.ReadInt32(); int duration = reader.ReadInt32(); int dataSize = reader.ReadInt32(); int dataOffset = reader.ReadInt32(); int channelCount = (flags == 3) ? 2 : 1; writer.Write(8); writer.WriteInt16(2); writer.WriteInt16(channelCount); writer.Write(22050); writer.Write(11155); writer.WriteInt16(512); writer.WriteInt16(4); writer.WriteInt16(32); writer.Write(new byte[] { 0xf4, 0x03, 0x07, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x40, 0x00, 0xf0, 0x00, 0x00, 0x00, 0xcc, 0x01, 0x30, 0xff, 0x88, 0x01, 0x18, 0xff }); writer.Write((short)duration); writer.Write(dataSize); writer.Write(RemapRawOffsetCore(entry, dataOffset, null)); } private void ConvertTXMPHack(DescriptorTableEntry entry, Stream stream) { stream.Position = header.DataTableOffset + entry.DataOffset + 0x80; stream.Read(copyBuffer1, 0, 28); if (header.TemplateChecksum == InstanceFileHeader.OniPCTemplateChecksum) { // // Swap Bytes is always set for PC files. // copyBuffer1[1] |= 0x10; } else if (IsV31 && header.TemplateChecksum == InstanceFileHeader.OniMacTemplateChecksum) { // // Swap Bytes if always set for MacPPC files except if the format is RGBA // which requires no conversion. // if (bigEndian && copyBuffer1[8] == (byte)Motoko.TextureFormat.RGBA) copyBuffer1[1] &= 0xef; else copyBuffer1[1] |= 0x10; } if (entry.SourceDescriptor.TemplateChecksum != header.TemplateChecksum) { // // Swap the 0x94 and 0x98 fields to convert between Mac and PC TXMPs // for (int i = 20; i < 24; i++) { byte b = copyBuffer1[i]; copyBuffer1[i] = copyBuffer1[i + 4]; copyBuffer1[i + 4] = b; } } stream.Position = header.DataTableOffset + entry.DataOffset + 0x80; stream.Write(copyBuffer1, 0, 28); } private bool ZeroTRAMPositionPointsHack(DescriptorTableEntry entry, CopyVisitor state) { if (entry.Code != TemplateTag.TRAM) return false; int offset = state.GetInt32(); if (state.Position == 0x04) { entry.AnimationPositionPointHack = (offset == 0); } else if (state.Position == 0x28 && entry.AnimationPositionPointHack) { if (offset != 0) { InstanceFile input = entry.SourceFile; int size = input.GetRawPartSize(offset); offset = AllocateRawPart(null, 0, size, null); state.SetInt32(offset); } return true; } return false; } private void RemapRawOffset(DescriptorTableEntry entry, CopyVisitor state) { if (ZeroTRAMPositionPointsHack(entry, state)) return; state.SetInt32(RemapRawOffsetCore(entry, state.GetInt32(), state.Field)); } private int RemapRawOffsetCore(DescriptorTableEntry entry, int oldOffset, Field field) { if (oldOffset == 0) return 0; InstanceFile input = entry.SourceFile; Dictionary rawOffsetMap = rawOffsetMaps[input]; int newOffset; if (!rawOffsetMap.TryGetValue(oldOffset, out newOffset)) { int size = input.GetRawPartSize(oldOffset); // // .oni files are always in PC format (except SNDD files) so when importing // to a Mac file we need to allocate some binary parts in the sep file // instead of the raw file. // if (header.TemplateChecksum == InstanceFileHeader.OniMacTemplateChecksum && (entry.Code == TemplateTag.TXMP || entry.Code == TemplateTag.OSBD || entry.Code == TemplateTag.BINA)) { newOffset = AllocateSepPart(input.RawFilePath, oldOffset + input.Header.RawTableOffset, size, null); } else { newOffset = AllocateRawPart(input.RawFilePath, oldOffset + input.Header.RawTableOffset, size, field); } rawOffsetMap[oldOffset] = newOffset; } return newOffset; } private void RemapSepOffset(DescriptorTableEntry entry, CopyVisitor state) { int oldOffset = state.GetInt32(); if (oldOffset == 0) return; InstanceFile input = entry.SourceFile; Dictionary sepOffsetMap = sepOffsetMaps[input]; int newOffset; if (!sepOffsetMap.TryGetValue(oldOffset, out newOffset)) { int size = input.GetSepPartSize(oldOffset); // // If we're writing a PC file then there is no sep file, everything gets allocated // in the raw file // if (header.TemplateChecksum == InstanceFileHeader.OniPCTemplateChecksum) newOffset = AllocateRawPart(input.SepFilePath, oldOffset, size, null); else newOffset = AllocateSepPart(input.SepFilePath, oldOffset, size, null); sepOffsetMap[oldOffset] = newOffset; } state.SetInt32(newOffset); } private int AllocateRawPart(string sourceFile, int sourceOffset, int size, Field field) { var entry = new BinaryPartEntry(sourceFile, sourceOffset, size, rawOffset, field); rawOffset = Utils.Align32(rawOffset + size); rawParts.Add(entry); return entry.DestinationOffset; } private int AllocateSepPart(string sourceFile, int sourceOffset, int size, Field field) { var entry = new BinaryPartEntry(sourceFile, sourceOffset, size, sepOffset, field); sepOffset = Utils.Align32(sepOffset + size); sepParts.Add(entry); return entry.DestinationOffset; } private void RemapLinkId(DescriptorTableEntry entry, CopyVisitor state) { int oldId = state.GetInt32(); if (oldId != 0) { int newId = RemapLinkIdCore(entry.SourceDescriptor, oldId); state.SetInt32(newId); } } private int RemapLinkIdCore(InstanceDescriptor descriptor, int id) { var file = descriptor.File; if (IsV31) { InstanceDescriptor oldDescriptor = file.GetDescriptor(id); InstanceDescriptor newDescriptor; int newDescriptorId; if (oldDescriptor.HasName) { // // Always lookup named instances, this deals with cases where an instance from one source file // is replaced by one from another source file. // if (namedInstancedIdMap.TryGetValue(oldDescriptor.FullName, out newDescriptorId)) return newDescriptorId; } if (sharedMap.TryGetValue(oldDescriptor, out newDescriptor)) return linkMaps[newDescriptor.File][newDescriptor.Index]; } return linkMaps[file][id >> 8]; } private void WriteBinaryParts(BinaryWriter writer, string filePath) { if (IsV32) { // // For .oni files the raw/sep parts are written to the .oni file. // Separate .raw/.sep files are not used. // WriteParts(writer, rawParts); return; } string rawFilePath = Path.ChangeExtension(filePath, ".raw"); Console.WriteLine("Writing {0}", rawFilePath); using (var rawOutputStream = new FileStream(rawFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 65536)) using (var rawWriter = new BinaryWriter(rawOutputStream)) { WriteParts(rawWriter, rawParts); } if (header.TemplateChecksum == InstanceFileHeader.OniMacTemplateChecksum) { // // Only Mac/PC Demo files have a .sep file. // string sepFilePath = Path.ChangeExtension(filePath, ".sep"); Console.WriteLine("Writing {0}", sepFilePath); using (var sepOutputStream = new FileStream(sepFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 65536)) using (var sepWriter = new BinaryWriter(sepOutputStream)) { WriteParts(sepWriter, sepParts); } } } private void WriteParts(BinaryWriter writer, List binaryParts) { if (binaryParts.Count == 0) { writer.Write(padding, 0, 32); return; } binaryParts.Sort(); int fileLength = 0; foreach (BinaryPartEntry entry in binaryParts) { if (entry.DestinationOffset + entry.Size > fileLength) fileLength = entry.DestinationOffset + entry.Size; } if (IsV31) writer.BaseStream.SetLength(fileLength); else writer.BaseStream.SetLength(fileLength + header.RawTableOffset); BinaryReader reader = null; foreach (BinaryPartEntry entry in binaryParts) { if (entry.SourceFile == null) continue; if (reader == null) { reader = new BinaryReader(entry.SourceFile); } else if (reader.Name != entry.SourceFile) { reader.Dispose(); reader = new BinaryReader(entry.SourceFile); } reader.Position = entry.SourceOffset; // // Smart change of output's stream position. This assumes that // the binary parts are sorted by destination offset. // int padSize = entry.DestinationOffset + header.RawTableOffset - writer.Position; if (padSize <= 32) writer.Write(padding, 0, padSize); else writer.Position = entry.DestinationOffset + header.RawTableOffset; if (entry.Field == null || entry.Field.RawType == null) { // // If we don't know the field or the fieldtype for this binary part // we just copy it over without cleaning up garbage. // if (copyBuffer1.Length < entry.Size) copyBuffer1 = new byte[entry.Size * 2]; reader.Read(copyBuffer1, 0, entry.Size); writer.Write(copyBuffer1, 0, entry.Size); } else { int size = entry.Size; while (size > 0) { int copiedSize = entry.Field.RawType.Copy(reader, writer, null); if (copiedSize > size) throw new InvalidOperationException(string.Format("Bad metadata copying field {0}", entry.Field.Name)); size -= copiedSize; } } } if (reader != null) reader.Dispose(); } private List RemoveDuplicates(List descriptors) { var checksums = new Dictionary>(); var newDescriptorList = new List(descriptors.Count); foreach (var descriptor in descriptors) { // // We only handle duplicates for these types of instances. // These are the most common cases and they are simple to handle // because they do not contain links to other instances. // if (!(descriptor.Template.Tag == TemplateTag.IDXA || descriptor.Template.Tag == TemplateTag.PNTA || descriptor.Template.Tag == TemplateTag.VCRA || descriptor.Template.Tag == TemplateTag.TXCA || descriptor.Template.Tag == TemplateTag.TRTA || descriptor.Template.Tag == TemplateTag.TRIA || descriptor.Template.Tag == TemplateTag.ONCP || descriptor.Template.Tag == TemplateTag.ONIA)) { newDescriptorList.Add(descriptor); continue; } int checksum = GetInstanceChecksum(descriptor); List existingDescriptors; if (!checksums.TryGetValue(checksum, out existingDescriptors)) { existingDescriptors = new List(); checksums.Add(checksum, existingDescriptors); } else { InstanceDescriptor existing = existingDescriptors.Find(x => AreInstancesEqual(descriptor, x)); if (existing != null) { sharedMap.Add(descriptor, existing); continue; } } existingDescriptors.Add(descriptor); newDescriptorList.Add(descriptor); } return newDescriptorList; } private int GetInstanceChecksum(InstanceDescriptor descriptor) { using (var checksumStream = new ChecksumStream()) using (var writer = new BinaryWriter(checksumStream)) { descriptor.Template.Type.Copy(streamCache.GetReader(descriptor), writer, null); return checksumStream.Checksum; } } private bool AreInstancesEqual(InstanceDescriptor d1, InstanceDescriptor d2) { if (d1.File == d2.File && d1.Index == d2.Index) return true; if (d1.Template.Tag != d2.Template.Tag || d1.DataSize != d2.DataSize) return false; if (copyBuffer1.Length < d1.DataSize) copyBuffer1 = new byte[d1.DataSize * 2]; if (copyBuffer2.Length < d2.DataSize) copyBuffer2 = new byte[d2.DataSize * 2]; MetaType type = d1.Template.Type; //return type.Compare(streamCache.GetStream(d1), streamCache.GetStream(d2)); using (var writer1 = new BinaryWriter(new MemoryStream(copyBuffer1))) using (var writer2 = new BinaryWriter(new MemoryStream(copyBuffer2))) { int s1 = type.Copy(streamCache.GetReader(d1), writer1, null); int s2 = type.Copy(streamCache.GetReader(d2), writer2, null); if (s1 != s2) return false; for (int i = 0; i < s1; i++) { if (copyBuffer1[i] != copyBuffer2[i]) return false; } } return true; } private bool IsV31 => header.Version == InstanceFileHeader.Version31; private bool IsV32 => header.Version == InstanceFileHeader.Version32; private static int MakeFileId(string filePath) { // // File id is generated from the filename. The filename is expected to be in // XXXXXN_YYYYY.dat format where the XXXXX (5 characters) part is ignored, N is treated // as a number (level number) and the YYYYY (this part can have any length) is hashed. // The file extension is ignored. // string fileName = Path.GetFileNameWithoutExtension(filePath); if (fileName.Length < 6) return 0; fileName = fileName.Substring(5); int levelNumber = 0; int buildTypeHash = 0; int i = fileName.IndexOf('_'); if (i != -1) { int.TryParse(fileName.Substring(0, i), out levelNumber); if (!string.Equals(fileName.Substring(i + 1), "Final", StringComparison.Ordinal)) { for (int j = 1; i + j < fileName.Length; j++) buildTypeHash += (char.ToUpperInvariant(fileName[i + j]) - 0x40) * j; } } return (((levelNumber << 24) | (buildTypeHash & 0xffffff)) << 1) | 1; } public static int MakeInstanceId(int index) => (index << 8) | 1; } }