From 358b03de752590b46494ac69e94c8f9034571e64 Mon Sep 17 00:00:00 2001 From: iAmAsval Date: Tue, 18 Aug 2020 17:46:52 +0200 Subject: [PATCH] added VirtualTexture "support" + Oodle compression support --- FModel/Creator/Bases/BaseIcon.cs | 4 +- FModel/Creator/Creator.cs | 2 +- FModel/Creator/Rarities/Rarity.cs | 1 + FModel/PakReader/Parsers/Class/UTexture2D.cs | 21 ++++- .../Parsers/Objects/EVirtualTextureCodec.cs | 14 ++++ FModel/PakReader/Parsers/Objects/FPakEntry.cs | 1 + FModel/PakReader/Parsers/Objects/FPakInfo.cs | 2 +- .../Parsers/Objects/FTexturePlatformData.cs | 10 ++- .../Objects/FVirtualTextureBuiltData.cs | 73 ++++++++++++++++ .../Objects/FVirtualTextureDataChunk.cs | 30 +++++++ FModel/PakReader/Parsers/OodleStream.cs | 84 +++++++++++++++++++ FModel/PakReader/Parsers/PackageReader.cs | 1 + .../Parsers/PropertyTagData/BaseProperty.cs | 4 +- .../Parsers/PropertyTagData/ByteProperty.cs | 7 +- FModel/PakReader/WwiseReader.cs | 6 +- .../Windows/SoundPlayer/AudioPlayer.xaml.cs | 1 + 16 files changed, 246 insertions(+), 15 deletions(-) create mode 100644 FModel/PakReader/Parsers/Objects/EVirtualTextureCodec.cs create mode 100644 FModel/PakReader/Parsers/Objects/FVirtualTextureBuiltData.cs create mode 100644 FModel/PakReader/Parsers/Objects/FVirtualTextureDataChunk.cs create mode 100644 FModel/PakReader/Parsers/OodleStream.cs diff --git a/FModel/Creator/Bases/BaseIcon.cs b/FModel/Creator/Bases/BaseIcon.cs index 8dc34798..24b035dc 100644 --- a/FModel/Creator/Bases/BaseIcon.cs +++ b/FModel/Creator/Bases/BaseIcon.cs @@ -85,10 +85,10 @@ namespace FModel.Creator.Bases { } // ^^^^ will return false if image not found, if so, we try to get the normal icon else if (export.GetExport("HeroDefinition", "WeaponDefinition") is ObjectProperty itemDef) LargeSmallImage.GetPreviewImage(this, itemDef, assetName); - else if (export.GetExport("SmallPreviewImage") is ObjectProperty smallPreviewImage) - this.IconImage = Utils.GetObjectTexture(smallPreviewImage); else if (export.GetExport("LargePreviewImage", "SmallPreviewImage", "ItemDisplayAsset") is SoftObjectProperty previewImage) LargeSmallImage.GetPreviewImage(this, previewImage); + else if (export.GetExport("SmallPreviewImage") is ObjectProperty smallPreviewImage) + this.IconImage = Utils.GetObjectTexture(smallPreviewImage); else if (export.GetExport("IconBrush") is StructProperty iconBrush) // abilities LargeSmallImage.GetPreviewImage(this, iconBrush); diff --git a/FModel/Creator/Creator.cs b/FModel/Creator/Creator.cs index ed53ad1f..c13600e8 100644 --- a/FModel/Creator/Creator.cs +++ b/FModel/Creator/Creator.cs @@ -250,7 +250,7 @@ namespace FModel.Creator Process.Start(new ProcessStartInfo { FileName = string.Format( - "http://valorant.dyn.riotcdn.net/x/videos/release-01.04/{0}_default_universal.mp4", + "http://valorant.dyn.riotcdn.net/x/videos/release-01.05/{0}_default_universal.mp4", $"{uuid.A:x8}-{uuid.B >> 16:x4}-{uuid.B & 0xFFFF:x4}-{uuid.C >> 16:x4}-{uuid.C & 0xFFFF:x4}{uuid.D:x8}"), UseShellExecute = true }); diff --git a/FModel/Creator/Rarities/Rarity.cs b/FModel/Creator/Rarities/Rarity.cs index b8728072..1af83fa2 100644 --- a/FModel/Creator/Rarities/Rarity.cs +++ b/FModel/Creator/Rarities/Rarity.cs @@ -46,6 +46,7 @@ namespace FModel.Creator.Rarities rarity = EFortRarity.Transcendent; break; case "EFortRarity::Unattainable": + case "EFortRarity::Badass": rarity = EFortRarity.Unattainable; break; } diff --git a/FModel/PakReader/Parsers/Class/UTexture2D.cs b/FModel/PakReader/Parsers/Class/UTexture2D.cs index 17b88778..e48eae22 100644 --- a/FModel/PakReader/Parsers/Class/UTexture2D.cs +++ b/FModel/PakReader/Parsers/Class/UTexture2D.cs @@ -16,8 +16,25 @@ namespace PakReader.Parsers.Class { if (image == null) { - var mip = PlatformDatas[0].Mips[0]; - image = TextureDecoder.DecodeImage(mip.BulkData.Data, mip.SizeX, mip.SizeY, mip.SizeZ, PlatformDatas[0].PixelFormat); + int sizeX = 0; + int sizeY = 0; + int sizeZ = 1; + List data = new List(); + if (PlatformDatas[0].Mips.Length > 0) + { + sizeX = PlatformDatas[0].Mips[0].SizeX; + sizeY = PlatformDatas[0].Mips[0].SizeY; + sizeZ = PlatformDatas[0].Mips[0].SizeZ; + data.AddRange(PlatformDatas[0].Mips[0].BulkData.Data); + } + + //if (PlatformDatas[0].bIsVirtual) + //{ + // sizeX = PlatformDatas[0].VTData.Width; + // sizeY = PlatformDatas[0].VTData.Height; + //} + + image = TextureDecoder.DecodeImage(data.ToArray(), sizeX, sizeY, sizeZ, PlatformDatas[0].PixelFormat); } return image; } diff --git a/FModel/PakReader/Parsers/Objects/EVirtualTextureCodec.cs b/FModel/PakReader/Parsers/Objects/EVirtualTextureCodec.cs new file mode 100644 index 00000000..dcf29d38 --- /dev/null +++ b/FModel/PakReader/Parsers/Objects/EVirtualTextureCodec.cs @@ -0,0 +1,14 @@ +namespace PakReader.Parsers.Objects +{ + public enum EVirtualTextureCodec : byte + { + Black, //Special case codec, always outputs black pixels 0,0,0,0 + OpaqueBlack, //Special case codec, always outputs opaque black pixels 0,0,0,255 + White, //Special case codec, always outputs white pixels 255,255,255,255 + Flat, //Special case codec, always outputs 128,125,255,255 (flat normal map) + RawGPU, //Uncompressed data in an GPU-ready format (e.g R8G8B8A8, BC7, ASTC, ...) + ZippedGPU, //Same as RawGPU but with the data zipped + Crunch, //Use the Crunch library to compress data + Max, // Add new codecs before this entry + } +} diff --git a/FModel/PakReader/Parsers/Objects/FPakEntry.cs b/FModel/PakReader/Parsers/Objects/FPakEntry.cs index fafa8f78..14dd7dd9 100644 --- a/FModel/PakReader/Parsers/Objects/FPakEntry.cs +++ b/FModel/PakReader/Parsers/Objects/FPakEntry.cs @@ -189,6 +189,7 @@ namespace PakReader.Parsers.Objects { "Zlib" => new ZlibStream(blockMs, CompressionMode.Decompress), "Gzip" => new GZipStream(blockMs, CompressionMode.Decompress), + "Oodle" => new OodleStream(blockBbuffer, uncompressedSize), _ => throw new NotImplementedException($"Decompression not yet implemented ({compressionMethod})") }; diff --git a/FModel/PakReader/Parsers/Objects/FPakInfo.cs b/FModel/PakReader/Parsers/Objects/FPakInfo.cs index 326b2725..c6abf70d 100644 --- a/FModel/PakReader/Parsers/Objects/FPakInfo.cs +++ b/FModel/PakReader/Parsers/Objects/FPakInfo.cs @@ -80,7 +80,7 @@ namespace PakReader.Parsers.Objects var MethodList = new List(4); for (int i = 0; i < 4; i++) { - if (Methods[i*COMPRESSION_METHOD_NAME_LEN] != 0) + if (Methods[i * COMPRESSION_METHOD_NAME_LEN] != 0) { MethodList.Add(Encoding.ASCII.GetString(Methods, i * COMPRESSION_METHOD_NAME_LEN, COMPRESSION_METHOD_NAME_LEN).TrimEnd('\0')); } diff --git a/FModel/PakReader/Parsers/Objects/FTexturePlatformData.cs b/FModel/PakReader/Parsers/Objects/FTexturePlatformData.cs index 05920f1f..6d345f20 100644 --- a/FModel/PakReader/Parsers/Objects/FTexturePlatformData.cs +++ b/FModel/PakReader/Parsers/Objects/FTexturePlatformData.cs @@ -10,6 +10,8 @@ namespace PakReader.Parsers.Objects public readonly int NumSlices; public readonly EPixelFormat PixelFormat; public readonly FTexture2DMipMap[] Mips; + public readonly FVirtualTextureBuiltData VTData; + public readonly bool bIsVirtual; internal FTexturePlatformData(PackageReader reader, Stream ubulk, long bulkOffset) { @@ -17,6 +19,8 @@ namespace PakReader.Parsers.Objects SizeY = reader.ReadInt32(); NumSlices = reader.ReadInt32(); PixelFormat = Enum.Parse(reader.ReadFString()); + VTData = default; + bIsVirtual = false; var FirstMipToSerialize = reader.ReadInt32(); FirstMipToSerialize = 0; // what: https://github.com/EpicGames/UnrealEngine/blob/4.24/Engine/Source/Runtime/Engine/Private/TextureDerivedData.cpp#L1316 @@ -25,7 +29,11 @@ namespace PakReader.Parsers.Objects if (FModel.Globals.Game.Version > EPakVersion.FNAME_BASED_COMPRESSION_METHOD || FModel.Globals.Game.SubVersion == 1) { - if (reader.ReadInt32() != 0) throw new FileLoadException("VirtualTextures are not supported"); + bIsVirtual = reader.ReadInt32() != 0; + if (bIsVirtual) + { + VTData = new FVirtualTextureBuiltData(reader, ubulk, bulkOffset); + } } } } diff --git a/FModel/PakReader/Parsers/Objects/FVirtualTextureBuiltData.cs b/FModel/PakReader/Parsers/Objects/FVirtualTextureBuiltData.cs new file mode 100644 index 00000000..7d569367 --- /dev/null +++ b/FModel/PakReader/Parsers/Objects/FVirtualTextureBuiltData.cs @@ -0,0 +1,73 @@ +using System; +using System.IO; + +namespace PakReader.Parsers.Objects +{ + public readonly struct FVirtualTextureBuiltData + { + public readonly uint NumLayers; + public readonly uint NumMips; + public readonly int Width; // Width of the texture in pixels. Note the physical width may be larger due to tiling + public readonly int Height; // Height of the texture in pixels. Note the physical height may be larger due to tiling + public readonly uint WidthInBlocks; // Number of UDIM blocks that make up the texture, used to compute UV scaling factor + public readonly uint HeightInBlocks; + public readonly uint TileSize; // Tile size excluding borders + public readonly uint TileBorderSize; // A BorderSize pixel border will be added around all tiles + + /** + * The pixel format output of the data on the i'th layer. The actual data + * may still be compressed but will decompress to this pixel format (e.g. zipped DXT5 data). + */ + public readonly EPixelFormat[] LayerTypes; + + /** + * Tile data is packed into separate chunks, typically there is 1 mip level in each chunk for high resolution mips. + * After a certain threshold, all remaining low resolution mips will be packed into one final chunk. + */ + public readonly FVirtualTextureDataChunk[] Chunks; + + /** Index of the first tile within each chunk */ + public readonly uint[] TileIndexPerChunk; + + /** Index of the first tile within each mip level */ + public readonly uint[] TileIndexPerMip; + + /** + * Info for the tiles organized per level. Within a level tile info is organized in Morton order. + * This is in morton order which can waste a lot of space in this array for non-square images + * e.g.: + * - An 8x1 tile image will allocate 8x4 indexes in this array. + * - An 1x8 tile image will allocate 8x8 indexes in this array. + */ + public readonly uint[] TileOffsetInChunk; + + internal FVirtualTextureBuiltData(BinaryReader reader, Stream ubulk, long bulkOffset) + { + reader.ReadInt32(); // bCooked + NumLayers = reader.ReadUInt32(); + WidthInBlocks = reader.ReadUInt32(); + HeightInBlocks = reader.ReadUInt32(); + TileSize = reader.ReadUInt32(); + TileBorderSize = reader.ReadUInt32(); + + NumMips = reader.ReadUInt32(); + Width = reader.ReadInt32(); + Height = reader.ReadInt32(); + TileIndexPerChunk = reader.ReadTArray(() => reader.ReadUInt32()); + TileIndexPerMip = reader.ReadTArray(() => reader.ReadUInt32()); + TileOffsetInChunk = reader.ReadTArray(() => reader.ReadUInt32()); + + LayerTypes = new EPixelFormat[8]; + for (int Layer = 0; Layer < NumLayers; Layer++) + { + LayerTypes[Layer] = Enum.Parse(reader.ReadFString()); + } + + Chunks = new FVirtualTextureDataChunk[reader.ReadInt32()]; + for (int ChunkId = 0; ChunkId < Chunks.Length; ChunkId++) + { + Chunks[ChunkId] = new FVirtualTextureDataChunk(reader, ubulk, bulkOffset, NumLayers); + } + } + } +} diff --git a/FModel/PakReader/Parsers/Objects/FVirtualTextureDataChunk.cs b/FModel/PakReader/Parsers/Objects/FVirtualTextureDataChunk.cs new file mode 100644 index 00000000..7a3bf1f7 --- /dev/null +++ b/FModel/PakReader/Parsers/Objects/FVirtualTextureDataChunk.cs @@ -0,0 +1,30 @@ +using System.IO; + +namespace PakReader.Parsers.Objects +{ + public readonly struct FVirtualTextureDataChunk + { + public readonly FByteBulkData BulkData; + public readonly uint SizeInBytes; + public readonly uint CodecPayloadSize; + public readonly ushort[] CodecPayloadOffset; + public readonly EVirtualTextureCodec[] CodecType; + + internal FVirtualTextureDataChunk(BinaryReader reader, Stream ubulk, long bulkOffset, uint numLayers) + { + CodecPayloadOffset = new ushort[8]; + CodecType = new EVirtualTextureCodec[8]; + + SizeInBytes = reader.ReadUInt32(); + CodecPayloadSize = reader.ReadUInt32(); + for (int LayerIndex = 0; LayerIndex < numLayers; ++LayerIndex) + { + byte CodecTypeAsByte = reader.ReadByte(); + CodecType[LayerIndex] = (EVirtualTextureCodec)CodecTypeAsByte; + CodecPayloadOffset[LayerIndex] = reader.ReadUInt16(); + } + + BulkData = new FByteBulkData(reader, ubulk, bulkOffset); + } + } +} diff --git a/FModel/PakReader/Parsers/OodleStream.cs b/FModel/PakReader/Parsers/OodleStream.cs new file mode 100644 index 00000000..191dcbce --- /dev/null +++ b/FModel/PakReader/Parsers/OodleStream.cs @@ -0,0 +1,84 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace PakReader.Parsers +{ + /// + /// https://gist.github.com/Scobalula/37229307de57de685d16ec621d5aceb5 + /// + public class OodleStream : Stream + { + protected internal Stream _baseStream; + bool _disposed; + + public OodleStream(byte[] input, long decompressedLength) + { + _baseStream = new MemoryStream(Decompress(input, decompressedLength), false) + { + Position = 0 + }; + } + + /// + /// Oodle Library Path + /// + private const string OodleLibraryPath = "oo2core_5_win64"; + + protected override void Dispose(bool disposing) + { + try + { + if (!_disposed) + { + if (disposing && (this._baseStream != null)) + this._baseStream.Dispose(); + _disposed = true; + } + } + finally + { + base.Dispose(disposing); + } + } + + /// + /// Oodle64 Decompression Method + /// + [DllImport(OodleLibraryPath, CallingConvention = CallingConvention.Cdecl)] + private static extern long OodleLZ_Decompress(byte[] buffer, long bufferSize, byte[] result, long outputBufferSize, int a, int b, int c, long d, long e, long f, long g, long h, long i, int ThreadModule); + + /// + /// Decompresses a byte array of Oodle Compressed Data (Requires Oodle DLL) + /// + /// Input Compressed Data + /// Decompressed Size + /// Resulting Array if success, otherwise null. + public static byte[] Decompress(byte[] input, long decompressedLength) + { + // Resulting decompressed Data + byte[] result = new byte[decompressedLength]; + // Decode the data (other parameters such as callbacks not required) + long decodedSize = OodleLZ_Decompress(input, input.Length, result, decompressedLength, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3); + // Check did we fail + if (decodedSize == 0) return null; + // Return Result + return result; + } + public override int Read(byte[] buffer, int offset, int count) + { + if (_disposed) throw new ObjectDisposedException("OodleStream"); + return _baseStream.Read(buffer, offset, count); + } + + public override void Flush() => throw new NotImplementedException(); + public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException(); + public override void SetLength(long value) => throw new NotImplementedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException(); + public override bool CanRead => throw new NotImplementedException(); + public override bool CanSeek => throw new NotImplementedException(); + public override bool CanWrite => throw new NotImplementedException(); + public override long Length => throw new NotImplementedException(); + public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + } +} diff --git a/FModel/PakReader/Parsers/PackageReader.cs b/FModel/PakReader/Parsers/PackageReader.cs index 238a9349..8da12dce 100644 --- a/FModel/PakReader/Parsers/PackageReader.cs +++ b/FModel/PakReader/Parsers/PackageReader.cs @@ -51,6 +51,7 @@ namespace PakReader.Parsers DataExports[i] = ExportType.String switch { "Texture2D" => new UTexture2D(this, ubulk, ExportMap.Sum(e => e.SerialSize) + PackageFileSummary.TotalHeaderSize), + "VirtualTexture2D" => new UTexture2D(this, ubulk, ExportMap.Sum(e => e.SerialSize) + PackageFileSummary.TotalHeaderSize), "CurveTable" => new UCurveTable(this), "DataTable" => new UDataTable(this), "FontFace" => new UFontFace(this, ubulk), diff --git a/FModel/PakReader/Parsers/PropertyTagData/BaseProperty.cs b/FModel/PakReader/Parsers/PropertyTagData/BaseProperty.cs index 3150df5d..f1227c78 100644 --- a/FModel/PakReader/Parsers/PropertyTagData/BaseProperty.cs +++ b/FModel/PakReader/Parsers/PropertyTagData/BaseProperty.cs @@ -8,7 +8,7 @@ namespace PakReader.Parsers.PropertyTagData { BaseProperty prop = type.String switch { - "ByteProperty" => new ByteProperty(reader, readType), + "ByteProperty" => new ByteProperty(reader, tag, readType), "BoolProperty" => new BoolProperty(reader, tag, readType), "IntProperty" => new IntProperty(reader), "FloatProperty" => new FloatProperty(reader), @@ -43,7 +43,7 @@ namespace PakReader.Parsers.PropertyTagData { object prop = type.String switch { - "ByteProperty" => new ByteProperty(reader, readType).Value, + "ByteProperty" => new ByteProperty(reader, tag, readType).Value, "BoolProperty" => new BoolProperty(reader, tag, readType).Value, "IntProperty" => new IntProperty(reader).Value, "FloatProperty" => new FloatProperty(reader).Value, diff --git a/FModel/PakReader/Parsers/PropertyTagData/ByteProperty.cs b/FModel/PakReader/Parsers/PropertyTagData/ByteProperty.cs index 1c5d0a44..8d12b6c3 100644 --- a/FModel/PakReader/Parsers/PropertyTagData/ByteProperty.cs +++ b/FModel/PakReader/Parsers/PropertyTagData/ByteProperty.cs @@ -1,15 +1,16 @@ -using System; +using PakReader.Parsers.Objects; +using System; namespace PakReader.Parsers.PropertyTagData { public sealed class ByteProperty : BaseProperty { - internal ByteProperty(PackageReader reader, ReadType readType) + internal ByteProperty(PackageReader reader, FPropertyTag tag, ReadType readType) { Position = reader.Position; Value = readType switch { - ReadType.NORMAL => (byte)reader.ReadFName().Index, + ReadType.NORMAL => tag.EnumName.IsNone ? reader.ReadByte() : (byte)reader.ReadFName().Index, ReadType.MAP => (byte)reader.ReadUInt32(), ReadType.ARRAY => reader.ReadByte(), _ => throw new ArgumentOutOfRangeException(nameof(readType)), diff --git a/FModel/PakReader/WwiseReader.cs b/FModel/PakReader/WwiseReader.cs index 8540300b..efd6eaea 100644 --- a/FModel/PakReader/WwiseReader.cs +++ b/FModel/PakReader/WwiseReader.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Text; -namespace FModel.PakReader +namespace PakReader { /// /// http://wiki.xentax.com/index.php/Wwise_SoundBank_(*.bnk) @@ -98,7 +98,7 @@ namespace FModel.PakReader string key = $"{didxSection.WemFilesRef[i].Id}.wem"; if (stidSection != null && stidSection.SoundBanks.TryGetValue(didxSection.WemFilesRef[i].Id, out string name)) key = $"{name}.wem"; - else if (Globals.Game.ActualGame == EGame.Valorant && ValoloWwiseDict.ValorantWemToName.TryGetValue(didxSection.WemFilesRef[i].Id, out string hardcodedname)) + else if (FModel.Globals.Game.ActualGame == FModel.EGame.Valorant && ValoloWwiseDict.ValorantWemToName.TryGetValue(didxSection.WemFilesRef[i].Id, out string hardcodedname)) key = $"{hardcodedname}.wem"; AudioFiles[key] = dataSection.WemFiles[i]; @@ -113,7 +113,7 @@ namespace FModel.PakReader string key = $"{entry.Path.ToUpper()}_{entry.NameHash}.wem"; if (stidSection != null && stidSection.SoundBanks.TryGetValue(entry.NameHash, out string name)) key = $"{name}.wem"; - else if (Globals.Game.ActualGame == EGame.Valorant && ValoloWwiseDict.ValorantWemToName.TryGetValue(entry.NameHash, out string hardcodedname)) + else if (FModel.Globals.Game.ActualGame == FModel.EGame.Valorant && ValoloWwiseDict.ValorantWemToName.TryGetValue(entry.NameHash, out string hardcodedname)) key = $"{hardcodedname}.wem"; AudioFiles[key] = entry.Data; diff --git a/FModel/Windows/SoundPlayer/AudioPlayer.xaml.cs b/FModel/Windows/SoundPlayer/AudioPlayer.xaml.cs index 86d69792..9d63728d 100644 --- a/FModel/Windows/SoundPlayer/AudioPlayer.xaml.cs +++ b/FModel/Windows/SoundPlayer/AudioPlayer.xaml.cs @@ -5,6 +5,7 @@ using FModel.ViewModels.ListBox; using FModel.ViewModels.SoundPlayer; using FModel.Windows.SoundPlayer.Visualization; using Microsoft.Win32; +using PakReader; using System; using System.Collections.Generic; using System.Diagnostics;