public void Read(string filepath) { try { FileName = filepath; Chunkoffsets = new List <uint>(); using (var br = new BinaryReader(new FileStream(filepath, FileMode.Open))) { Files = new List <TextureCacheItem>(); br.BaseStream.Seek(-32, SeekOrigin.End); Crc = br.ReadUInt64(); UsedPages = br.ReadUInt32(); EntryCount = br.ReadUInt32(); StringTableSize = br.ReadUInt32(); MipOffsetTableSize = br.ReadUInt32(); IDString = br.ReadUInt32(); Version = br.ReadUInt32(); //JMP to the top of the info table: //32 is the the size of the stuff we read so far. //Every entry has 52 bytes of info //The stringtable //Every offset is 4 bytes //The sum of this is how much we need to jump from the back var jmp = -(32 + (EntryCount * 52) + StringTableSize + (MipOffsetTableSize * 4)); br.BaseStream.Seek(jmp, SeekOrigin.End); for (var i = 0; i < MipOffsetTableSize; i++) { Chunkoffsets.Add(br.ReadUInt32()); } Names = new List <string>(); for (var i = 0; i < EntryCount; i++) { Names.Add(br.ReadCR2WString()); } for (var i = 0; i < EntryCount; i++) { var ti = new TextureCacheItem(this) { Name = Names[i], ParentFile = FileName, Hash = br.ReadUInt32(), /*-------------TextureCacheEntryBase---------------*/ PathStringIndex = br.ReadInt32(), PageOFfset = br.ReadInt32(), CompressedSize = br.ReadInt32(), UncompressedSize = br.ReadInt32(), BaseAlignment = br.ReadUInt32(), BaseWidth = br.ReadUInt16(), BaseHeight = br.ReadUInt16(), Mipcount = br.ReadUInt16(), SliceCount = br.ReadUInt16(), MipOffsetIndex = br.ReadInt32(), NumMipOffsets = br.ReadInt32(), TimeStamp = br.ReadInt64(), /*-------------TextureCacheEntryBase---------------*/ Type1 = br.ReadByte(), Type2 = br.ReadByte(), IsCube = br.ReadByte(), Unk1 = br.ReadByte() }; Files.Add(ti); } //BUG: "C:\\Users\\bence.hambalko\\Documents\\The Witcher 3\\bin\\x64\\..\\..\\Mods\\modW3EE\\content\\texture.cache" dies here! Investigate!!!!!!!!!!!!! foreach (var t in Files) { br.BaseStream.Seek(t.PageOFfset * 4096, SeekOrigin.Begin); t.ZSize = br.ReadUInt32(); //Compressed size t.Size = br.ReadInt32(); //Uncompressed size t.SliceIdx = br.ReadByte(); //maybe the 48bit part of OFFSET var lastpos = br.BaseStream.Position + t.ZSize; for (int i = 0; i < t.NumMipOffsets; i++) { br.BaseStream.Seek(lastpos, SeekOrigin.Begin); var mzsize = br.ReadUInt32(); t.MipMapInfo.Add(new Tuple <uint, uint>( (uint)lastpos + 9, // mipmap offset mzsize)); //zsize lastpos += 9 + mzsize; } } } } catch (Exception e) { Debug.Assert(e != null); } }
public void Read(string filepath) { FileName = filepath; Chunkoffsets = new List <uint>(); using (var br = new BinaryReader(new FileStream(filepath, FileMode.Open))) { Files = new List <TextureCacheItem>(); br.BaseStream.Seek(-20, SeekOrigin.End); TextureCount = br.ReadUInt32(); NamesBlockSize = br.ReadUInt32(); ChunkOffsetCount = br.ReadUInt32(); Magic = br.ReadUInt32(); Version = br.ReadUInt32(); var jmp = -(20 + 12 + (TextureCount * 52) + NamesBlockSize + (ChunkOffsetCount * 4)); br.BaseStream.Seek(jmp, SeekOrigin.End); for (var i = 0; i < ChunkOffsetCount; i++) { Chunkoffsets.Add(br.ReadUInt32()); } Names = new List <string>(); for (var i = 0; i < TextureCount; i++) { Names.Add(br.ReadCR2WString()); } for (var i = 0; i < TextureCount; i++) { var ti = new TextureCacheItem(this) { Name = Names[i], ParentFile = FileName, Id = br.ReadUInt32(), //number (unique???) Filenameoffset = br.ReadUInt32(), //filename, start offset in block2 Offset = br.ReadUInt32(), //* 4096 = start offset, first chunk PackedSize = br.ReadUInt32(), //packed size (all chunks) UnpackedSize = br.ReadUInt32(), //unpacked size Bpp = br.ReadUInt32(), //bpp? always 16 Width = br.ReadUInt16(), //width Height = br.ReadUInt16(), //height Mips = br.ReadUInt16(), //mips Typeinfo = br.ReadUInt16(), //1/6/N, single, cubemaps, arrays B1Offset = br.ReadUInt32(), //offset in block1, second packed chunk Rpc = br.ReadUInt32(), //the number of remaining packed chunks Unk1 = br.ReadUInt32(), //??? Unk2 = br.ReadUInt32(), //??? Dxt = br.ReadByte(), //0-RGBA?, 7-DXT1, 8-DXT5, 10-???, 13-DXT3, 14-ATI1, 15-???, 253-??? Type = br.ReadByte(), //3-cubemaps, 4-texture Unk3 = br.ReadUInt16() //0/1 ??? }; Files.Add(ti); } for (var i = 0; i < 3; i++) { br.ReadBytes(4); } foreach (var t in Files) { br.BaseStream.Seek(t.Offset * 4096, SeekOrigin.Begin); t.ZSize = br.ReadUInt32(); //Compressed size t.Size = br.ReadInt32(); //Uncompressed size t.Part = br.ReadByte(); //maybe the 48bit part of OFFSET } } }
private void Read(string filepath) { try { ArchiveAbsolutePath = filepath; MipOffsets = new List <uint>(); using (var br = new BinaryReader(new FileStream(filepath, FileMode.Open))) { Files = new List <TextureCacheItem>(); Names = new List <string>(); #region Footer br.BaseStream.Seek(-32, SeekOrigin.End); Crc = br.ReadUInt64(); UsedPages = br.ReadUInt32(); EntryCount = br.ReadUInt32(); StringTableSize = br.ReadUInt32(); MipTableEntryCount = br.ReadUInt32(); var magic = br.ReadUInt32(); if (magic != MagicInt) { throw new Exception("Invalid file."); } Version = br.ReadUInt32(); #endregion Footer // JMP to the top of the info table: // 32 is the the size of the stuff we read so far. // + Every entry has 52 bytes of info // + The stringtablesize (found in the footer) // + MipTableEntryCount * offset is 4 bytes // The sum of this is how much we need to jump from the back var stringtableoffset = -(32 + (EntryCount * 52) + StringTableSize + (MipTableEntryCount * 4)); br.BaseStream.Seek(stringtableoffset, SeekOrigin.End); #region MipMapTable for (var i = 0; i < MipTableEntryCount; i++) { MipOffsets.Add(br.ReadUInt32()); } #endregion MipMapTable #region StringTable for (var i = 0; i < EntryCount; i++) { Names.Add(br.ReadCR2WString()); } #endregion StringTable #region EntryTable // jump to entry table var entrytableoffset = -(32 + (EntryCount * 52)); br.BaseStream.Seek(entrytableoffset, SeekOrigin.End); for (var i = 0; i < EntryCount; i++) { var ti = new TextureCacheItem(this) { Name = Names[i], ParentFile = ArchiveAbsolutePath, Hash = br.ReadUInt32(), /*-------------TextureCacheEntryBase---------------*/ StringTableOffset = br.ReadInt32(), PageOffset = br.ReadUInt32(), CompressedSize = br.ReadUInt32(), UncompressedSize = br.ReadUInt32(), BaseAlignment = br.ReadUInt32(), BaseWidth = br.ReadUInt16(), BaseHeight = br.ReadUInt16(), Mipcount = br.ReadUInt16(), SliceCount = br.ReadUInt16(), MipOffsetIndex = br.ReadInt32(), NumMipOffsets = br.ReadInt32(), TimeStamp = br.ReadInt64(), /*-------------TextureCacheEntryBase---------------*/ Type1 = br.ReadByte(), Type2 = br.ReadByte(), IsCube = br.ReadByte(), Unk1 = br.ReadByte() }; ti.Format = CommonImageTools.GetEFormatFromREDEngineByte(ti.Type1); Files.Add(ti); } #endregion EntryTable //BUG: "C:\\Users\\bence.hambalko\\Documents\\The Witcher 3\\bin\\x64\\..\\..\\Mods\\modW3EE\\content\\texture.cache" dies here! Investigate!!!!!!!!!!!!! foreach (var t in Files) { br.BaseStream.Seek(t.PageOffset * 4096, SeekOrigin.Begin); t.ZSize = br.ReadUInt32(); //Compressed size t.Size = br.ReadUInt32(); //Uncompressed size t.MipIdx = br.ReadByte(); //maybe the 48bit part of OFFSET var lastpos = br.BaseStream.Position + t.ZSize; for (int i = 0; i < t.NumMipOffsets; i++) { br.BaseStream.Seek(lastpos, SeekOrigin.Begin); var mzsize = br.ReadUInt32(); var msize = br.ReadUInt32(); var midx = br.ReadByte(); t.MipMapInfo.Add(new MipmapInfo(lastpos + 9, mzsize, msize, midx)); lastpos += 9 + mzsize; } } } } catch (Exception e) { Debug.Assert(e != null); } }
/// <summary> /// /// </summary> /// <param name="inputfolder"></param> public void LoadFiles(string inputfolder, ILoggerService logger = null) { var di = new DirectoryInfo(inputfolder); if (!di.Exists) { return; } var inputfiles = di.GetFiles("*.dds", SearchOption.AllDirectories) .Select(_ => _.FullName).ToList(); logger?.LogString($"[TextureCache] Begin caching.", Logtype.Important); logger?.LogString($"[TextureCache] Found {inputfiles.Count} files.", Logtype.Important); // clear data Files.Clear(); Names.Clear(); MipOffsets.Clear(); foreach (var filename in inputfiles) { var ext = Path.GetExtension(filename); switch (ext) { //case ".xbm": // { // // read cr2wfile // var cr2w = new CR2WFile() // { // FileName = filename, // }; // using (var cfs = new FileStream(filename, FileMode.Open, FileAccess.Read)) // using (var reader = new BinaryReader(cfs)) // { // var errorcode = cr2w.Read(reader); // if (errorcode != EFileReadErrorCodes.NoError) // continue; // } // // check if CBitmapTexture // if (!(cr2w.chunks.FirstOrDefault()?.data is CBitmapTexture xbm)) // { // continue; // } // break; // } case ".DDS": case ".dds": { var ddsheader = DDSUtils.ReadHeader(filename); var redpath = Path.ChangeExtension(filename, ddsheader.Iscubemap ? ".w2cube" : ".xbm"); var relativepath = redpath.Substring(di.FullName.Length + 1); #region Create Table Item var(type1, type2) = CommonImageTools.GetREDEngineByteFromEFormat(ddsheader.Format); if (ddsheader.Width % 2 != 0 || ddsheader.Height % 2 != 0) { continue; } var maxSide = Math.Max(ddsheader.Width, ddsheader.Height); //var minSide = Math.Min(ddsheader.Width, ddsheader.Height); var realmipscount = Math.Max(1, Math.Log10(maxSide / 32) / Math.Log10(2)); if (ddsheader.Mipscount == 1) //TODO: fix this { realmipscount = 0; } if (ddsheader.Mipscount == 0) //TODO: fix this { realmipscount = 0; } var ti = new TextureCacheItem(this) { Name = relativepath, FullName = filename, Hash = relativepath.HashStringKey(), /*------------- TextureCache Data ---------------*/ // NOTE: these need to be populated after writing the files ParentFile = "", //done StringTableOffset = -1, //done PageOffset = 0, //done CompressedSize = 0, //done UncompressedSize = 0, //done MipOffsetIndex = 0, //done /*------------- Image data ---------------*/ NumMipOffsets = (int)realmipscount, BaseAlignment = ddsheader.Bpp, BaseWidth = (ushort)ddsheader.Width, BaseHeight = (ushort)ddsheader.Height, Mipcount = (ushort)Math.Max(1, ddsheader.Mipscount), SliceCount = (ushort)ddsheader.Slicecount, //TODO TimeStamp = 0 /*(long)CDateTime.Now.ToUInt64()*/, //NOTE: Not even CDPR could be bothered to use their own Timestamps Type1 = type1, Type2 = type2, IsCube = ddsheader.Iscubemap ? (byte)1 : (byte)0, Unk1 = 0x00, //TODO: figure this out Format = CommonImageTools.GetEFormatFromREDEngineByte(type1) }; #endregion Create Table Item Files.Add(ti); Names.Add(ti.Name); logger?.LogString($"Cached {ti.Name}", Logtype.Normal); break; } } } logger?.LogString($"[TextureCache] Caching successful.", Logtype.Success); }
/// <summary> /// Generate a bundle from a list of binary Files. (uncompressed) /// </summary> /// <param name="Files"></param> private void Read(FileInfo[] _files) { //stringtable var stringTable = new Dictionary <string, int>(); int stoffset = 0; foreach (var f in _files) { stringTable.Add(GetRelativePath(f.FullName, f.Directory.FullName), stoffset); stoffset += f.Name.Length + 1; } long offset = 0; foreach (FileInfo f in _files) { long size; uint zSize; //get the raw bytes, rawbyte length and compressed bytes length using (var file = MemoryMappedFile.CreateFromFile(f.FullName, FileMode.Open)) using (var vs = file.CreateViewStream(0, f.Length)) { var buffer = new byte[f.Length]; vs.Read(buffer, 0, buffer.Length); size = buffer.Length; byte[] compressed = GetCompressed(buffer); zSize = (uint)compressed.Length; } //padding long nextOffset = GetOffset(offset + (int)zSize); TextureCacheItem newItem = new TextureCacheItem(this) { DepotPath = GetRelativePath(f.FullName, f.Directory.FullName), Hash = 0, //FIXME /*-------------TextureCacheEntryBase---------------*/ PathStringIndex = stringTable[f.Name], PageOffset = offset / ALIGNMENT_TARGET, ZSize = zSize, Size = size, BaseAlignment = 0, //FIXME BaseWidth = 0, //FIXME BaseHeight = 0, //FIXME TotalMipsCount = 0, //FIXME SliceCount = 0, //FIXME MipOffsetIndex = 0, //FIXME MipsCount = 0, //FIXME TimeStamp = 0, //FIXME /*-------------TextureCacheEntryBase---------------*/ Type = 0, //FIXME IsCube = 0, //FIXME }; Items.Add(newItem); offset = nextOffset; } //create footer Footer = new TextureCacheFooter { Crc = 0, //NOTE this stays 0, crc will get created when writing UsedPages = (UInt32)(offset / ALIGNMENT_TARGET), EntryCount = (UInt32)Items.Count, StringTableSize = (UInt32)stoffset, MipEntryCount = 0, //FIXME IDString = IDString, }; }
/// <summary> /// Reads a list of TextureCacheItems. (compressed) /// </summary> /// <param name="_files"></param> private void Read(params TextureCacheItem[] _files) { //stringtable var stringTable = new Dictionary <int, int>(); int stoffset = 0; var relMipsTable = new Dictionary <int, uint[]>(); for (int i = 0; i < _files.Length; i++) { TextureCacheItem f = _files[i]; stringTable.Add(i, stoffset); stoffset += f.DepotPath.Length + 1; //mipmaps relMipsTable.Add(i, f.GetMipsOffsettable()); } //entrytable long offset = 0; for (int i = 0; i < _files.Length; i++) { TextureCacheItem item = (TextureCacheItem)_files[i]; long nextOffset = GetOffset(offset + (int)item.ZSize); //calculate mipoffset var mo = relMipsTable.Where(_ => _.Key < i).SelectMany(_ => _.Value).Count(); var l = relMipsTable[i]; for (int j = 0; j < relMipsTable[i].Count(); j++) { l[j] += item.CachedZSizeNoMips + 9; } MipsOffsets.AddRange(l); if (offset / ALIGNMENT_TARGET < 0) { } TextureCacheItem newItem = new TextureCacheItem(item.Bundle) { BundlePath = item.Bundle.FileName, CachedMipsCount = item.CachedMipsCount, CachedZSizeNoMips = item.CachedZSizeNoMips, CachedSizeNoMips = item.CachedSizeNoMips, DepotPath = item.DepotPath, Hash = item.Hash, /*-------------TextureCacheEntryBase---------------*/ PathStringIndex = stringTable[i], PageOffset = offset / ALIGNMENT_TARGET, ZSize = item.ZSize, Size = item.Size, BaseAlignment = item.BaseAlignment, BaseWidth = item.BaseWidth, BaseHeight = item.BaseHeight, TotalMipsCount = item.TotalMipsCount, SliceCount = item.SliceCount, MipOffsetIndex = mo, MipsCount = item.MipsCount, TimeStamp = item.TimeStamp, /*-------------TextureCacheEntryBase---------------*/ Type = item.Type, IsCube = item.IsCube }; Items.Add(newItem); offset = nextOffset; } //create footer Footer = new TextureCacheFooter { Crc = 0, UsedPages = (UInt32)(offset / ALIGNMENT_TARGET), EntryCount = (UInt32)Items.Count, StringTableSize = (UInt32)stoffset, MipEntryCount = (uint)MipsOffsets.Count, IDString = IDString, }; }
/// <summary> /// Reads a texture.cache file. /// </summary> /// <param name="filepath"></param> private void Read(string filepath) { FileName = filepath; if (!File.Exists(filepath)) { return; } using (var br = new BinaryReader(new FileStream(filepath, FileMode.Open))) { Items = new List <TextureCacheItem>(); #region Footer br.BaseStream.Seek(-32, SeekOrigin.End); Footer = new TextureCacheFooter(); Footer.Read(br); //errorhandling if (!IDString.SequenceEqual(Footer.IDString)) { throw new InvalidCacheException("Cache header mismatch."); } //errorhandling #endregion #region InfoTable //JMP to the top of the info table: //32 is the the size of the stuff we read so far. //Every entry has 52 bytes of info //The stringtable //Every offset is 4 bytes //The sum of this is how much we need to jump from the back var jmp = -(32 + (Footer.EntryCount * 52) + Footer.StringTableSize + (Footer.MipEntryCount * 4)); br.BaseStream.Seek(jmp, SeekOrigin.End); var jmpoffset = br.BaseStream.Position; //Mips for (var i = 0; i < Footer.MipEntryCount; i++) { MipsOffsets.Add(br.ReadUInt32()); } //Names //BUG: "modW3EE\\content\\texture.cache" dies here! Investigate!!!!!!!!!!!!! /* * for some reason, some entries are doubled in the (middle of the) stringtable of the texture.cache. * leading to the string table being longer than it should be (more entries than entrycount) * this in turn let's the for loop (which runs over entrycount) to stop in the middle of the string-table * As a twist, there are actually duplicate file NAMES (but different compressed files) inside the bob texture.cache * which breaks any way of properly solving the w3ee problem * FIX * - check for entrys and names count * - if they are different (in the case of w3ee) try to resolve the error by making the names distinct * - this works for w3ee but is not guaranteed to work in all cases. * - skip loading if that didnt resolve the names/entry count */ var Names = new List <string>(); var entrytableoffset = jmpoffset + (Footer.MipEntryCount * 4) + Footer.StringTableSize; while (br.BaseStream.Position < entrytableoffset) { string entryname = br.ReadCR2WString(); Names.Add(entryname); } //errorhandling if (Footer.EntryCount != Names.Count) { //try resolving the error var resolvedNames = Names.Distinct().ToList(); if (Footer.EntryCount == resolvedNames.Count) { Names = resolvedNames; } else { throw new NotImplementedException(); } } //errorhandling //Entries br.BaseStream.Seek(entrytableoffset, SeekOrigin.Begin); for (var i = 0; i < Footer.EntryCount; i++) { var ti = new TextureCacheItem(this) { DepotPath = Names[i], ParentFile = FileName, Hash = br.ReadInt32(), /*-------------TextureCacheEntryBase---------------*/ PathStringIndex = br.ReadInt32(), PageOffset = br.ReadInt32(), //NOTE: texturecache pointers are stored as pagenumber, while bundleitems store absolute offset -_- ZSize = (uint)br.ReadInt32(), Size = br.ReadInt32(), BaseAlignment = br.ReadUInt32(), BaseWidth = br.ReadUInt16(), BaseHeight = br.ReadUInt16(), TotalMipsCount = br.ReadUInt16(), SliceCount = br.ReadUInt16(), MipOffsetIndex = br.ReadInt32(), MipsCount = br.ReadInt32(), TimeStamp = br.ReadInt64(), /*-------------TextureCacheEntryBase---------------*/ Type = br.ReadInt16(), IsCube = br.ReadInt16() }; Items.Add(ti); } #endregion #region Data //errorhandling var footeroffset = br.BaseStream.Length - 32; if (br.BaseStream.Position != footeroffset) { throw new NotImplementedException(); } //errorhandling for (int i = 0; i < Items.Count; i++) { TextureCacheItem t = Items[i]; br.BaseStream.Seek(t.PageOffset * 4096, SeekOrigin.Begin); t.CachedZSizeNoMips = br.ReadUInt32(); //Compressed size t.CachedSizeNoMips = br.ReadInt32(); //Uncompressed size t.CachedMipsCount = br.ReadByte(); //mips count } #endregion } }