/// <summary> /// Returns a byte[] containing the converted image data from the cache. /// </summary> /// <param name="cache"></param> /// <param name="bitmapTag"></param> /// <param name="index"></param> /// <param name="version"></param> /// <returns></returns> public static BaseBitmap ConvertGen3Bitmap(CacheFile cache, Bitmap bitmapTag, int index, CacheVersion version) { if (cache.ResourceLayoutTable == null || cache.ResourceGestalt == null) { cache.LoadResourceTags(); } byte[] imageData = null; byte[] mipMapData = null; XboxBitmap xboxBitmap = null; int bitmapSize = 0; int mipMapSize = 0; var image = bitmapTag.Images[index]; var handle = GetBitmapResourceHandle(bitmapTag, index, version); if (!ResourceEntryValid(cache, handle) || (!HasPrimaryResource(cache, handle) && !HasSecondaryResource(cache, handle))) { Console.WriteLine($"Invalid resource entry at {handle}. No data to convert."); return(null); } // interleaved means two images are inside a single resource along with the mipmaps. if (image.XboxFlags.HasFlag(BitmapFlagsXbox.UseInterleavedTextures)) { var resourceDef = GetInterleavedResourceDefinition(cache, handle); xboxBitmap = new XboxBitmap(resourceDef, image.InterleavedTextureIndex2, image); bitmapSize = BitmapUtils.GetXboxImageSize(xboxBitmap); mipMapSize = 0; xboxBitmap.Offset = 0; if (!xboxBitmap.InTile) { var offset = image.InterleavedTextureIndex2 * (int)(xboxBitmap.VirtualHeight * xboxBitmap.VirtualWidth / xboxBitmap.CompressionFactor); imageData = cache.GetSecondaryResource(handle, bitmapSize, offset, true); mipMapData = null; } else { if (xboxBitmap.Type == BitmapType.CubeMap && image.Flags.HasFlag(BitmapFlags.Compressed) && xboxBitmap.Width <= 16) { xboxBitmap.Offset = (int)(16 * 4 / xboxBitmap.CompressionFactor); // account for the mipmaps } imageData = cache.GetPrimaryResource(handle, bitmapSize, 0, true); mipMapData = null; } if (image.InterleavedTextureIndex2 == 1 && xboxBitmap.InTile) { byte[] totalData = null; var tileSize = (int)(xboxBitmap.MinimalBitmapSize * xboxBitmap.MinimalBitmapSize / xboxBitmap.CompressionFactor); var subCount = 0; switch (xboxBitmap.Type) { case BitmapType.Texture2D: subCount = 1; break; case BitmapType.Texture3D: case BitmapType.Array: subCount = xboxBitmap.Depth; break; case BitmapType.CubeMap: subCount = 6; break; } if (mipMapData != null) { totalData = new byte[bitmapSize + mipMapSize]; Array.Copy(imageData, 0, totalData, 0, bitmapSize); Array.Copy(mipMapData, 0, totalData, bitmapSize, mipMapSize); } else { totalData = imageData; } for (int i = 0; i < subCount; i++) { // make sure to copy the right amount of data var copySize = tileSize; if (copySize > totalData.Length - ((tileSize * i) + tileSize / 2)) { copySize = totalData.Length - ((tileSize * i) + tileSize / 2); } Array.Copy(totalData, (tileSize * i) + tileSize / 2, imageData, (tileSize * i), copySize); } } } else { var resourceDef = GetResourceDefinition(cache, handle); xboxBitmap = new XboxBitmap(resourceDef, image); bitmapSize = BitmapUtils.GetXboxImageSize(xboxBitmap); if (HasSecondaryResource(cache, handle)) { imageData = cache.GetSecondaryResource(handle, bitmapSize, 0, true); if (xboxBitmap.MipMapCount > 0) { if (HasPrimaryResource(cache, handle)) { // dedicated resource for mipmaps mipMapData = cache.GetPrimaryResource(handle, mipMapSize, 0, true); } else { //throw new Exception($"Unsupported layout. Compute bitmap offset for weird bitmap."); mipMapData = null; } } else { mipMapData = null; } } else { // Bitmap doesn't have a secondary resource means either no mipmaps or everything is packed in the primary resource. if (xboxBitmap.MipMapCount > 0) { imageData = cache.GetPrimaryResource(handle, 2 * bitmapSize, 0, true); mipMapData = cache.GetPrimaryResource(handle, mipMapSize, 0, true); // Formula seems quite complex, small hack to make it work if (xboxBitmap.BlockDimension == 4) { if (xboxBitmap.Width > xboxBitmap.Height) { xboxBitmap.Offset = 4 * (int)(BitmapUtils.RoundSize(xboxBitmap.Height, 4) * xboxBitmap.VirtualWidth / xboxBitmap.CompressionFactor); } else if (xboxBitmap.Width == xboxBitmap.Height) { var width = xboxBitmap.Width; if (xboxBitmap.Width >= 4) { width = 4; } xboxBitmap.Offset = 4 * (int)(width * 4 / xboxBitmap.CompressionFactor); } else { var width = xboxBitmap.Width / 2; if (width == 0) { width = 1; } xboxBitmap.Offset = 4 * (int)(BitmapUtils.RoundSize(width, 4) * xboxBitmap.BlockDimension / xboxBitmap.CompressionFactor); } } else { xboxBitmap.Offset = (int)(xboxBitmap.Width * 4 / xboxBitmap.CompressionFactor); Console.WriteLine("WEIRD BITMAP"); } } else { imageData = cache.GetPrimaryResource(handle, bitmapSize, 0, true); mipMapData = null; } } } // // Convert main bitmap // List <XboxBitmap> xboxBitmaps = ParseImages(xboxBitmap, image, imageData, bitmapSize); bool flipImage = true; // rearrange cubemaps order if (xboxBitmap.Type == BitmapType.CubeMap) { XboxBitmap temp = xboxBitmaps[1]; xboxBitmaps[1] = xboxBitmaps[2]; xboxBitmaps[2] = temp; } List <BaseBitmap> finalBitmaps = new List <BaseBitmap>(); foreach (var bitmap in xboxBitmaps) { // extract bitmap from padded image BaseBitmap finalBitmap = ExtractImage(bitmap); // convert to PC format flipImage = ConvertImage(finalBitmap); // flip data if required if (flipImage) { FlipImage(finalBitmap, image); } // until I write code to move mipmaps at the end of the file, remove cubemap mipmaps if (xboxBitmap.Type == BitmapType.CubeMap) { finalBitmap.MipMapCount = 0; } // generate mipmaps for uncompressed textures if (!finalBitmap.Flags.HasFlag(BitmapFlags.Compressed) && finalBitmap.MipMapCount > 0) { GenerateUncompressedMipMaps(finalBitmap); } finalBitmaps.Add(finalBitmap); } // build and return the final bitmap return(RebuildBitmap(finalBitmaps)); }