Skip to content

Instantly share code, notes, and snippets.

@Xkeeper0
Last active May 9, 2022 11:52
Show Gist options
  • Save Xkeeper0/d1ef62e5464e8bbfa655b556a78af1ac to your computer and use it in GitHub Desktop.
Save Xkeeper0/d1ef62e5464e8bbfa655b556a78af1ac to your computer and use it in GitHub Desktop.
Decompressor for NIS's YKCMP_V1 compression
<?php
// This file was butchered from the Disgaea PC savefile tools
// https://github.com/Xkeeper0/disgaea-pc-tools/
if (!isset($argv[1])) {
die("Usage: php ". $argv[0] ." <file to decompress>\n");
}
if (!file_exists($argv[1])) {
die("file not found: ". $argv[1] ."\n");
}
$infile = $argv[1];
$outfile = $infile .".decomp";
$file = file_get_contents($infile);
$magic = substr($file, 0, 8); // Magic value "YKCMP_V1"
$unks = substr($file, 8, 4); // Unknown value
$sizecs = substr($file, 12, 4); // Compressed size
$sizeds = substr($file, 16, 4); // Decompressed size
$cdata = substr($file, 20); // Compressed data
// Convert to strings
$unk = getLEValue($unks);
$sizec = getLEValue($sizecs);
$sized = getLEValue($sizeds);
$sizecd = strlen($cdata);
if ($magic !== "YKCMP_V1") {
die("File isn't YKCMP_V1 compressed\n");
}
printf("YKCMP_V1 decompression ------------------------------\n".
"Unknown value: %08X\n".
"Compressed size: %08X (%d bytes)\n".
"Decompressed size: %08X (%d bytes)\n",
$unk,
$sizec, $sizec,
$sized, $sized
);
if ($sizecd !== $sizec) {
// Extra data past what the header says should exist
printf("Data size doesn't match... Header says 0x%X, actual data is 0x%X.\n", $sizec, $sizecd);
// Remove the excess
$cdata = substr($cdata, 0, $sizec);
}
$cmp = new CompressionHandler($cdata, $sized);
$cmp->setLogLevel(1);
try {
$ddata = $cmp->decompress(true);
} catch (Exception $e) {
die("\n\nERROR: ". $e->getMessage() ."\n");
}
file_put_contents($outfile, $ddata);
printf("Decompressed to '$outfile'\n");
function getLEValue($s) {
$t = str_split($s);
$o = 0;
foreach ($t as $i => $v) {
$o += ord($v) << ($i * 8);
}
return $o;
}
class CompressionHandler {
protected $_compressed = "";
protected $_decompressed = "";
protected $_cp = 0;
protected $_dp = 0;
protected $_volume = 1;
protected $_progress = array(
'last' => 0,
'max' => null,
);
public function __construct($d, $l) {
$this->_compressed = $d;
$this->_decompressed = str_repeat("\x00", $l);
$this->_progress['max'] = $l;
}
public function getDecompressedData() {
return $this->_decompressed;
}
/**
* 0: Silent*
* 1: Normal
* 2: Describe events
* 3: Show actual byte values being written
*/
public function setLogLevel($volume) {
$this->_volume = $volume;
}
public function decompress($showProgress = false) {
if ($showProgress) {
$this->setLogLevel(1);
}
$this->_cp = 0;
$this->_dp = 0;
$dataLen = strlen($this->_compressed);
$this->_log(1, sprintf("Decompressing %08X bytes of data...\n", $dataLen));
$this->_log(1, sprintf("Expected size %08X\n", strlen($this->_decompressed)));
if ($showProgress) {
print str_repeat(".........|", 10);
print "\n";
}
while ($this->_cp < $dataLen) {
if ($this->_dp >= strlen($this->_decompressed)) {
// Got more "compressed data" when we've already filled the expected decompressed size
$this->_log(1, sprintf("\nuhh. this is weird.\nthere's still 0x%X bytes of data to decompress,\nbut we've already filled the 0x%X-sized buffer.\n\nso...i'm just gonna stop now, ok?\nthis file is weird.\n", strlen($this->_compressed) - $this->_cp, strlen($this->_decompressed)));
break;
}
$b = $this->_cb();
$this->_log(3, "\n"); // For separating out the byte values
$this->_log(2, sprintf("\n%08X ", $this->_cp));
$logt = sprintf("%08X", $this->_dp);
if ($b == 0) {
$this->_log(2, sprintf("Null-op??? %02X\n", $b));
$this->_log(2, sprintf($logt ." Is this even allowed??"));
$this->_cp++;
continue;
}
// "If data[p] < $80, read and output data[p] bytes"
if ($b > 0 && $b < 0x80) {
$this->_log(2, sprintf("Direct copy: %02X\n", $b));
$this->_log(2, sprintf($logt ." Copying %02X bytes\n", $b));
$this->_log(2, sprintf(" "));
$this->_cp++;
$this->_copy($b);
continue;
}
// The ">= x && < y" is not needed but improves readability a bit
// Turn the origin byte (optionally next 1 or 2) bytes into two numbers
// indicating "bytes to copy" and "bytes to move back", respectively
// removing the lower bound (e.g. 8F -> 0F, F2 = (F2 - E0) = 11)
// 80: AB
// C0: AA BB
// E0: AA AB BB
if ($b >= 0x80 && $b < 0xC0) {
// One-byte lookbehind
$this->_log(2, sprintf("1B lookbehind: %02X\n", $this->_cb()));
$b -= 0x80;
$len = (($b & 0xF0) >> 4) + 1;
$back = ($b & 0x0F) + 1;
} elseif ($b >= 0xC0 && $b < 0xE0) {
// Two bytes; first byte - C0, second byte normal
$this->_cp++;
$len = $b - 0xC0 + 2;
$back = $this->_cb() + 1;
$this->_log(2, sprintf("2B lookbehind: %02X %02X\n", $this->_cb(-1), $this->_cb()));
} elseif ($b >= 0xE0 && $b <= 0xFF) {
// Three bytes; AAA BBB for values
$this->_cp++;
$temp = $this->_cb();
$this->_cp++;
$temp2 = $this->_cb();
$len = (($b - 0xE0) << 4) + (($temp & 0xF0) >> 4) + 3;
$back = (($temp & 0x0F) << 8) + $temp2 + 1;
$this->_log(2, sprintf("3B lookbehind: %02X %02X %02X\n", $this->_cb(-2), $this->_cb(-1), $this->_cb()));
} else {
throw new Exception("This should never happen");
}
$this->_log(2, sprintf("$logt Copying %03X from %03X bytes ago\n", $len, $back));
$this->_copyback($len, $back);
$this->_cp++;
if ($showProgress) {
$progressPos = floor($this->_dp / $this->_progress['max'] * 100);
if ($progressPos > $this->_progress['last']) {
$positions = $progressPos - $this->_progress['last'];
print str_repeat("#", $positions);
$this->_progress['last'] = $progressPos;
}
}
}
$this->_log(1, "\n");
return $this->_decompressed;
}
/**
* Copy $v bytes from source to decompressed version
*/
protected function _copy($num) {
for ($i = $num; $i > 0; $i--) {
$this->_log(3, sprintf("%02X", $this->_cb()));
$this->_write_db($this->_cb());
$this->_cp++;
}
}
/**
* Copy $v bytes from source to decompressed version
*/
protected function _copyback($num, $back) {
$back *= -1;
for ($i = $num; $i > 0; $i--) {
$b = $this->_db($back);
$this->_log(3, sprintf("%02X", $b));
$this->_write_db($b);
}
}
protected function _getByte($from, $pos) {
if ($pos > strlen($from) || $pos < 0) {
throw new Exception(sprintf("Asked to get byte at position 0x%X (max range 0x%X)", $pos, strlen($from)));
}
return ord($from{$pos});
}
protected function _cb($o = 0) {
return $this->_getByte($this->_compressed, $this->_cp + $o);
}
protected function _db($o = 0) {
return $this->_getByte($this->_decompressed, $this->_dp + $o);
}
/**
* Writes a byte to the decompressed output and increments the write pointer
*/
protected function _write_db($b, $o = 0) {
$ro = $this->_dp + $o;
if ($ro < 0 || $ro > strlen($this->_decompressed)) {
throw new Exception("Trying to write to byte outside of decompress area (ofs $ro)");
}
$this->_decompressed{$ro} = chr($b);
$this->_dp++;
}
protected function _log($vol, $msg) {
if ($vol > $this->_volume) {
return;
}
print $msg;
}
/**
* Hastily-written nop compression
* Rewrites data using nothing but "copy next values" bytes
* Does not actually compress anything
* Inflates file size by about 0.79%
*/
public static function compress($s, $size = false) {
$len = strlen($s);
$chunksize = ($size ? $size : 0x7F);
if ($chunksize <= 0 || $chunksize >= 0x80) {
throw new \Exception("'Compression' chunk size must be between 0x01-0x7F");
}
$out = "";
for ($p = 0; $p < $len; $p += $chunksize) {
$sz = min($chunksize, $len - $p);
$out .= chr($sz);
$out .= substr($s, $p, $chunksize);
}
return $out;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment