diff --git a/CSharp/DSDecmp/Formats/LZOvl.cs b/CSharp/DSDecmp/Formats/LZOvl.cs index 3f4decb..957db32 100644 --- a/CSharp/DSDecmp/Formats/LZOvl.cs +++ b/CSharp/DSDecmp/Formats/LZOvl.cs @@ -59,7 +59,7 @@ namespace DSDecmp.Formats uint extraSize = IOUtils.ToNDSu32(header, header.Length - 4); if (extraSize == 0) - return true; + return false; // do not decompress whenevr the last 4 bytes are 0; too many files have that. // if the extrasize is nonzero, the minimum header length is 8 bytes if (header.Length < 8) return false; diff --git a/CSharp/DSDecmp/NewProgram.cs b/CSharp/DSDecmp/NewProgram.cs index 8b28317..5c162ee 100644 --- a/CSharp/DSDecmp/NewProgram.cs +++ b/CSharp/DSDecmp/NewProgram.cs @@ -1,12 +1,31 @@ using System; using System.Collections.Generic; using System.Text; +using DSDecmp.Formats.Nitro; +using DSDecmp.Formats; +using System.IO; namespace DSDecmp { public static class NewProgram { - public static void Main3(string[] args) + /// + /// The formats allowed when compressing a file. + /// + public enum Formats + { + LZOVL, // keep this as the first one, as only the end of a file may be LZ-ovl-compressed (and overlay files are oftenly double-compressed) + LZ10, + LZ11, + HUFF4, + HUFF8, + RLE, + HUFF, + NDS, + GBA, + } + + public static void Main(string[] args) { if (args.Length == 0) { @@ -16,7 +35,102 @@ namespace DSDecmp #endif return; } - + + int argIndex = 0; + bool compress = false; + Formats compressFormat = Formats.NDS; + + #region check for the -c option and its parameter(s) + if (args[argIndex].Equals("-c")) + { + argIndex++; + compress = true; + + if (args.Length < argIndex + 2) + { + Console.WriteLine("A compression format and input file is required in order to compress."); + Console.WriteLine(); + PrintUsage(); + return; + } + switch (args[argIndex].ToLower()) + { + case "lz10": compressFormat = Formats.LZ10; break; + case "lz11": compressFormat = Formats.LZ11; break; + case "lzovl": compressFormat = Formats.LZOVL; break; + case "rle": compressFormat = Formats.RLE; break; + case "huff4": compressFormat = Formats.HUFF4; break; + case "huff8": compressFormat = Formats.HUFF8; break; + case "huff": compressFormat = Formats.HUFF; break; + case "gba*": compressFormat = Formats.GBA; break; + case "nds*": compressFormat = Formats.NDS; break; + default: + Console.WriteLine("Unknown compression format " + args[argIndex]); + Console.WriteLine(); + PrintUsage(); + return; + } + argIndex++; + // handle the format options + switch (compressFormat) + { + case Formats.LZ10: + case Formats.GBA: + if (args[argIndex].Equals("-opt")) + { + LZ10.LookAhead = true; + argIndex++; + } + break; + case Formats.LZ11: + if (args[argIndex].Equals("-opt")) + { + LZ11.LookAhead = true; + argIndex++; + } + break; + case Formats.LZOVL: + if (args[argIndex].Equals("-opt")) + { + LZOvl.LookAhead = true; + argIndex++; + } + break; + case Formats.NDS: + if (args[argIndex].Equals("-opt")) + { + LZ10.LookAhead = true; + LZ11.LookAhead = true; + LZOvl.LookAhead = true; + argIndex++; + } + break; + } + } + #endregion + + if (args.Length < argIndex + 1) + throw new ArgumentException("No input file given."); + + bool guessExtension = false; + if (args[argIndex].Equals("-ge")) + { + guessExtension = true; + argIndex++; + } + + if (args.Length < argIndex + 1) + throw new ArgumentException("No input file given."); + + string input = args[argIndex++]; + string output = null; + if (args.Length > argIndex) + output = args[argIndex++]; + + if (compress) + Compress(input, output, compressFormat, guessExtension); + else + Decompress(input, output, guessExtension); #if DEBUG Console.ReadLine(); @@ -27,12 +141,14 @@ namespace DSDecmp { Console.WriteLine("DSDecmp - Decompressor for compression formats used on the NDS - by Barubary"); Console.WriteLine(); - Console.WriteLine("Usage:\tDSDecmp (-c FORMAT (FORMATOPTS)) (-ge) input (output)"); + Console.WriteLine("Usage:\tDSDecmp (-c FORMAT FORMATOPT*) (-ge) input (output)"); Console.WriteLine(); Console.WriteLine("Without the -c modifier, DSDecmp will decompress the input file to the output"); Console.WriteLine("file. If the output file is a directory, the output file will be placed in that"); Console.WriteLine("directory with the same filename as the original file. The extension will be"); Console.WriteLine("appended with a format-specific extension."); + Console.WriteLine("If the output is a nonexistent file or directory, it is assumed to be a"); + Console.WriteLine("directory iff there is no '.' in the name."); Console.WriteLine("The input can also be a directory. In that case, it would be the same as"); Console.WriteLine("calling DSDecmp for every non-directory in the given directory with the same"); Console.WriteLine("options, with one exception; the output is by default the input folder, but"); @@ -56,15 +172,20 @@ namespace DSDecmp Console.WriteLine(" --- utility 'formats' ---"); Console.WriteLine(" huff - The Huffman compression that gives the bext compression ratio."); Console.WriteLine(" nds* - The built-in compression format that gives the best compression"); - Console.WriteLine(" ratio."); + Console.WriteLine(" ratio. Will never compress using lzovl."); Console.WriteLine(" gba* - The built-in compression format that gives the best compression"); Console.WriteLine(" ratio, and is also supported by the GBA."); Console.WriteLine(); - Console.WriteLine("The following format options are available:"); - Console.WriteLine(" lz10, lz11 and lzovl:"); + Console.WriteLine("The following format options (FORMATOPT) are available:"); + Console.WriteLine(" lz10, lz11, lzovl, gba* and nds*:"); Console.WriteLine(" -opt : employs a better compression algorithm to boost the compression"); Console.WriteLine(" ratio. Not using this option will result in using the algorithm"); Console.WriteLine(" originally used to compress the game files."); + Console.WriteLine(" Using this option for the gba* and nds* will only have effect on"); + Console.WriteLine(" the lz10, lz11 and lzovl algorithms."); + Console.WriteLine(); + Console.WriteLine("If the input is a directory when the -c option, the default output directory"); + Console.WriteLine("is the input directory appended with '_cmp'."); Console.WriteLine(); Console.WriteLine("Supplying the -ge modifier together with the -c modifier, the extension of the"); Console.WriteLine("compressed files will be extended with the 'FORMAT' value that always results"); @@ -73,5 +194,371 @@ namespace DSDecmp Console.WriteLine("extended with .cdat"); } + + #region compression methods + + private static void Compress(string input, string output, Formats format, bool guessExtension) + { + if (!File.Exists(input) && !Directory.Exists(input)) + { + Console.WriteLine("Cannot compress a file or directory that does not exist (" + input + ")"); + return; + } + + // set the default value of the output + if (string.IsNullOrEmpty(output)) + { + if (Directory.Exists(input)) + { + if (!Directory.Exists(input + "_cmp")) + Directory.CreateDirectory(input + "_cmp"); + foreach (string file in Directory.GetFiles(input)) + { + Compress(file, input + "_cmp", format, guessExtension); + } + return; + } + else + { + if (!guessExtension) + output = input; // the .cdat extension is added automatically + else + output = Path.GetDirectoryName(input); + } + } + + if (Directory.Exists(input)) + { + if (!Directory.Exists(output)) + Directory.CreateDirectory(output); + foreach (string file in Directory.GetFiles(input)) + { + Compress(file, output, format, guessExtension); + } + return; + } + + + // compress the input + MemoryStream compressedData = new MemoryStream(); + Formats compressedFormat; + int outsize = DoCompress(input, compressedData, format, out compressedFormat); + if (outsize < 0) + return; + + if (Directory.Exists(output)) + { + output = CombinePaths(output, Path.GetFileName(input)); + } + if (Path.GetExtension(output) == ".dat") + output = RemoveExtension(output); + if (guessExtension) + output += "." + compressedFormat.ToString().ToLower(); + else + output += ".cdat"; + + using (FileStream outStream = File.Create(output)) + { + compressedData.WriteTo(outStream); + Console.WriteLine(compressedFormat.ToString() + "-compressed " + input + " to " + output); + } + } + + private static int DoCompress(string infile, MemoryStream output, Formats format, out Formats actualFormat) + { + CompressionFormat fmt = null; + switch (format) + { + case Formats.LZ10: fmt = new LZ10(); break; + case Formats.LZ11: fmt = new LZ11(); break; + case Formats.LZOVL: fmt = new LZOvl(); break; + case Formats.RLE: fmt = new RLE(); break; + case Formats.HUFF4: Huffman.CompressBlockSize = Huffman.BlockSize.FOURBIT; fmt = new Huffman(); break; + case Formats.HUFF8: Huffman.CompressBlockSize = Huffman.BlockSize.EIGHTBIT; fmt = new Huffman(); break; + case Formats.HUFF: + return CompressHuff(infile, output, out actualFormat); + case Formats.GBA: + return CompressGBA(infile, output, out actualFormat); + case Formats.NDS: + return CompressNDS(infile, output, out actualFormat); + default: + throw new Exception("Unhandled compression format " + format); + } + actualFormat = format; + + using (FileStream inStream = File.OpenRead(infile)) + { + try + { + return fmt.Compress(inStream, inStream.Length, output); + } + catch (Exception s) + { + // any exception generated by compression is a fatal exception + Console.WriteLine(s.Message); + return -1; + } + } + } + + private static int CompressHuff(string infile, MemoryStream output, out Formats actualFormat) + { + return CompressBest(infile, output, out actualFormat, Formats.HUFF4, Formats.HUFF8); + } + + private static int CompressGBA(string infile, MemoryStream output, out Formats actualFormat) + { + return CompressBest(infile, output, out actualFormat, Formats.HUFF4, Formats.HUFF8, Formats.LZ10, Formats.RLE); + } + + private static int CompressNDS(string infile, MemoryStream output, out Formats actualFormat) + { + return CompressBest(infile, output, out actualFormat, Formats.HUFF4, Formats.HUFF8, Formats.LZ10, Formats.LZ11, Formats.RLE); + } + + private static int CompressBest(string infile, MemoryStream output, out Formats actualFormat, params Formats[] formats) + { + // only read the input data once from the file. + byte[] inputData; + using (FileStream inStream = File.OpenRead(infile)) + { + inputData = new byte[inStream.Length]; + inStream.Read(inputData, 0, inputData.Length); + } + + MemoryStream bestOutput = null; + int minCompSize = int.MaxValue; + actualFormat = Formats.GBA; + foreach (Formats format in formats) + { + #region compress the file in each format, and save the best one + + MemoryStream currentOutput = new MemoryStream(); + CompressionFormat realFormat = null; + switch (format) + { + case Formats.HUFF4: Huffman.CompressBlockSize = Huffman.BlockSize.FOURBIT; realFormat = new Huffman(); break; + case Formats.HUFF8: Huffman.CompressBlockSize = Huffman.BlockSize.EIGHTBIT; realFormat = new Huffman(); break; + case Formats.LZ10: realFormat = new LZ10(); break; + case Formats.LZ11: realFormat = new LZ11(); break; + case Formats.LZOVL: realFormat = new LZOvl(); break; + case Formats.RLE: realFormat = new RLE(); break; + } + + int currentOutSize; + try + { + using (MemoryStream inStream = new MemoryStream(inputData)) + { + currentOutSize = realFormat.Compress(inStream, inStream.Length, currentOutput); + } + } + catch (InputTooLargeException i) + { + Console.WriteLine(i.Message); + actualFormat = format; + return -1; + } + catch (Exception) + { + continue; + } + if (currentOutSize < minCompSize) + { + bestOutput = currentOutput; + minCompSize = currentOutSize; + actualFormat = format; + } + + #endregion + } + + if (bestOutput == null) + { + Console.WriteLine("The file could not be compressed in any format."); + return -1; + } + bestOutput.WriteTo(output); + return minCompSize; + } + + #endregion + + #region decompression methods + + private static void Decompress(string input, string output, bool guessExtension) + { + if (!File.Exists(input) && !Directory.Exists(input)) + { + Console.WriteLine("Cannot decompress a file or directory that does not exist (" + input + ")"); + return; + } + + // set the default value of the output + if (string.IsNullOrEmpty(output)) + { + if (Directory.Exists(input)) + { + if (!Directory.Exists(input + "_dec")) + Directory.CreateDirectory(input + "_dec"); + foreach (string file in Directory.GetFiles(input)) + { + Decompress(file, input + "_dec", guessExtension); + } + return; + } + else + { + if (!guessExtension) + output = input; // '.dat' gets added automatically if -ge is not given + else + output = Path.GetDirectoryName(input); + } + } + + if (Directory.Exists(input)) + { + if (File.Exists(output)) + { + Console.WriteLine("Cannot decompress a folder to a single file."); + return; + } + if (!Directory.Exists(output)) + Directory.CreateDirectory(output); + foreach (string file in Directory.GetFiles(input)) + { + Decompress(file, output, guessExtension); + } + return; + } + + + byte[] inData; + using (FileStream inStream = File.OpenRead(input)) + { + inData = new byte[inStream.Length]; + inStream.Read(inData, 0, inData.Length); + } + + MemoryStream decompressedData = new MemoryStream(); + long decSize = -1; + Formats usedFormat = Formats.NDS; + // just try all formats, and stop once one has been found that can decompress it. + foreach (Formats f in Enum.GetValues(typeof(Formats))) + { + using (MemoryStream inStream = new MemoryStream(inData)) + { + decSize = Decompress(inStream, decompressedData, f); + if (decSize >= 0) + { + usedFormat = f; + break; + } + } + } + if (decSize < 0) + { + Console.WriteLine("Could not decompress " + input + "; no matching compression method found."); + return; + } + + if (Directory.Exists(output)) + { + output = CombinePaths(output, Path.GetFileName(input)); + } + + switch (Path.GetExtension(output)) + { + case ".cdat": + case ".lz10": + case ".lz11": + case ".lzovl": + case ".rle": + case ".huff4": + case ".huff8": + output = RemoveExtension(output); + break; + } + byte[] outData = decompressedData.ToArray(); + if (guessExtension) + { + string ext = ""; + for (int i = 0; i < 4; i++) + { + if ((outData[i] >= 'a' && outData[i] <= 'z') + || (outData[i] >= 'A' && outData[i] <= 'Z') + || char.IsDigit((char)outData[i])) + ext += (char)outData[i]; + else + break; + } + if (ext.Length > 0) + output += "." + ext; + else + output += ".dat"; + } + else + output += ".dat"; + + using (FileStream outStream = File.Create(output)) + { + outStream.Write(outData, 0, outData.Length); + Console.WriteLine(usedFormat.ToString() + "-decompressed " + input); + } + + } + + private static long Decompress(MemoryStream inputStream, MemoryStream output, Formats format) + { + CompressionFormat realFormat = null; + switch (format) + { + case Formats.HUFF: + realFormat = new Huffman(); break; + case Formats.LZ10: + realFormat = new LZ10(); break; + case Formats.LZ11: + realFormat = new LZ11(); break; + case Formats.LZOVL: + realFormat = new LZOvl(); break; + case Formats.RLE: + realFormat = new RLE(); break; + default: + return -1; + } + if (!realFormat.Supports(inputStream, inputStream.Length)) + return -1; + try + { + return realFormat.Decompress(inputStream, inputStream.Length, output); + } + catch (TooMuchInputException e) + { + Console.WriteLine(e.Message); + return output.Length; + } + catch (Exception e) + { + Console.WriteLine("Could not decompress using the " + format.ToString() + " format; " + e.Message); + return -1; + } + } + + #endregion + + private static string CombinePaths(string dir, string file) + { + if (Path.IsPathRooted(file)) + return file; + if (!dir.EndsWith(Path.DirectorySeparatorChar + "") + && !dir.EndsWith(Path.AltDirectorySeparatorChar + "")) + return dir + Path.DirectorySeparatorChar + file; + else + return dir + file; + } + private static string RemoveExtension(string path) + { + return Path.GetDirectoryName(path) + Path.DirectorySeparatorChar + Path.GetFileNameWithoutExtension(path); + } } } diff --git a/CSharp/bin/DSDecmp.exe b/CSharp/bin/DSDecmp.exe index 257337e..ff73417 100644 Binary files a/CSharp/bin/DSDecmp.exe and b/CSharp/bin/DSDecmp.exe differ