diff --git a/HyoutaToolsLib/ProgramNames.cs b/HyoutaToolsLib/ProgramNames.cs index d02cc4c..8e03d4b 100644 --- a/HyoutaToolsLib/ProgramNames.cs +++ b/HyoutaToolsLib/ProgramNames.cs @@ -106,6 +106,7 @@ public class ProgramNames { { new KeyValuePair( new ProgramName( "Patches.Bps.BpsToTextConverter", "-" ), Patches.Bps.BpsToTextConverter.Execute) }, { new KeyValuePair( new ProgramName( "Patches.Bps.TextToBpsConverter", "-" ), Patches.Bps.TextToBpsConverter.Execute) }, { new KeyValuePair( new ProgramName( "Patches.Bps.Create", "-" ), Patches.Bps.Program.ExecuteCreate) }, + { new KeyValuePair( new ProgramName( "Tales.Compression.Decompress", "-" ), Tales.Compression.Program.Decompress) }, }; } } diff --git a/HyoutaToolsLib/Tales/Compression/Decompression.cs b/HyoutaToolsLib/Tales/Compression/Decompression.cs new file mode 100644 index 0000000..63a4b34 --- /dev/null +++ b/HyoutaToolsLib/Tales/Compression/Decompression.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HyoutaTools.Tales.Compression { + public static class Decompression { + // ported from https://github.com/AdmiralCurtiss/topdec + + private static void InitializeDictionary(byte[] dict) { + int offset = 0; + for (int i = 0; i < 0x100; ++i) { + dict[offset++] = (byte)(i); + dict[offset++] = (byte)(0); + dict[offset++] = (byte)(i); + dict[offset++] = (byte)(0); + dict[offset++] = (byte)(i); + dict[offset++] = (byte)(0); + dict[offset++] = (byte)(i); + dict[offset++] = (byte)(0); + } + for (int i = 0; i < 0x100; ++i) { + dict[offset++] = (byte)(i); + dict[offset++] = (byte)(0xff); + dict[offset++] = (byte)(i); + dict[offset++] = (byte)(0xff); + dict[offset++] = (byte)(i); + dict[offset++] = (byte)(0xff); + dict[offset++] = (byte)(i); + } + for (int i = 0; i < 0x100; ++i) { + dict[offset++] = 0; + } + } + + public static int decompress_reserve_extra_bytes() { + return 273; + } + + private static long decompress_internal(bool HasDict, + bool HasMultiByte, + System.IO.Stream compressed, + uint compressedLength, + System.IO.Stream uncompressed, + uint uncompressedLength) { + byte[] dict = new byte[0x1000]; + ulong inpos = 0; + ulong outpos = 0; + uint dictpos = 0; + + if (HasDict) { + InitializeDictionary(dict); + dictpos = HasMultiByte ? 0xfefu : 0xfeeu; + } + + int literalBits = 0; + while (true) { + if (outpos >= uncompressedLength) { + return (long)outpos; + } + if (inpos >= compressedLength) { + return -1; + } + + int isLiteralByte = (literalBits & 1); + literalBits = (literalBits >> 1); + if (literalBits == 0) { + literalBits = (byte)(compressed.ReadByte()); + ++inpos; + isLiteralByte = (literalBits & 1); + literalBits = (0x80 | (literalBits >> 1)); + } + if (isLiteralByte != 0) { + byte c = (byte)(compressed.ReadByte()); + uncompressed.WriteByte(c); + dict[dictpos] = c; + dictpos = (dictpos + 1u) & 0xfffu; + ++inpos; + ++outpos; + continue; + } + + if ((inpos + 1) >= compressedLength) { + return -1; + } + + byte bnext = (byte)(compressed.ReadByte()); + byte b = (byte)(compressed.ReadByte()); + byte blow = (byte)(b & 0xf); + byte bhigh = (byte)((b & 0xf0) >> 4); + byte nibble1 = HasDict ? blow : bhigh; + byte nibble2 = HasDict ? bhigh : blow; + if (HasMultiByte && (nibble1 == 0xf)) { + // multiple copies of the same byte + + if (nibble2 == 0) { + if ((inpos + 2) >= compressedLength) { + return -1; + } + + // 19 to 274 bytes + ulong count = (ulong)((byte)(bnext)) + 19; + byte c = (byte)(compressed.ReadByte()); + for (ulong i = 0; i < count; ++i) { + uncompressed.WriteByte(c); + dict[dictpos] = c; + dictpos = (dictpos + 1u) & 0xfffu; + ++outpos; + } + inpos += 3; + } else { + // 4 to 18 bytes + ulong count = (ulong)(nibble2) + 3; + byte c = bnext; + for (ulong i = 0; i < count; ++i) { + uncompressed.WriteByte(c); + dict[dictpos] = c; + dictpos = (dictpos + 1u) & 0xfffu; + ++outpos; + } + inpos += 2; + } + } else { + ushort offset = (ushort)((ushort)((byte)(bnext)) | ((ushort)(nibble2) << 8)); + ulong count = (ushort)(nibble1) + 3u; + + if (HasDict) { + // reference into dictionary + for (ulong i = 0; i < count; ++i) { + byte c = dict[(offset + i) & 0xfffu]; + uncompressed.WriteByte(c); + dict[dictpos] = c; + dictpos = (dictpos + 1u) & 0xfffu; + ++outpos; + } + } else { + // backref into decompressed data + if (offset == 0) { + // the game just reads the unwritten output buffer and copies it over itself inpos + // this case... while I suppose one *could* use this behavior inpos a really + // creative way by pre-initializing the output buffer to something known, I + // doubt it actually does that. so consider this a corrupted data stream. + return -1; + } + if (outpos < offset) { + // backref to before start of uncompressed data. this is invalid. + return -1; + } + + for (ulong i = 0; i < count; ++i) { + byte c = dict[(outpos - offset) & 0xfffu]; + uncompressed.WriteByte(c); + dict[dictpos] = c; + dictpos = (dictpos + 1u) & 0xfffu; + ++outpos; + } + } + + inpos += 2; + } + } + } + + public static long decompress_81(System.IO.Stream compressed, uint compressedLength, System.IO.Stream uncompressed, uint uncompressedLength) { + return decompress_internal(false, false, compressed, compressedLength, uncompressed, uncompressedLength); + } + + public static long decompress_83(System.IO.Stream compressed, uint compressedLength, System.IO.Stream uncompressed, uint uncompressedLength) { + return decompress_internal(false, true, compressed, compressedLength, uncompressed, uncompressedLength); + } + + public static long decompress_01(System.IO.Stream compressed, uint compressedLength, System.IO.Stream uncompressed, uint uncompressedLength) { + return decompress_internal(true, false, compressed, compressedLength, uncompressed, uncompressedLength); + } + + public static long decompress_03(System.IO.Stream compressed, uint compressedLength, System.IO.Stream uncompressed, uint uncompressedLength) { + return decompress_internal(true, true, compressed, compressedLength, uncompressed, uncompressedLength); + } + } +} diff --git a/HyoutaToolsLib/Tales/Compression/Decompressor.cs b/HyoutaToolsLib/Tales/Compression/Decompressor.cs new file mode 100644 index 0000000..951f7a6 --- /dev/null +++ b/HyoutaToolsLib/Tales/Compression/Decompressor.cs @@ -0,0 +1,74 @@ +using HyoutaPluginBase; +using HyoutaUtils; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; + +namespace HyoutaTools.Tales.Compression { + internal class Decompressor : HyoutaPluginBase.IDecompressor { + public CanDecompressAnswer CanDecompress(DuplicatableStream stream) { + long pos = stream.Position; + try { + int b0 = stream.ReadByte(); + if (b0 == 0x00 || b0 == 0x01 || b0 == 0x03 || b0 == 0x81 || b0 == 0x83) { + uint compressed = stream.ReadUInt32(); + uint uncompressed = stream.ReadUInt32(); + + if (b0 == 0) { + return (compressed == uncompressed && compressed + 9 == stream.Length) ? CanDecompressAnswer.Yes : CanDecompressAnswer.No; + } else { + return (compressed + 9 == stream.Length) ? CanDecompressAnswer.Yes : CanDecompressAnswer.No; + } + } + + return CanDecompressAnswer.No; + } finally { + stream.Position = pos; + } + } + + // returns negative on failure, or number of bytes written to output on success + public static long DecompressToStream(DuplicatableStream input, Stream output) { + int b0 = input.ReadByte(); + uint compressed = input.ReadUInt32(); + uint uncompressed = input.ReadUInt32(); + long result = -1; + if (b0 == 0x00 && compressed == uncompressed) { + StreamUtils.CopyStream(input, output, compressed); + result = compressed; + } else if (b0 == 0x01) { + result = Decompression.decompress_01(input, compressed, output, uncompressed); + } else if (b0 == 0x03) { + result = Decompression.decompress_03(input, compressed, output, uncompressed); + } else if (b0 == 0x81) { + result = Decompression.decompress_81(input, compressed, output, uncompressed); + } else if (b0 == 0x83) { + result = Decompression.decompress_83(input, compressed, output, uncompressed); + } + return result; + } + + public DuplicatableStream Decompress(DuplicatableStream input) { + using (MemoryStream ms = new MemoryStream()) { + input.Position = 0; + long result = DecompressToStream(input, ms); + if (result < 0 || result > ms.Length) { + return null; + } + + ms.Position = 0; + byte[] data = new byte[(int)result]; + ms.Read(data, 0, (int)result); + return new HyoutaUtils.Streams.DuplicatableByteArrayStream(data); + } + } + + public string GetId() { + return "compto"; + } + } +} diff --git a/HyoutaToolsLib/Tales/Compression/Program.cs b/HyoutaToolsLib/Tales/Compression/Program.cs new file mode 100644 index 0000000..d00b01c --- /dev/null +++ b/HyoutaToolsLib/Tales/Compression/Program.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HyoutaTools.Tales.Compression { + public class Program { + public static void PrintDecompressUsage() { + Console.WriteLine("Usage: compressed.bin decompressed.bin"); + } + + public static int Decompress(List args) { + string inpath = null; + string outpath = null; + + try { + for (int i = 0; i < args.Count; ++i) { + switch (args[i]) { + default: + if (inpath == null) { inpath = args[i]; } else if (outpath == null) { outpath = args[i]; } else { PrintDecompressUsage(); return -1; } + break; + } + } + } catch (IndexOutOfRangeException) { + PrintDecompressUsage(); + return -1; + } + + if (inpath == null) { + PrintDecompressUsage(); + return -1; + } + + if (outpath == null) { + outpath = inpath + ".dec"; + } + + using (var infile = new HyoutaUtils.Streams.DuplicatableFileStream(inpath)) + using (var outfile = new FileStream(outpath, FileMode.Create)) { + if (Decompressor.DecompressToStream(infile, outfile) < 0) { + Console.WriteLine("decompression failure"); + return -1; + } + } + + return 0; + } + } +}