├── .gitattributes ├── Makefile ├── LICENSE ├── README.md ├── pzzcomp_jojo_batch.py ├── pzz_comp_jojo.py └── pzzcomp_jojo.c /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | gcc -O3 -Wall -Wextra -static -static-libgcc pzzcomp_jojo.c -o pzzcomp_jojo.exe 3 | release: 4 | gcc -O3 -Wall -Wextra -static -static-libgcc pzzcomp_jojo.c -o pzzcomp_jojo.exe -DNDEBUG 5 | debug: 6 | gcc -g3 -Wall -Wextra -static -static-libgcc pzzcomp_jojo.c -o pzzcomp_jojo.exe 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PZZ compressor & unpacker 2 | [PS2] GioGio’s Bizarre Adventure / JoJo no Kimyō na Bōken: Ōgon no Kaze 3 | ## Usage 4 | 1. Extract .pzz files from AFS_DATA.AFS. 5 | 2. Unpack [.dat] files from .pzz (only Python version). Compressed files: *_compressed.dat. 6 | 3. Decompress. The last zero bytes are skipped. 7 | 4. Compress. Add padding. 8 | 5. ??? 9 | ### Python 3 version 10 | pzz_comp_jojo.py 11 | 12 | Unpack 13 | ``` 14 | pzz_comp_jojo.py -u file.pzz dir 15 | pzz_comp_jojo.py -bu *.pzz dir 16 | ``` 17 | Decompress 18 | ``` 19 | pzz_comp_jojo.py -d file.dat file.bin 20 | pzz_comp_jojo.py -bd *.dat dir 21 | ``` 22 | Compress 23 | ``` 24 | pzz_comp_jojo.py -c file.bin file.dat 25 | pzz_comp_jojo.py -bc *.bin dir 26 | ``` 27 | ### C version (faster) 28 | pzzcomp_jojo.exe (see releases) 29 | 30 | Decompress 31 | ``` 32 | pzzcomp_jojo -d file.dat file.bin 33 | ``` 34 | Compress 35 | ``` 36 | pzzcomp_jojo -c file.bin file.dat 37 | ``` 38 | ### C version batch 39 | pzzcomp_jojo_batch.py 40 | 41 | Decompress 42 | ``` 43 | pzzcomp_jojo_batch.py -d Unpacked\*_compressed.dat Uncompressed --extension .bin 44 | ``` 45 | Compress 46 | ``` 47 | pzzcomp_jojo_batch.py -c Uncompressed\*.bin dir --extension .dat 48 | ``` 49 | **Note**: a path that starts with `\` or `/` is absolute. Example for Windows: `\dir` or `/dir` -> `C:\dir`. 50 | -------------------------------------------------------------------------------- /pzzcomp_jojo_batch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | from pathlib import Path 4 | from os import system 5 | 6 | parser = argparse.ArgumentParser(description='pzzcomp_jojo helper') 7 | parser.add_argument('input_pattern', metavar='INPUT', help='relative pattern; e.g. AFS_DATA\\*_compressed.dat') 8 | parser.add_argument('output_dir', metavar='OUTPUT', help='directory') 9 | parser.add_argument('-e', '--extension', metavar='EXT', help='output; .bin, .dat, .pzz, etc') 10 | parser.add_argument('-y', '--yes', action='store_true', help='overwrite') 11 | group = parser.add_mutually_exclusive_group(required=True) 12 | group.add_argument('-c', '--compress', action='store_true') 13 | group.add_argument('-d', '--decompress', action='store_true') 14 | args = parser.parse_args() 15 | 16 | p_output = Path(args.output_dir) 17 | p_output.mkdir(exist_ok=True) 18 | 19 | for path in Path('.').glob(args.input_pattern): 20 | print(path) 21 | 22 | if args.extension: 23 | ext = args.extension 24 | elif args.compress: 25 | ext = ".dat" 26 | else: 27 | ext = ".bin" 28 | other_path = (p_output / path.name).with_suffix(ext) 29 | 30 | if other_path.exists() and not args.yes: 31 | answer = input("File '{}' already exists. Overwrite ? [y/N]".format(other_path)) 32 | if answer.strip().lower() != 'y': 33 | continue 34 | 35 | print(">", other_path) 36 | if args.compress: 37 | system(r'pzzcomp_jojo -c "{}" "{}"'.format(path, other_path)) 38 | elif args.decompress: 39 | system(r'pzzcomp_jojo -d "{}" "{}"'.format(path, other_path)) 40 | -------------------------------------------------------------------------------- /pzz_comp_jojo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | __version__ = "1.0.1" 3 | __author__ = "infval" 4 | 5 | from pathlib import Path 6 | from struct import unpack 7 | 8 | 9 | def pzz_decompress(b): 10 | bout = bytearray() 11 | size_b = len(b) // 2 * 2 12 | 13 | cb = 0 # Control bytes 14 | cb_bit = -1 15 | i = 0 16 | while i < size_b: 17 | if cb_bit < 0: 18 | cb = b[i + 0] 19 | cb |= b[i + 1] << 8 20 | cb_bit = 15 21 | i += 2 22 | continue 23 | 24 | compress_flag = cb & (1 << cb_bit) 25 | cb_bit -= 1 26 | 27 | if compress_flag: 28 | c = b[i + 0] 29 | c |= b[i + 1] << 8 30 | offset = (c & 0x7FF) * 2 31 | if offset == 0: 32 | break # End of the compressed data 33 | count = (c >> 11) * 2 34 | if count == 0: 35 | i += 2 36 | c = b[i + 0] 37 | c |= b[i + 1] << 8 38 | count = c * 2 39 | 40 | index = len(bout) - offset 41 | for j in range(count): 42 | bout.append(bout[index + j]) 43 | else: 44 | bout.extend(b[i: i + 2]) 45 | i += 2 46 | 47 | return bout 48 | 49 | 50 | def pzz_compress(b): 51 | bout = bytearray() 52 | size_b = len(b) // 2 * 2 53 | 54 | cb = 0 # Control bytes 55 | cb_bit = 15 56 | cb_pos = 0 57 | bout.extend(b"\x00\x00") 58 | 59 | i = 0 60 | while i < size_b: 61 | start = max(i - 0x7FF * 2, 0) 62 | count_r = 0 63 | max_i = -1 64 | tmp = b[i: i + 2] 65 | init_count = len(tmp) 66 | while True: 67 | start = b.find(tmp, start, i + 1) 68 | if start != -1 and start % 2 != 0: 69 | start += 1 70 | continue 71 | if start != -1: 72 | count = init_count 73 | while i < size_b - count \ 74 | and count < 0xFFFF * 2 \ 75 | and b[start + count ] == b[i + count ] \ 76 | and b[start + count + 1] == b[i + count + 1]: 77 | count += 2 78 | if count_r < count: 79 | count_r = count 80 | max_i = start 81 | start += 2 82 | else: 83 | break 84 | start = max_i 85 | 86 | compress_flag = 0 87 | if count_r >= 4: 88 | compress_flag = 1 89 | offset = i - start 90 | offset //= 2 91 | count_r //= 2 92 | c = offset 93 | if count_r <= 0x1F: 94 | c |= count_r << 11 95 | bout.append(c & 0xFF) 96 | bout.append((c >> 8)) 97 | else: 98 | bout.append(c & 0xFF) 99 | bout.append((c >> 8)) 100 | bout.append(count_r & 0xFF) 101 | bout.append((count_r >> 8)) 102 | i += count_r * 2 103 | else: 104 | bout.extend(b[i: i + 2]) 105 | i += 2 106 | cb |= (compress_flag << cb_bit) 107 | cb_bit -= 1 108 | if cb_bit < 0: 109 | bout[cb_pos + 0] = cb & 0xFF 110 | bout[cb_pos + 1] = cb >> 8 111 | cb = 0x0000 112 | cb_bit = 15 113 | cb_pos = len(bout) 114 | bout.extend(b"\x00\x00") 115 | 116 | cb |= (1 << cb_bit) 117 | bout[cb_pos + 0] = cb & 0xFF 118 | bout[cb_pos + 1] = cb >> 8 119 | bout.extend(b"\x00\x00") 120 | 121 | return bout 122 | 123 | 124 | def pzz_unpack(path, dir_path): 125 | """ BMS script: https://zenhax.com/viewtopic.php?f=9&t=8724&p=39437#p39437 126 | """ 127 | with open(path, "rb") as f: 128 | file_count = f.read(4) 129 | file_count, = unpack(" 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | #define VERSION_STR "v1.0.0" 12 | 13 | void PZZ_Decompress(const uint8_t* src, uint8_t* dst, size_t src_len); 14 | size_t PZZ_GetDecompressedSize(const uint8_t* src, size_t size); 15 | size_t PZZ_Compress(const uint8_t* src, uint8_t* dst, size_t src_len); 16 | size_t PZZ_GetCompressedMaxSize(size_t src_len); 17 | 18 | void PZZ_Decompress(const uint8_t* src, uint8_t* dst, size_t src_len) 19 | { 20 | size_t offset, count; 21 | size_t spos = 0, dpos = 0; 22 | uint16_t cb = 0; 23 | int8_t cb_bit = -1; 24 | 25 | src_len = src_len / 2 * 2; 26 | while (spos < src_len) { 27 | if (cb_bit < 0) { 28 | cb = src[spos++] << 0; 29 | cb |= src[spos++] << 8; 30 | cb_bit = 15; 31 | } 32 | 33 | int compress_flag = cb & (1 << cb_bit); 34 | cb_bit--; 35 | 36 | if (compress_flag) { 37 | count = src[spos++] << 0; 38 | count |= src[spos++] << 8; 39 | offset = (count & 0x7FF) * 2; 40 | if (offset == 0) { 41 | break; // End of the compressed data 42 | } 43 | count >>= 11; 44 | if (count == 0) { 45 | count = src[spos++] << 0; 46 | count |= src[spos++] << 8; 47 | } 48 | count *= 2; 49 | for (size_t j = 0; j < count; j++) { 50 | dst[dpos] = dst[dpos - offset]; 51 | dpos++; 52 | } 53 | } 54 | else { 55 | dst[dpos++] = src[spos++]; 56 | dst[dpos++] = src[spos++]; 57 | } 58 | } 59 | } 60 | 61 | size_t PZZ_GetDecompressedSize(const uint8_t* src, size_t src_len) 62 | { 63 | #define CHECK_SPOS if (spos >= src_len) return -1; 64 | size_t offset, count; 65 | size_t spos = 0, dpos = 0; 66 | uint16_t cb = 0; 67 | int8_t cb_bit = -1; 68 | 69 | src_len = src_len / 2 * 2; 70 | while (spos < src_len) { 71 | if (cb_bit < 0) { 72 | CHECK_SPOS 73 | cb = src[spos++] << 0; 74 | CHECK_SPOS 75 | cb |= src[spos++] << 8; 76 | cb_bit = 15; 77 | } 78 | 79 | int compress_flag = cb & (1 << cb_bit); 80 | cb_bit--; 81 | 82 | if (compress_flag) { 83 | CHECK_SPOS 84 | count = src[spos++] << 0; 85 | CHECK_SPOS 86 | count |= src[spos++] << 8; 87 | offset = (count & 0x7FF) * 2; 88 | if (offset == 0) { 89 | break; // End of the compressed data 90 | } 91 | count >>= 11; 92 | if (count == 0) { 93 | CHECK_SPOS 94 | count = src[spos++] << 0; 95 | CHECK_SPOS 96 | count |= src[spos++] << 8; 97 | } 98 | count *= 2; 99 | if (dpos < offset) return -1; 100 | dpos += count; 101 | } 102 | else { 103 | CHECK_SPOS 104 | spos++; 105 | CHECK_SPOS 106 | spos++; 107 | dpos += 2; 108 | } 109 | } 110 | 111 | return dpos; 112 | #undef CHECK_SPOS 113 | } 114 | 115 | size_t PZZ_GetCompressedMaxSize(size_t src_len) 116 | { 117 | return (2 + 2) // Last 0x0000 118 | + src_len // 119 | + src_len / 16; // 0x0000 for every 32 bytes 120 | } 121 | 122 | size_t PZZ_Compress(const uint8_t* src, uint8_t* dst, size_t src_len) 123 | { 124 | size_t spos = 0, dpos = 0; 125 | uint16_t cb = 0; 126 | int8_t cb_bit = 15; 127 | size_t cb_pos = 0; 128 | 129 | src_len = src_len / 2 * 2; 130 | 131 | dst[dpos++] = 0x00; 132 | dst[dpos++] = 0x00; 133 | while (spos < src_len) { 134 | size_t offset = 0; 135 | size_t length = 0; 136 | 137 | for (size_t i = (spos >= 0x7FF * 2 ? (intptr_t)spos - 0x7FF * 2 : 0); i < spos; i += 2) { 138 | if (src[i] == src[spos] && src[i + 1] == src[spos + 1]) { 139 | size_t cur_len = 0; 140 | do { 141 | cur_len += 2; 142 | } while ((cur_len < 0xFFFF * 2) 143 | && (spos + cur_len < src_len) 144 | && src[i + cur_len] == src[spos + cur_len] 145 | && src[i + 1 + cur_len] == src[spos + 1 + cur_len]); 146 | 147 | if (cur_len > length) { 148 | offset = spos - i; 149 | length = cur_len; 150 | if (length >= 0xFFFF * 2) { 151 | break; 152 | } 153 | } 154 | } 155 | } 156 | 157 | uint16_t compress_flag = 0; 158 | if (length >= 4) { 159 | compress_flag = 1; 160 | offset /= 2; 161 | length /= 2; 162 | size_t c = offset; 163 | if (length <= 0x1F) { 164 | c |= length << 11; 165 | dst[dpos++] = c & 0xFF; 166 | dst[dpos++] = c >> 8; 167 | } 168 | else { 169 | dst[dpos++] = c & 0xFF; 170 | dst[dpos++] = c >> 8; 171 | dst[dpos++] = length & 0xFF; 172 | dst[dpos++] = length >> 8; 173 | } 174 | spos += length * 2; 175 | } 176 | else { 177 | dst[dpos++] = src[spos++]; 178 | dst[dpos++] = src[spos++]; 179 | } 180 | 181 | cb |= compress_flag << cb_bit; 182 | cb_bit--; 183 | 184 | if (cb_bit < 0) { 185 | dst[cb_pos + 0] = cb & 0xFF; 186 | dst[cb_pos + 1] = cb >> 8; 187 | cb = 0x0000; 188 | cb_bit = 15; 189 | cb_pos = dpos; 190 | dst[dpos++] = 0x00; 191 | dst[dpos++] = 0x00; 192 | } 193 | } 194 | 195 | cb |= 1 << cb_bit; 196 | dst[cb_pos + 0] = cb & 0xFF; 197 | dst[cb_pos + 1] = cb >> 8; 198 | dst[dpos++] = 0x00; 199 | dst[dpos++] = 0x00; 200 | 201 | return dpos; 202 | } 203 | 204 | // Max: 2 GB 205 | long GetFileSize(FILE* file) 206 | { 207 | fseek(file, 0, SEEK_END); 208 | long size = ftell(file); 209 | fseek(file, 0, SEEK_SET); 210 | return size; 211 | } 212 | 213 | int main(int argc, char* argv[]) 214 | { 215 | if (argc < 4) { 216 | printf("PZZ (de)compressor - [PS2] JoJo no Kimyou na Bouken - Ougon no Kaze || " VERSION_STR " by infval\n" 217 | "Usage: program.exe (-d | -c) INPUT OUTPUT\n" 218 | "-d - decompress, -c - compress"); 219 | return 0; 220 | } 221 | 222 | FILE* finput = fopen(argv[2], "rb"); 223 | if (finput == NULL) { 224 | fprintf(stderr, "Can't open: %s", argv[2]); 225 | return 1; 226 | } 227 | size_t size = GetFileSize(finput); 228 | uint8_t* source = (uint8_t*)malloc(size); 229 | if (source == NULL) { 230 | fprintf(stderr, "Error: malloc()"); 231 | return 2; 232 | } 233 | size_t readBytes = fread(source, 1, size, finput); 234 | if (readBytes != size) { 235 | fprintf(stderr, "Can't open: %s", argv[2]); 236 | return 3; 237 | } 238 | fclose(finput); 239 | 240 | size_t dsize = 0; 241 | uint8_t* dest = NULL; 242 | if (!strcmp(argv[1], "-c")) { 243 | size_t maxsize = PZZ_GetCompressedMaxSize(size); 244 | dest = (uint8_t*)malloc(maxsize); 245 | if (dest == NULL) { 246 | fprintf(stderr, "Error: malloc()"); 247 | return 5; 248 | } 249 | dsize = PZZ_Compress(source, dest, size); 250 | assert(dsize <= maxsize); 251 | } 252 | else { // !strcmp(argv[1], "-d") 253 | dsize = PZZ_GetDecompressedSize(source, size); 254 | if (dsize == (size_t)-1) { 255 | fprintf(stderr, "Bad PZZ file"); 256 | return 4; 257 | } 258 | dest = (uint8_t*)malloc(dsize); 259 | if (dest == NULL) { 260 | fprintf(stderr, "Error: malloc()"); 261 | return 5; 262 | } 263 | PZZ_Decompress(source, dest, size); 264 | } 265 | 266 | FILE* fout = fopen(argv[3], "wb"); 267 | if (fout == NULL) { 268 | fprintf(stderr, "Can't open: %s", argv[3]); 269 | return 6; 270 | } 271 | size_t writeBytes = fwrite(dest, 1, dsize, fout); 272 | if (writeBytes != dsize) { 273 | fprintf(stderr, "Can't write: %s", argv[3]); 274 | return 7; 275 | } 276 | fclose(fout); 277 | 278 | free(dest); 279 | free(source); 280 | return 0; 281 | } 282 | --------------------------------------------------------------------------------