/// <summary> /// Serialize bundle to a .bundle file. /// </summary> /// <param name="outdir"></param> public void Write(string outdir) { var filePath = Path.Combine(outdir, Name); using (var fs = new FileStream(filePath, FileMode.Create)) using (var bw = new BinaryWriter(fs)) { // Write Header Header.Write(bw); // Write ToC var minDataOffset = ALIGNMENT_TARGET; foreach (BundleItem f in Items) { f.Write(bw); } //pad the ToC int writePosition = (int)bw.BaseStream.Position; int tocPadding = GetOffset(writePosition) - writePosition; if (tocPadding > 0) { bw.Write(new byte[tocPadding]); } // Write Body for (int i = 0; i < Items.Count; i++) { BundleItem item = Items[i]; //compressed file var compressedFile = new List <byte>(); using (var ms = new MemoryStream()) { item.GetCompressedFile(ms); compressedFile.AddRange(ms.ToArray()); } //pad body items int filesize = (int)item.ZSize; int nextOffset = GetOffset((int)item.PageOffset + filesize); int paddingLength = nextOffset - ((int)item.PageOffset + filesize); if (paddingLength > 0 && i < (Items.Count - 1)) //don't pad the last item { compressedFile.AddRange(new byte[paddingLength]); } bw.Write(compressedFile.ToArray()); } } }
//FIXME buffers and blobs /// <summary> /// Create a .bundle file from a directory. /// </summary> /// <param name="outDir"></param> /// <param name="inDir"></param> public static void Pack(string inDir, string outDir) { List <IWitcherFile> bufferFiles = new List <IWitcherFile>(); List <IWitcherFile> blobFiles = new List <IWitcherFile>(); foreach (var f in Directory.EnumerateFiles(inDir, "*", SearchOption.AllDirectories)) { var name = Encoding.Default.GetBytes(GetRelativePath(f, inDir)).ToArray(); if (name.Length > 0x100) { name = name.Take(0x100).ToArray(); } if (name.Length < 0x100) { Array.Resize(ref name, 0x100); } var bi = new BundleItem { Name = System.Text.Encoding.Default.GetString(name), Compression = 5, //FIXME make variable }; //sort buffers out if (f.Split('\\').Last().Split('.').Last() == "buffer") { bufferFiles.Add(bi); } else { blobFiles.Add(bi); } } List <Bundle> bundles = new List <Bundle>(); if (blobFiles.Count > 0) { bundles.Add(new Bundle(blobFiles.ToArray())); } if (bufferFiles.Count > 0) { bundles.Add(new Bundle(bufferFiles.ToArray())); } foreach (var b in bundles) { b.Write(outDir); } }
/// <summary> /// Generate a bundle from a list of BundleItems. (compressed) /// </summary> /// <param name="Files"></param> private void Read(BundleItem[] Files) { uint tocRealSize = (UInt32)(Files.Length * TOCEntrySize); // MAIN BODY int offset = GetOffset((int)tocRealSize + HEADER_SIZE); foreach (BundleItem item in Files) { int nextOffset = GetOffset(offset + (int)item.ZSize); BundleItem newItem = new BundleItem() { FileAccessor = new BundleAccesor(item.Bundle.FileName, item.PageOffset), DepotPath = item.DepotPath, Hash = item.Hash, Size = item.Size, ZSize = item.ZSize, Compression = item.Compression, DateString = item.DateString, Bundle = this, PageOffset = (uint)offset, CRC = item.CRC }; Items.Add(newItem); offset = nextOffset; } //create header uint dummysize = 0; uint bundlesize = (uint)offset; Header = new BundleHeader(IDString, bundlesize, dummysize, tocRealSize); }
/// <summary> /// Reads a Metadata_Store from a list of bundles. /// </summary> /// <param name="Bundles"></param> private void Read(params Bundle[] Bundles) { #region Info FileStringTable.Add(0x00); fileInfoList.Add(new UFileInfo()); fileEntryInfoList.Add(new UFileEntryInfo()); bundleInfoList.Add(new UBundleInfo()); #endregion var stOffsetDict = new Dictionary <string, uint>(); var _entries = new List <BundleItem>(); var _entryNames = new List <string>(); var _bufferNames = new List <string>(); var _bundleNames = new List <string>(); var _fileNames = new List <string>(); var _dirNames = new List <string>(); var _dirInfos = new List <DirectoryInfo>(); var _fileInfos = new List <FileInfo>(); #region Dir and Files Table foreach (var b in Bundles) { string bundleName = b.Name; _bundleNames.Add(bundleName); stOffsetDict.Add(bundleName, (uint)FileStringTable.Count); FileStringTable.AddRange(Encoding.UTF8.GetBytes(bundleName)); FileStringTable.Add(0x00); foreach (var item in b.Items) { string relFullPath = item.DepotPath; if (_entryNames.Contains(relFullPath)) { continue; } // stringtable: files stOffsetDict.Add(relFullPath, (uint)FileStringTable.Count); FileStringTable.AddRange(Encoding.UTF8.GetBytes(relFullPath)); FileStringTable.Add(0x00); //add buffername if (relFullPath.Split('.').Last() == "buffer") { //add to buffer list string buffername = relFullPath.Split(new string[] { ".1.buffer" }, StringSplitOptions.None).First(); if (_bufferNames.Contains(relFullPath)) { continue; } _bufferNames.Add(buffername); } else { // stringtable: dir and file names FileInfo fi = new FileInfo($"\\{relFullPath}"); _fileInfos.Add(fi); if (!_fileNames.Contains(fi.Name)) { _fileNames.Add(fi.Name); } var dirs = relFullPath.Split('\\').ToList(); dirs.Remove(dirs.Last()); _dirNames.AddRange(dirs.Where(_ => !_dirNames.Contains(_))); } // add to entry list _entryNames.Add(relFullPath); _entries.Add(item); } } FileStringTable.Add(0x00); foreach (var d in _dirNames) { stOffsetDict.Add(d, (uint)FileStringTable.Count); FileStringTable.AddRange(Encoding.UTF8.GetBytes(d)); FileStringTable.Add(0x00); } foreach (var f in _fileNames) { stOffsetDict.Add(f, (uint)FileStringTable.Count); FileStringTable.AddRange(Encoding.UTF8.GetBytes(f)); FileStringTable.Add(0x00); } int StringTableSize = FileStringTable.Count; #endregion #region UFileInitInfo and Hashes foreach (var fi in _fileInfos) { //add directoryInfo to Directory info list DirectoryInfo di = fi.Directory; while (true) { if (_dirInfos.Select(_ => _.Name).Contains(di.Name)) { break; } _dirInfos.Add(di); di = di.Parent; if (di.Parent == null) { break; } } var fii = new UFileInitInfo() { FileIF = _fileInfos.IndexOf(fi) + 1, DirID = (Int32)_dirNames.IndexOf(fi.Directory.Name) + 1, Name = (Int32)stOffsetDict[fi.Name] }; fileInitInfoList.Add(fii); var fullname = _entries.First(_ => _.DepotPath.Split('\\').Last() == fi.Name).DepotPath; UInt64 hash = (UInt64)FNV1a.HashFNV1a64(fullname); var h = new UHash() { Hash = (UInt64)hash, FileID = (UInt64)_fileInfos.IndexOf(fi) + 1 }; hashes.Add(h); } //hashes are sorted by hashsize not by filenumber hashes.Sort((x, y) => x.Hash.CompareTo(y.Hash)); #endregion #region UDirInitInfo //first directoryinfo i null with offset to the first 0 byte of the dir Table _dirInfos.Reverse(); dirInitInfoList.Add(new UDirInitInfo() { ParentID = 0, Name = (Int32)stOffsetDict[_dirNames.First()] - 1 }); foreach (var di in _dirInfos) { Int32 parentID = 0; if (di.Parent.Parent != null) { parentID = (int)_dirNames.IndexOf(di.Parent.Name) + 1; } var dii = new UDirInitInfo() { ParentID = parentID, Name = (Int32)stOffsetDict[di.Name] }; dirInitInfoList.Add(dii); } #endregion #region UBundleInfo foreach (var b in Bundles) { string bundleName = b.Name; BundleItem ffe = _entries.First(_ => ((Bundle)_.Bundle).Name == bundleName); uint dataBlockOffset = b.Header.TocRealSize + 32; uint dataBlockSize = b.Header.Bundlesize - dataBlockOffset; var bi = new UBundleInfo() { Name = stOffsetDict[bundleName], FirstFileEntry = (UInt32)(_entries.IndexOf(ffe) + 1), NumBundleEntries = (UInt32)b.Items.Count, DataBlockSize = dataBlockSize, //NOTE this is simply wrong for Buffers in vanilla (always 4096) DataBlockOffset = dataBlockOffset, BurstDataBlockSize = 0, }; bundleInfoList.Add(bi); } #endregion #region UFileInfos and UFileEntryInfos for (int i = 0; i < _entries.Count; i++) { BundleItem e = _entries[i]; UInt32 bufferid = 0; UInt32 hasbuffer = 0; if (_bufferNames.Contains(e.DepotPath)) { hasbuffer = 1; bufferid = (uint)_bufferNames.IndexOf(e.DepotPath); } var fi = new UFileInfo() { StringTableNameOffset = stOffsetDict[e.DepotPath], PathHash = 0, //FIXME this is always 0... SizeInBundle = e.ZSize, SizeInMemory = (UInt32)e.Size, FirstEntry = (UInt32)(_entries.IndexOf(e) + 1), CompressionType = e.Compression, bufferid = bufferid, hasbuffer = hasbuffer }; fileInfoList.Add(fi); string bundleName = ((Bundle)e.Bundle).Name; var fei = new UFileEntryInfo() { FileID = (uint)i + 1, BundleID = (uint)_bundleNames.ToList().IndexOf(bundleName) + 1, OffsetInBundle = (uint)e.PageOffset, SizeInBundle = e.ZSize, NextEntry = 0 }; fileEntryInfoList.Add(fei); } #endregion #region Buffers foreach (var buffer in _entries.Where(_ => _.DepotPath.Split('.')?.Last() == "buffer")) { Buffers.Add(_entries.IndexOf(buffer) + 1); } #endregion MaxFileSizeInBundle = fileInfoList.Select(_ => _.SizeInBundle).ToList().Max(); MaxFileSIzeInMemory = fileInfoList.Select(_ => _.SizeInMemory).ToList().Max(); }
/// <summary> /// Reads the Table Of Contents of the bundle. /// </summary> private void Read() { Items = new Dictionary <string, BundleItem>(); using (var reader = new BinaryReader(new FileStream(ArchiveAbsolutePath, FileMode.Open, FileAccess.Read))) { var idstring = reader.ReadBytes(IDString.Length); if (!IDString.SequenceEqual(idstring)) { throw new InvalidBundleException("Archive header mismatch."); } bundlesize = reader.ReadUInt32(); dummysize = reader.ReadUInt32(); dataoffset = reader.ReadUInt32(); reader.BaseStream.Seek(0x20, SeekOrigin.Begin); while (reader.BaseStream.Position < dataoffset + 0x20) { var item = new BundleItem { Archive = this }; var strname = Encoding.GetEncoding("ISO-8859-1").GetString(reader.ReadBytes(0x100)); item.Name = strname.Substring(0, strname.IndexOf('\0')); item.Hash = reader.ReadBytes(16); item.Empty = reader.ReadUInt32(); item.Size = reader.ReadUInt32(); item.ZSize = reader.ReadUInt32(); item.PageOffset = reader.ReadUInt32(); var date = reader.ReadUInt32(); var y = date >> 20; var m = date >> 15 & 0x1F; var d = date >> 10 & 0x1F; var time = reader.ReadUInt32(); var h = time >> 22; var n = time >> 16 & 0x3F; var s = time >> 10 & 0x3F; item.DateString = string.Format(" {0}/{1}/{2} {3}:{4}:{5}", d, m, y, h, n, s); item.Zero = reader.ReadBytes(16); //00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 (always, in every archive) item.CRC = reader.ReadUInt32(); //CRC32 for the uncompressed data item.Compression = reader.ReadUInt32(); if (!Items.ContainsKey(item.Name)) { Items.Add(item.Name, item); } else { Console.WriteLine("Warning: archive '" + ArchiveAbsolutePath + "' could not be fully loaded as resource '" + item.Name + "' is defined more than once. Only the first definition was loaded."); } } reader.Close(); } }
/// <summary> /// Reads a .bundle file. /// </summary> public void Read(string filename) { FileName = filename; Name = filename.Split('\\').Last(); Items = new Dictionary <string, BundleItem>(); using (var reader = new BinaryReader(new FileStream(FileName, FileMode.Open, FileAccess.Read))) { var idstring = reader.ReadBytes(IDString.Length); if (!IDString.SequenceEqual(idstring)) { throw new InvalidBundleException("Bundle header mismatch."); } bundlesize = reader.ReadUInt32(); dummysize = reader.ReadUInt32(); tocRealSize = reader.ReadUInt32(); DataBlockOffset = tocRealSize + (uint)HEADER_SIZE; DataBlockSize = bundlesize - DataBlockOffset; reader.BaseStream.Seek(0x20, SeekOrigin.Begin); while (reader.BaseStream.Position < tocRealSize + 0x20) { var item = new BundleItem { Bundle = this }; var strname = Encoding.Default.GetString(reader.ReadBytes(0x100)); item.Name = strname.Substring(0, strname.IndexOf('\0')); item.Hash = reader.ReadBytes(16); item.Empty = reader.ReadUInt32(); item.Size = reader.ReadUInt32(); item.ZSize = reader.ReadUInt32(); item.PageOFfset = reader.ReadUInt32(); var date = reader.ReadUInt32(); var y = date >> 20; var m = date >> 15 & 0x1F; var d = date >> 10 & 0x1F; var time = reader.ReadUInt32(); var h = time >> 22; var n = time >> 16 & 0x3F; var s = time >> 10 & 0x3F; item.DateString = string.Format(" {0}/{1}/{2} {3}:{4}:{5}", d, m, y, h, n, s); item.Zero = reader.ReadBytes(16); //00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 (always, in every archive) item.CRC = reader.ReadUInt32(); //CRC32 for the uncompressed data item.Compression = reader.ReadUInt32(); Items.Add(item.Name, item); ItemsList.Add(item); } reader.Close(); } }