From 331aaedaa3341936464af63a67035233c2fa850f Mon Sep 17 00:00:00 2001 From: LJ Date: Sat, 30 Dec 2023 23:01:39 +0800 Subject: [PATCH] updated logic to work with new warzone fastfiles, and cleaned up code. --- ...xtractor.csproj => COD FF Extractor.csproj | 2 +- MW2 FF Extractor.sln => COD FF Extractor.sln | 2 +- Decompression/{Decompressor.cs => MW2.cs} | 146 ++++++++------- Decompression/Structs.cs | 17 +- Decompression/Warzone.cs | 168 ++++++++++++++++++ Program.cs | 131 ++++++++++++-- Properties/launchSettings.json | 8 + README.md | 11 +- Utility/Utils.cs | 7 +- 9 files changed, 395 insertions(+), 97 deletions(-) rename MW2 FF Extractor.csproj => COD FF Extractor.csproj (92%) rename MW2 FF Extractor.sln => COD FF Extractor.sln (88%) rename Decompression/{Decompressor.cs => MW2.cs} (57%) create mode 100644 Decompression/Warzone.cs create mode 100644 Properties/launchSettings.json diff --git a/MW2 FF Extractor.csproj b/COD FF Extractor.csproj similarity index 92% rename from MW2 FF Extractor.csproj rename to COD FF Extractor.csproj index 09b831f..647c8f7 100644 --- a/MW2 FF Extractor.csproj +++ b/COD FF Extractor.csproj @@ -3,7 +3,7 @@ Exe net7.0 - MW2_FF_Extractor + MW_FF_Extractor enable enable diff --git a/MW2 FF Extractor.sln b/COD FF Extractor.sln similarity index 88% rename from MW2 FF Extractor.sln rename to COD FF Extractor.sln index 0f177f4..3d57081 100644 --- a/MW2 FF Extractor.sln +++ b/COD FF Extractor.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.4.33103.184 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MW2 FF Extractor", "MW2 FF Extractor.csproj", "{41E9FEEC-3880-4359-B9C9-19B1BAF69543}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "COD FF Extractor", "COD FF Extractor.csproj", "{41E9FEEC-3880-4359-B9C9-19B1BAF69543}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/Decompression/Decompressor.cs b/Decompression/MW2.cs similarity index 57% rename from Decompression/Decompressor.cs rename to Decompression/MW2.cs index ddc3964..f1ded5d 100644 --- a/Decompression/Decompressor.cs +++ b/Decompression/MW2.cs @@ -1,57 +1,17 @@ -using System.Text; -using static MW2_FF_Extractor.Structs; - -namespace MW2_FF_Extractor +using COD_FF_Extractor; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static COD_FF_Extractor.Structs; + +namespace COD_FF_Extractor.Decompression { - public static class Decompressor + class MW2 { - // Decompresses FastFiles. returns -1 if it is an original FastFile, if it's a patch FastFile return the size of the patched FastFile - public static long Decompress(string path, string outPath) + private static void decompressBlocks(BinaryReader reader, BinaryWriter writer) { - BinaryReader reader = Utils.OpenBinaryReader(path); - if (reader == null) - throw new Exception(String.Format("Unable to open file {0}.", path)); - - BinaryWriter writer = Utils.OpenBinaryWriter(outPath); - if (writer == null) - throw new Exception(String.Format("Unable to create file {0}.", outPath)); - - // Get FF type - byte[] FFMagic = reader.ReadBytes(0x08); - FastFileType FFType; - - if (Utils.CompareArray(FFMagic, FFHeaderMagic)) - FFType = FastFileType.REGULAR; - else if (Utils.CompareArray(FFMagic, FFPatchHeaderMagic)) - FFType = FastFileType.PATCH; - else - throw new Exception("Invalid FF magic."); - - long DiffSize = -1; - if (FFType == FastFileType.PATCH) - { - // Check if patch FF is empty - reader.BaseStream.Position = 0x28; - if (reader.ReadInt64() == 0) //Read patch FF compressed size - return DiffSize; - - // Get size of FF after patching - reader.BaseStream.Position = 0x140; - DiffSize = reader.ReadInt64(); - } - - // Read FF name - if(FFType == FastFileType.REGULAR) - reader.BaseStream.Position = 0x208; - else - reader.BaseStream.Position = 0x318; - string FastFileName = Encoding.UTF8.GetString(reader.ReadBytes(0x40)).Replace("\x00", String.Empty); - - // Read the block header - if (FFType == FastFileType.REGULAR) - reader.BaseStream.Position = 0x80DC; - else - reader.BaseStream.Position = 0x81EC; BlockHeader headerBlock = new BlockHeader(); headerBlock.decompressedDataLen = reader.ReadInt32(); reader.BaseStream.Position += 0x3; @@ -106,7 +66,7 @@ public static long Decompress(string path, string outPath) default: throw new Exception("Unknown type of compression!"); } - + if (decompressedData == null) throw new Exception("Decompressor returned null!"); @@ -115,29 +75,77 @@ public static long Decompress(string path, string outPath) decompressedDataCount += block.decompressedLen; loopCount++; } + } + + private static void decompressRegularFF(BinaryReader reader, BinaryWriter writer) + { + // move to start of the signed fast file + long fastFileStart = 0xDC; + reader.BaseStream.Position = fastFileStart; + + // skip SHA256 hashes + reader.BaseStream.Position += 0x8000; + + decompressBlocks(reader, writer); + } + + private static void decompressPatchFF(BinaryReader reader, BinaryWriter writer) + { + reader.BaseStream.Position = 0x28; + if (reader.ReadInt32() == 0) // check if it's an empty patch file, exit if it is + return; + + // move to start of the signed fast file + long fastFileStart = 0x1EC; + reader.BaseStream.Position = fastFileStart; + + // skip SHA256 hashes + reader.BaseStream.Position += 0x8000; + + decompressBlocks(reader, writer); + } + + public static void decompress(string path, string outPath) + { + BinaryReader reader = Utils.OpenBinaryReader(path); + if (reader == null) + throw new Exception(String.Format("Unable to open file {0}.", path)); + + BinaryWriter writer = Utils.OpenBinaryWriter(outPath); + if (writer == null) + throw new Exception(String.Format("Unable to create file {0}.", outPath)); + + byte[] FFMagic = reader.ReadBytes(0x08); + + if (Utils.CompareArray(FFMagic, FFHeaderMagic)) + { + decompressRegularFF(reader, writer); + } + else if (Utils.CompareArray(FFMagic, FFPatchHeaderMagic)) + { + decompressPatchFF(reader, writer); + } + else + throw new Exception("Invalid FF magic."); reader.Close(); writer.Close(); - - // diff the files if there is a patch file - string FFPatchPath = path.Replace(".ff", ".fp"); - if (DiffSize == -1 && File.Exists(FFPatchPath)) - { - long outSize = Decompress(FFPatchPath, FFPatchPath + ".tmp"); + } - if(outSize > 0) - { - byte[] patchedFF = MWBDiff.BDiff.patchData(File.ReadAllBytes(outPath), File.ReadAllBytes(FFPatchPath + ".tmp"), outSize); + // gets the resulting size of a fastfile when patched with a patch fastfile + // patchFilePath must be the path to a patch file + public static int getDiffResultSizeFromFile(string patchFilePath) + { + BinaryReader reader = Utils.OpenBinaryReader(patchFilePath); + if (reader == null) + throw new Exception(String.Format("Unable to open file {0}.", patchFilePath)); - if(patchedFF == null) - throw new Exception("patchData returned null! outSize: " + outSize); + reader.BaseStream.Position = 0x140; + int diffResultSize = reader.ReadInt32(); - File.WriteAllBytes(outPath, patchedFF); // overwrite already written non patched FF - File.Delete(FFPatchPath + ".tmp"); - } - } - - return DiffSize; + reader.Close(); + + return diffResultSize; } } } diff --git a/Decompression/Structs.cs b/Decompression/Structs.cs index c953fc7..86ed0b9 100644 --- a/Decompression/Structs.cs +++ b/Decompression/Structs.cs @@ -1,15 +1,15 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Reflection.PortableExecutable; using System.Text; -using System.Threading.Tasks; +using static COD_FF_Extractor.Structs; -namespace MW2_FF_Extractor +namespace COD_FF_Extractor { public static class Structs { + //IWffa100 public static readonly byte[] FFHeaderMagic = new byte[] { 0x49, 0x57, 0x66, 0x66, 0x61, 0x31, 0x30, 0x30 }; + //IWffd100 public static readonly byte[] FFPatchHeaderMagic = new byte[] { 0x49, 0x57, 0x66, 0x66, 0x64, 0x31, 0x30, 0x30 }; public enum FastFileType @@ -17,6 +17,13 @@ public enum FastFileType REGULAR, PATCH } + public struct FastFileHeader + { + public byte[] Magic; + public int Version; + public byte isCompressed; + public byte isEncrypted; + } public struct BlockHeader { diff --git a/Decompression/Warzone.cs b/Decompression/Warzone.cs new file mode 100644 index 0000000..464ef06 --- /dev/null +++ b/Decompression/Warzone.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection.PortableExecutable; +using System.Text; +using System.Threading.Tasks; +using static COD_FF_Extractor.Structs; + +namespace COD_FF_Extractor.Decompression +{ + class Warzone + { + private static FastFileHeader ReadFastFileHeader(BinaryReader reader) + { + FastFileHeader header = new FastFileHeader(); + + reader.BaseStream.Position = 0; + header.Magic = reader.ReadBytes(0x08); + header.Version = reader.ReadInt32(); + reader.ReadInt32(); + header.isCompressed = reader.ReadByte(); + header.isEncrypted = reader.ReadByte(); + + return header; + } + + private static void decompressBlocks(BinaryReader reader, BinaryWriter writer) + { + // read block header + BlockHeader headerBlock = new BlockHeader(); + headerBlock.decompressedDataLen = reader.ReadInt32(); + reader.BaseStream.Position += 0x3; + headerBlock.compressionType = reader.ReadByte(); + + // start decompressing blocks + long totalDecompressedDataSize = 0; + int loopCount = 1; + Block block = new Block(); + while (totalDecompressedDataSize < headerBlock.decompressedDataLen) + { +#if DEBUG + Console.WriteLine("Block Offset: {0:X}, total: {1:X}", reader.BaseStream.Position, totalDecompressedDataSize); +#endif + + if (loopCount == 512) + { + // Skip an RSA block + reader.ReadBytes(0x4000); + loopCount = 0; + } + + block.compressedLen = (reader.ReadInt32() + 3) & 0xFFFFFFC; // calc allignment + block.decompressedLen = reader.ReadInt32(); + reader.BaseStream.Position += 0x4; + + byte[] compressedData = reader.ReadBytes(block.compressedLen); + byte[] decompressedData; + switch (headerBlock.compressionType) + { + // Decompress None + case 1: + decompressedData = compressedData; + break; + + // unknown + case 4: + case 5: + throw new Exception("unimplemented compression type!"); + + // Decompress Oodle + case 6: + case 7: + case 12: + case 13: + case 14: + case 15: + case 16: + case 17: + decompressedData = OodleSharp.Oodle.Decompress(compressedData, block.compressedLen, block.decompressedLen); + break; + + default: + throw new Exception("Unknown type of compression!"); + } + + if (decompressedData == null) + throw new Exception("Decompressor returned null!"); + + writer.Write(decompressedData); + + totalDecompressedDataSize += block.decompressedLen; + loopCount++; + } + } + + private static void decompressRegularFF(BinaryReader reader, BinaryWriter writer) + { + // move to start of the signed fast file + long fastFileStart = 0x124; + reader.BaseStream.Position = fastFileStart; + + // skip SHA256 hashes + reader.BaseStream.Position += 0x8000; + + decompressBlocks(reader, writer); + } + + private static void decompressPatchFF(BinaryReader reader, BinaryWriter writer) + { + reader.BaseStream.Position = 0x30; + if (reader.ReadInt32() == 0) // check if it's an empty patch file, exit if it is + return; + + // move to start of the signed fast file + long fastFileStart = 0x23C; + reader.BaseStream.Position = fastFileStart; + + // skip SHA256 hashes + reader.BaseStream.Position += 0x8000; + + decompressBlocks(reader, writer); + } + + public static void decompress(string path, string outPath) + { + BinaryReader reader = Utils.OpenBinaryReader(path); + if (reader == null) + throw new Exception(String.Format("Unable to open file {0}.", path)); + + BinaryWriter writer = Utils.OpenBinaryWriter(outPath); + if (writer == null) + throw new Exception(String.Format("Unable to create file {0}.", outPath)); + + FastFileHeader header = ReadFastFileHeader(reader); + + if (Utils.CompareArray(header.Magic, FFHeaderMagic)) // regular FastFile + { + decompressRegularFF(reader, writer); + } + else if (Utils.CompareArray(header.Magic, FFPatchHeaderMagic)) // patch FastFile + { + decompressPatchFF(reader, writer); + } + else + throw new Exception("Invalid FF magic."); + + reader.Close(); + writer.Close(); + } + + // gets the resulting size of a fastfile when patched with a patch fastfile + // patchFilePath must be the path to a patch file + public static int getDiffResultSizeFromFile(string patchFilePath) + { + BinaryReader reader = Utils.OpenBinaryReader(patchFilePath); + if (reader == null) + throw new Exception(String.Format("Unable to open file {0}.", patchFilePath)); + + reader.BaseStream.Position = 0x150; + int diffResultSize = reader.ReadInt32(); + + reader.Close(); + + return diffResultSize; + } + } +} diff --git a/Program.cs b/Program.cs index 6a16453..e266b78 100644 --- a/Program.cs +++ b/Program.cs @@ -1,54 +1,151 @@ -using System.IO; +using COD_FF_Extractor.Decompression; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using static System.Runtime.InteropServices.JavaScript.JSType; -namespace MW2_FF_Extractor +namespace COD_FF_Extractor { internal class Program { + static string outputPath = "output"; + + enum GAME_VERSION + { + WARZONE, + MW2 + } + static void checkRequiredFiles() { if (!File.Exists("dll\\oo2core_8_win64.dll")) throw new Exception("dll\\oo2core_8_win64.dll not found!"); - if(!File.Exists("dll\\MWBDiff.dll")) + if (!File.Exists("dll\\MWBDiff.dll")) throw new Exception("dll\\MWBDiff.dll not found!"); + + if (!Directory.Exists(outputPath)) + Directory.CreateDirectory(outputPath); } - static void decompressFile(string filePath) + static void decompressUsingGameVersion(string path, string outPath, GAME_VERSION gameVersion) { - Console.WriteLine("Decompressing {0}", filePath); - - Decompressor.Decompress(filePath, filePath + ".decomp"); + if (gameVersion == GAME_VERSION.WARZONE) + Warzone.decompress(path, outPath); + else if (gameVersion == GAME_VERSION.MW2) + MW2.decompress(path, outPath); + } - Console.WriteLine("Decompression successful!"); + static int getDiffResultSizeUsingGameVersion(string patchFilePath, GAME_VERSION gameVersion) + { + if (gameVersion == GAME_VERSION.WARZONE) + return Warzone.getDiffResultSizeFromFile(patchFilePath); + else if (gameVersion == GAME_VERSION.MW2) + return MW2.getDiffResultSizeFromFile(patchFilePath); + else + return 0; + } + + static void decompressFile(string filePath, GAME_VERSION gameVersion) + { + string fileName = Path.GetFileName(filePath); + + Console.WriteLine("Decompressing {0}...", fileName); + + string resultPath = Path.Combine(outputPath, fileName + ".decomp"); + decompressUsingGameVersion(filePath, resultPath, gameVersion); + + string diffFilePath = filePath.Replace(".ff", ".fp"); + string diffFileName = Path.GetFileName(diffFilePath); + if (File.Exists(diffFilePath)) + { + Console.WriteLine("Applying patch file..."); + + string diffResultPath = Path.Combine(outputPath, diffFileName + ".diff"); + decompressUsingGameVersion(diffFilePath, diffResultPath, gameVersion); + + if (!Utils.IsFileEmpty(diffResultPath)) + { + int fileSizeAfterPatching = getDiffResultSizeUsingGameVersion(diffFilePath, gameVersion); + + byte[] patchedData = MWBDiff.BDiff.patchData(File.ReadAllBytes(resultPath), File.ReadAllBytes(diffResultPath), fileSizeAfterPatching); + + File.WriteAllBytes(resultPath, patchedData); + } + + File.Delete(diffResultPath); + } + + + Console.WriteLine("Sucessful!"); } static void Main(string[] args) { checkRequiredFiles(); - if (args.Length != 1) + if (args.Length != 2) { - Console.WriteLine("Incorrect argument count!"); - Console.WriteLine("usage is: MW2 FF Extractor.exe "); + Console.WriteLine("Incorrect usage! Usage:"); + Console.WriteLine("COD FF Extractor.exe "); + Console.WriteLine("GAME can be: \"MW2\" or \"WZ\""); + Console.WriteLine("PATH is a path to a FastFile file or a folder of FastFiles"); return; } - if(Utils.IsDirectory(args[0])) + string gameVersionStr = args[0]; + string path = args[1]; + + GAME_VERSION gameVersion; + if (gameVersionStr.Equals("WZ")) + gameVersion = GAME_VERSION.WARZONE; + else if (gameVersionStr.Equals("MW2")) + gameVersion = GAME_VERSION.WARZONE; + else + throw new Exception(string.Format("Unknown game type {0}!", gameVersionStr)); + + if (Utils.IsDirectory(path)) { - foreach (string file in Directory.GetFiles(args[0], "*.ff")) + foreach (string file in Directory.GetFiles(path, "*.ff")) { - decompressFile(file); + decompressFile(file, gameVersion); } } else { - if(args[0].EndsWith(".ff")) - decompressFile(args[0]); + if (args[0].EndsWith(".ff")) + decompressFile(path, gameVersion); else throw new Exception("File must end with .ff!"); } + + + + + } } -} \ No newline at end of file +} + +//if (args.Length != 2) +//{ +// Console.WriteLine("Incorrect argument count!"); +// Console.WriteLine("usage is: MW2 FF Extractor.exe "); +// return; +//} +// +//if(Utils.IsDirectory(args[0])) +//{ +// foreach (string file in Directory.GetFiles(args[0], "*.ff")) +// { +// decompressFile(file); +// } +//} +//else +//{ +// if(args[0].EndsWith(".ff")) +// decompressFile(args[0]); +// else +// throw new Exception("File must end with .ff!"); +//} \ No newline at end of file diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100644 index 0000000..9a5985c --- /dev/null +++ b/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "MW2 FF Extractor": { + "commandName": "Project", + "commandLineArgs": "WZ \"E:\\Program Files\\Steam\\steamapps\\common\\Call of Duty HQ\"" + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index f1012d9..63d2a53 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,18 @@ # Modern Warfare FastFile Extractor A uility to extract raw FastFiles and patch them from compressed FastFiles. -Currently only works with Modern Warfare 2022 FastFiles. +Works with warzone fastfiles, and should work with MW2 fastfiles. # Usage -`MW2 FF Extractor.exe ` +`COD FF Extractor.exe ` -All files ending with .ff will be parsed if it s a path to a folder. The file must end with ".ff" if its a path to a file as it is required to check if there is a patch file. +`GAME` can be either: `MW2` or `WZ`. +`PATH` is a path to a FastFile or a path to a folder of fastfiles. + +All output files are written to the `output` folder. + +All files ending with .ff will be parsed if it's a path to a folder. The file must end with ".ff" if its a path to a file as it is required to check if there is a patch file. Use CascView (http://www.zezula.net/en/casc/main.html) if you are using blizzard launcher to open the .000 - .XXX files. diff --git a/Utility/Utils.cs b/Utility/Utils.cs index 07174da..fcd055c 100644 --- a/Utility/Utils.cs +++ b/Utility/Utils.cs @@ -1,7 +1,7 @@ using System; using System.IO; -namespace MW2_FF_Extractor +namespace COD_FF_Extractor { public static class Utils { @@ -49,5 +49,10 @@ public static bool IsDirectory(string path) { return (File.GetAttributes(path) & FileAttributes.Directory) == FileAttributes.Directory; } + + public static bool IsFileEmpty(string path) + { + return new FileInfo(path).Length == 0; + } } }