Skip to content

Commit

Permalink
Add Tales.Compression based off topdec
Browse files Browse the repository at this point in the history
  • Loading branch information
AdmiralCurtiss committed Jun 21, 2024
1 parent 63f8bbb commit 89512b5
Show file tree
Hide file tree
Showing 4 changed files with 307 additions and 0 deletions.
1 change: 1 addition & 0 deletions HyoutaToolsLib/ProgramNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ public class ProgramNames {
{ new KeyValuePair<ProgramName, ExecuteProgramDelegate>( new ProgramName( "Patches.Bps.BpsToTextConverter", "-" ), Patches.Bps.BpsToTextConverter.Execute) },
{ new KeyValuePair<ProgramName, ExecuteProgramDelegate>( new ProgramName( "Patches.Bps.TextToBpsConverter", "-" ), Patches.Bps.TextToBpsConverter.Execute) },
{ new KeyValuePair<ProgramName, ExecuteProgramDelegate>( new ProgramName( "Patches.Bps.Create", "-" ), Patches.Bps.Program.ExecuteCreate) },
{ new KeyValuePair<ProgramName, ExecuteProgramDelegate>( new ProgramName( "Tales.Compression.Decompress", "-" ), Tales.Compression.Program.Decompress) },
};
}
}
181 changes: 181 additions & 0 deletions HyoutaToolsLib/Tales/Compression/Decompression.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
74 changes: 74 additions & 0 deletions HyoutaToolsLib/Tales/Compression/Decompressor.cs
Original file line number Diff line number Diff line change
@@ -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";
}
}
}
51 changes: 51 additions & 0 deletions HyoutaToolsLib/Tales/Compression/Program.cs
Original file line number Diff line number Diff line change
@@ -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<string> 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;
}
}
}

0 comments on commit 89512b5

Please sign in to comment.