using System; using System.Diagnostics; using System.IO; using System.Text; using System.Text.RegularExpressions; using BinderTool.Core; using BinderTool.Core.Bdf4; using BinderTool.Core.Bdt5; using BinderTool.Core.Bhd5; using BinderTool.Core.Bhf4; using BinderTool.Core.Bnd4; using BinderTool.Core.Dcx; using BinderTool.Core.Enc; using BinderTool.Core.Fmg; using BinderTool.Core.Param; using BinderTool.Core.Sl2; using BinderTool.Core.Tpf; namespace BinderTool { public static class Program { private static void Main(string[] args) { if (args.Length == 0) { ShowUsageInfo(); return; } Options options; try { options = Options.Parse(args); } catch (FormatException e) { Console.WriteLine(e.Message); ShowUsageInfo(); return; } switch (options.InputType) { // These files have a single output file. case FileType.EncryptedBhd: case FileType.Bhd: case FileType.Dcx: case FileType.Fmg: break; default: Directory.CreateDirectory(options.OutputPath); break; } switch (options.InputType) { case FileType.Regulation: UnpackRegulationFile(options); break; case FileType.Dcx: UnpackDcxFile(options); break; case FileType.EncryptedBdt: UnpackBdtFile(options); break; case FileType.EncryptedBhd: UnpackBhdFile(options); break; case FileType.Bdt: UnpackBdf4File(options); break; case FileType.Bhd: UnpackBhf4File(options); break; case FileType.Bnd: UnpackBndFile(options); break; case FileType.Savegame: UnpackSl2File(options); break; case FileType.Tpf: UnpackTpfFile(options); break; case FileType.Param: UnpackParamFile(options); break; case FileType.Fmg: UnpackFmgFile(options); break; default: throw new ArgumentOutOfRangeException($"Unable to handle type '{options.InputType}'"); } } private static void ShowUsageInfo() { Console.WriteLine( "BinderTool by Atvaark\n" + " A tool for unpacking Dark Souls II/III Bdt, Bhd, Dcx, Sl2, Tpf, Param and Fmg files\n" + "Usage:\n" + " BinderTool file_path [output_path]\n" + "Examples:\n" + " BinderTool data1.bdt\n" + " BinderTool data1.bdt data1"); } private static void UnpackBdtFile(Options options) { FileNameDictionary dictionary = FileNameDictionary.OpenFromFile(options.InputGameVersion); string fileNameWithoutExtension = Path.GetFileName(options.InputPath).Replace("Ebl.bdt", "").Replace(".bdt", ""); string archiveName = fileNameWithoutExtension.ToLower(); using (Bdt5FileStream bdtStream = Bdt5FileStream.OpenFile(options.InputPath, FileMode.Open, FileAccess.Read)) { Bhd5File bhdFile = Bhd5File.Read( inputStream: DecryptBhdFile( filePath: Path.ChangeExtension(options.InputPath, "bhd"), version: options.InputGameVersion), version: options.InputGameVersion ); foreach (var bucket in bhdFile.GetBuckets()) { foreach (var entry in bucket.GetEntries()) { MemoryStream data; if (entry.FileSize == 0) { long fileSize; if (!TryReadFileSize(entry, bdtStream, out fileSize)) { Console.WriteLine($"Unable to determine the length of file '{entry.FileNameHash:D10}'"); continue; } entry.FileSize = fileSize; } if (entry.IsEncrypted) { data = bdtStream.Read(entry.FileOffset, entry.PaddedFileSize); CryptographyUtility.DecryptAesEcb(data, entry.AesKey.Key, entry.AesKey.Ranges); data.Position = 0; data.SetLength(entry.FileSize); } else { data = bdtStream.Read(entry.FileOffset, entry.FileSize); } string fileName; string dataExtension = GetDataExtension(data); bool fileNameFound = dictionary.TryGetFileName(entry.FileNameHash, archiveName, out fileName); if (!fileNameFound) { fileNameFound = dictionary.TryGetFileName(entry.FileNameHash, archiveName, dataExtension, out fileName); } string extension; if (fileNameFound) { extension = Path.GetExtension(fileName); if (dataExtension == ".dcx" && extension != ".dcx") { extension = ".dcx"; fileName += ".dcx"; } } else { extension = dataExtension; fileName = $"{entry.FileNameHash:D10}_{fileNameWithoutExtension}{extension}"; } if (extension == ".enc") { byte[] decryptionKey; if (DecryptionKeys.TryGetAesFileKey(Path.GetFileName(fileName), out decryptionKey)) { EncFile encFile = EncFile.ReadEncFile(data, decryptionKey); data = encFile.Data; fileName = Path.Combine(Path.GetDirectoryName(fileName), Path.GetFileNameWithoutExtension(fileName)); extension = Path.GetExtension(fileName); } else { Debug.WriteLine($"No decryption key for file \'{fileName}\' found."); } } if (extension == ".dcx") { DcxFile dcxFile = DcxFile.Read(data); data = new MemoryStream(dcxFile.Decompress()); fileName = Path.Combine(Path.GetDirectoryName(fileName), Path.GetFileNameWithoutExtension(fileName)); if (fileNameFound) { extension = Path.GetExtension(fileName); } else { extension = GetDataExtension(data); fileName += extension; } } Debug.WriteLine( "{0}\t{1}\t{2}\t{3}\t{4}\t{5}\t{6}\t{7}\t{8}", fileNameWithoutExtension, fileName, extension, entry.FileNameHash, entry.FileOffset, entry.FileSize, entry.PaddedFileSize, entry.IsEncrypted, fileNameFound); string newFileNamePath = Path.Combine(options.OutputPath, fileName); Directory.CreateDirectory(Path.GetDirectoryName(newFileNamePath)); File.WriteAllBytes(newFileNamePath, data.ToArray()); } } } } private static bool TryReadFileSize(Bhd5BucketEntry entry, Bdt5FileStream bdtStream, out long fileSize) { fileSize = 0; const int sampleLength = 48; MemoryStream data = bdtStream.Read(entry.FileOffset, sampleLength); if (entry.IsEncrypted) { data = CryptographyUtility.DecryptAesEcb(data, entry.AesKey.Key); } string sampleSignature; if (!TryGetAsciiSignature(data, 4, out sampleSignature) || sampleSignature != DcxFile.DcxSignature) { return false; } fileSize = DcxFile.DcxSize + DcxFile.ReadCompressedSize(data); return true; } private static string GetDataExtension(MemoryStream data) { string signature; string extension; if (TryGetAsciiSignature(data, 4, out signature) && TryGetFileExtension(signature, out extension)) { return extension; } if (TryGetUnicodeSignature(data, 4, out signature) && TryGetFileExtension(signature, out extension)) { return extension; } if (TryGetAsciiSignature(data, 26, out signature) && TryGetFileExtension(signature.Substring(12, 14), out extension)) { return extension; } //Debug.WriteLine($"Unknown signature: '{BitConverter.ToString(Encoding.ASCII.GetBytes(signature)).Replace("-", " ")}'"); return ".bin"; } private static bool TryGetAsciiSignature(MemoryStream stream, int signatureLength, out string signature) { const int asciiBytesPerChar = 1; return TryGetSignature(stream, Encoding.ASCII, asciiBytesPerChar, signatureLength, out signature); } private static bool TryGetUnicodeSignature(MemoryStream stream, int signatureLength, out string signature) { const int unicodeBytesPerChar = 2; return TryGetSignature(stream, Encoding.Unicode, unicodeBytesPerChar, signatureLength, out signature); } private static bool TryGetSignature(MemoryStream stream, Encoding encoding, int bytesPerChar, int signatureLength, out string signature) { signature = null; long startPosition = stream.Position; if (stream.Length - startPosition < bytesPerChar * signatureLength) { return false; } BinaryReader reader = new BinaryReader(stream, encoding, true); signature = new string(reader.ReadChars(signatureLength)); stream.Position = startPosition; return true; } private static bool TryGetFileExtension(string signature, out string extension) { switch (signature) { case "BND4": extension = ".bnd"; return true; case "BHF4": extension = ".bhd"; return true; case "BDF4": extension = ".bdt"; return true; case "DCX\0": extension = ".dcx"; return true; case "DDS ": extension = ".dds"; return true; case "TAE ": extension = ".tae"; return true; case "FSB5": extension = ".fsb"; return true; case "fsSL": case "fSSL": extension = ".esd"; return true; case "TPF\0": extension = ".tpf"; return true; case "PFBB": extension = ".pfbbin"; return true; case "OBJB": extension = ".breakobj"; return true; case "filt": extension = ".fltparam"; // DS II //extension = ".gparam"; // DS III return true; case "VSDF": extension = ".vsd"; return true; case "NVG2": extension = ".ngp"; return true; case "#BOM": extension = ".txt"; return true; case "\x1BLua": extension = ".lua"; // or .hks return true; case "RIFF": extension = ".fev"; return true; case "GFX\v": extension = ".gfx"; return true; case "SMD\0": extension = ".metaparam"; return true; case "SMDD": extension = ".metadebug"; return true; case "CLM2": extension = ".clm2"; return true; case "FLVE": extension = ".flver"; return true; case "F2TR": extension = ".flver2tri"; return true; case "FRTR": extension = ".tri"; return true; case "FXR\0": extension = ".fxr"; return true; case "ITLIMITER_INFO": extension = ".itl"; return true; case "EVD\0": extension = ".emevd"; return true; case "ENFL": extension = ".entryfilelist"; return true; case "NVMA": extension = ".nvma"; // ? return true; case "MSB ": extension = ".msb"; // ? return true; case "BJBO": extension = ".bjbo"; // ? return true; case "ONAV": extension = ".onav"; // ? return true; default: extension = ".bin"; return false; } } private static void UnpackBhdFile(Options options) { using (var inputStream = DecryptBhdFile(options.InputPath, options.InputGameVersion)) using (var outputStream = File.OpenWrite(options.OutputPath)) { inputStream.WriteTo(outputStream); } } private static void UnpackBndFile(Options options) { using (FileStream inputStream = new FileStream(options.InputPath, FileMode.Open, FileAccess.Read)) { UnpackBndFile(inputStream, options.OutputPath); } } private static void UnpackBndFile(Stream inputStream, string outputPath) { Bnd4File file = Bnd4File.ReadBnd4File(inputStream); foreach (var entry in file.Entries) { string fileName = FileNameDictionary.NormalizeFileName(entry.FileName); string outputFilePath = Path.Combine(outputPath, fileName); Directory.CreateDirectory(Path.GetDirectoryName(outputFilePath)); File.WriteAllBytes(outputFilePath, entry.EntryData); } } private static void UnpackSl2File(Options options) { using (FileStream inputStream = new FileStream(options.InputPath, FileMode.Open, FileAccess.Read)) { byte[] key = GetSavegameKey(options.InputGameVersion); Sl2File sl2File = Sl2File.ReadSl2File(inputStream, key); foreach (var userData in sl2File.UserData) { string outputFilePath = Path.Combine(options.OutputPath, userData.Name); File.WriteAllBytes(outputFilePath, userData.DecryptedUserData); } } } private static byte[] GetSavegameKey(GameVersion version) { byte[] key; switch (version) { case GameVersion.DarkSouls2: key = DecryptionKeys.UserDataKeyDs2; break; case GameVersion.DarkSouls3: key = DecryptionKeys.UserDataKeyDs3; break; default: key = new byte[16]; break; } return key; } private static void UnpackRegulationFile(Options options) { using (FileStream inputStream = new FileStream(options.InputPath, FileMode.Open, FileAccess.Read)) { byte[] key = GetRegulationKey(options.InputGameVersion); EncFile encryptedFile = EncFile.ReadEncFile(inputStream, key, options.InputGameVersion); DcxFile compressedRegulationFile = DcxFile.Read(encryptedFile.Data); UnpackBndFile(new MemoryStream(compressedRegulationFile.Decompress()), options.OutputPath); } } private static byte[] GetRegulationKey(GameVersion version) { byte[] key; switch (version) { case GameVersion.DarkSouls2: key = DecryptionKeys.RegulationFileKeyDs2; break; case GameVersion.DarkSouls3: key = DecryptionKeys.RegulationFileKeyDs3; break; default: key = new byte[16]; break; } return key; } private static void UnpackDcxFile(Options options) { string unpackedFileName = Path.GetFileNameWithoutExtension(options.InputPath); string outputFilePath = options.OutputPath; bool hasExtension = Path.GetExtension(unpackedFileName) != ""; using (FileStream inputStream = new FileStream(options.InputPath, FileMode.Open, FileAccess.Read)) { DcxFile dcxFile = DcxFile.Read(inputStream); byte[] decompressedData = dcxFile.Decompress(); if (!hasExtension) { string extension = GetDataExtension(new MemoryStream(decompressedData)); if (extension != ".dcx") { outputFilePath += extension; } } File.WriteAllBytes(outputFilePath, decompressedData); } } private static void UnpackBdf4File(Options options) { string bdfDirectoryPath = Path.GetDirectoryName(options.InputPath); string bhf4Extension = Path.GetExtension(options.InputPath).Replace("bdt", "bhd"); string bhf4FilePath = Path.Combine(bdfDirectoryPath, Path.GetFileNameWithoutExtension(options.InputPath) + bhf4Extension); if (!File.Exists(bhf4FilePath)) { // HACK: Adding 132 to a hash of a text that ends with XXX.bdt will give you the hash of XXX.bhd. string[] split = Path.GetFileNameWithoutExtension(options.InputPath).Split('_'); uint hash; if (uint.TryParse(split[0], out hash)) { hash += 132; split[0] = hash.ToString("D10"); bhf4FilePath = Path.Combine(bdfDirectoryPath, string.Join("_", split) + ".bhd"); } } using (Bdf4FileStream bdf4InputStream = Bdf4FileStream.OpenFile(options.InputPath, FileMode.Open, FileAccess.Read)) { Bhf4File bhf4File = Bhf4File.OpenBhf4File(bhf4FilePath); foreach (var entry in bhf4File.Entries) { MemoryStream data = bdf4InputStream.Read(entry.FileOffset, entry.FileSize); string fileName = entry.FileName; string fileExtension = Path.GetExtension(fileName); if (fileExtension == ".dcx") { DcxFile dcxFile = DcxFile.Read(data); data = new MemoryStream(dcxFile.Decompress()); fileName = Path.Combine(Path.GetDirectoryName(fileName), Path.GetFileNameWithoutExtension(fileName)); } string outputFilePath = Path.Combine(options.OutputPath, fileName); Directory.CreateDirectory(Path.GetDirectoryName(outputFilePath)); File.WriteAllBytes(outputFilePath, data.ToArray()); } } } private static void UnpackTpfFile(Options options) { using (FileStream inputStream = new FileStream(options.InputPath, FileMode.Open, FileAccess.Read)) { TpfFile tpfFile = TpfFile.OpenTpfFile(inputStream); foreach (var entry in tpfFile.Entries) { string outputFilePath = Path.Combine(options.OutputPath, entry.FileName); Directory.CreateDirectory(Path.GetDirectoryName(outputFilePath)); File.WriteAllBytes(outputFilePath, entry.Data); } } } private static void UnpackBhf4File(Options options) { Console.WriteLine($"The file : \'{options.InputPath}\' is already decrypted."); } private static MemoryStream DecryptBhdFile(string filePath, GameVersion version) { string fileDirectory = Path.GetDirectoryName(filePath) ?? string.Empty; string fileName = Path.GetFileName(filePath) ?? string.Empty; string key = null; switch (version) { case GameVersion.DarkSouls2: string keyFileName = Regex.Replace(fileName, @"Ebl\.bhd$", "KeyCode.pem", RegexOptions.IgnoreCase); string keyFilePath = Path.Combine(fileDirectory, keyFileName); if (File.Exists(keyFilePath)) { key = File.ReadAllText(keyFilePath); } break; case GameVersion.DarkSouls3: DecryptionKeys.TryGetRsaFileKey(fileName, out key); break; } if (key == null) { throw new ApplicationException($"Missing decryption key for file \'{fileName}\'"); } return CryptographyUtility.DecryptRsa(filePath, key); } private static void UnpackParamFile(Options options) { using (FileStream inputStream = new FileStream(options.InputPath, FileMode.Open, FileAccess.Read)) { ParamFile paramFile = ParamFile.ReadParamFile(inputStream); foreach (var entry in paramFile.Entries) { string entryName = $"{entry.Id:D10}.{paramFile.StructName}"; string outputFilePath = Path.Combine(options.OutputPath, entryName); Directory.CreateDirectory(Path.GetDirectoryName(outputFilePath)); File.WriteAllBytes(outputFilePath, entry.Data); } } } private static void UnpackFmgFile(Options options) { using (FileStream inputStream = new FileStream(options.InputPath, FileMode.Open, FileAccess.Read)) { FmgFile fmgFile = FmgFile.ReadFmgFile(inputStream); StringBuilder builder = new StringBuilder(); foreach (var entry in fmgFile.Entries) { string value = entry.Value .Replace("\r", "\\r") .Replace("\n", "\\n") .Replace("\t", "\\t"); builder.AppendLine($"{entry.Id}\t{value}"); } string outputPath = options.OutputPath + ".txt"; File.WriteAllText(outputPath, builder.ToString()); } } } }