#!/usr/bin/python import argparse import io import os import sys from struct import pack, unpack from typing import Callable, Tuple idx_file = "SCPACK.idx" pak_file = "SCPACK.pak" idx_key = "1qaz2wsx3edc4rfv5tgb6yhn7ujm8ik,9ol.0p;/-@:^[]" pak_key = "EAGLS_SYSTEM" max_label_count = 100 text_offset_36 = max_label_count * 36 text_offset_126 = max_label_count * 126 MIN_PYTHON = (3, 7) if sys.version_info < MIN_PYTHON: sys.exit("python %s.%s or later is required" % MIN_PYTHON) def sc_unpack(script_idx_path: str, script_pak_path: str, data_dir: str, txt_mode: bool, offset_mode: bool) -> None: text_offset = text_offset_36 if offset_mode else text_offset_126 with open(script_idx_path, mode='rb') as idx_file: encrypted_idx_bin = bytearray(idx_file.read()) with open(script_pak_path, mode='rb') as pak_file: encrypted_pak_bin = bytearray(pak_file.read()) decrypted_idx_bin = decrypt_idx(encrypted_idx_bin) data_dict = decrypt_pak(decrypted_idx_bin, encrypted_pak_bin, text_offset) for entry in data_dict: filename = entry data = data_dict[entry] if txt_mode: filename = filename.rsplit(".", 1)[0] + ".txt" with open(os.path.join(data_dir, filename), mode='w', newline='', encoding='utf8') as txt_file: txt_file.write(data[text_offset:-2].decode("shift-jis-2004")) else: with open(os.path.join(data_dir, filename), mode='wb') as dat_file: dat_file.write(data) print("Finished extracting files!") def sc_pack(script_idx_path: str, script_pak_path: str, data_dir: str, txt_mode: bool, offset_mode: bool) -> None: text_offset = text_offset_36 if offset_mode else text_offset_126 with open(script_idx_path, mode='rb') as idx_file: encrypted_idx_bin = bytearray(idx_file.read()) with open(script_pak_path, mode='rb') as pak_file: encrypted_pak_bin = bytearray(pak_file.read()) decrypted_idx_bin = decrypt_idx(encrypted_idx_bin) data_dict_old = decrypt_pak(decrypted_idx_bin, encrypted_pak_bin, text_offset) data_dict = {} for entry in data_dict_old: filename = entry data_old = data_dict_old[entry] try: if txt_mode: filename = filename.rsplit(".", 1)[0] + ".txt" header = data_old[0:text_offset] with open(os.path.join(data_dir, filename), newline='', encoding='utf8') as txt_file: body = txt_file.read().encode("shift-jis-2004") footer = data_old[-2:] data_dict[entry] = b''.join([header, body, footer]) else: with open(os.path.join(data_dir, filename), mode='rb') as dat_file: data_dict[entry] = dat_file.read() except FileNotFoundError: print("Ignoring missing file %s" % filename) data_dict[entry] = data_dict_old[entry] if data_dict_old == data_dict: print("Skip packing!") sys.exit(0) new_idx_bin, new_pak_bin = replace_all_data(decrypted_idx_bin, data_dict) new_data_dict = decrypt_pak(new_idx_bin, new_pak_bin, text_offset) new_idx_bin = decrypt_idx(new_idx_bin) with open(script_idx_path, mode='wb') as new_idx_file: new_idx_file.write(new_idx_bin) with open(script_pak_path, mode='wb') as new_pak_file: new_pak_file.write(b''.join(new_data_dict.values())) print("Finished replacing files!") def replace_all_data(decrypted_idx_bin: bytearray, data_dict: dict) -> Tuple[bytearray, bytearray]: base_address = 0 decrypted_idx_buf = io.BytesIO(decrypted_idx_bin) new_idx_buf = io.BytesIO() new_pak_buf = io.BytesIO() while True: filename_bytes = decrypted_idx_buf.read(0x14) zero_idx = filename_bytes[0] if not zero_idx: break filename = filename_bytes.decode("shift-jis-2004").split('\x00', 1)[0] if not base_address: base_address = unpack("I", decrypted_idx_buf.read(4))[0] decrypted_idx_buf.seek(4, 1) else: decrypted_idx_buf.seek(8, 1) new_data = data_dict[filename] new_idx_buf.write(filename_bytes) new_idx_buf.write(pack("I", new_pak_buf.tell() + base_address)) new_idx_buf.write(pack("I", len(new_data))) new_pak_buf.write(new_data) new_idx_buf.write(filename_bytes) new_idx_buf.write(decrypted_idx_buf.read()) return bytearray(new_idx_buf.getvalue()), bytearray(new_pak_buf.getvalue()) def decrypt_idx(encrypted_idx_bin: bytearray) -> bytearray: idx_key_sjis = idx_key.encode("shift-jis-2004") key_len = len(idx_key_sjis) encrypted_idx_bin_len = len(encrypted_idx_bin) seed = unpack('b', pack('B', encrypted_idx_bin[encrypted_idx_bin_len - 4]))[0] rnd = msvcrt_rand(seed) for i in range(encrypted_idx_bin_len - 4): a = rnd() a = a % key_len encrypted_idx_bin[i] ^= idx_key_sjis[a] return encrypted_idx_bin def decrypt_pak(decrypted_idx_bin: bytearray, encrypted_pak_bin: bytearray, text_offset: int) -> dict: decrypted_idx_buf = io.BytesIO(decrypted_idx_bin) encrypted_pak_buf = io.BytesIO(encrypted_pak_bin) data_dict = {} while True: filename_bytes = decrypted_idx_buf.read(0x14) zero_idx = filename_bytes[0] if not zero_idx: break filename = filename_bytes.decode("shift-jis-2004").split('\x00', 1)[0] decrypted_idx_buf.seek(4, 1) length = unpack("I", decrypted_idx_buf.read(4))[0] data_dict[filename] = decrypt_slice(bytearray(encrypted_pak_buf.read(length)), text_offset) return data_dict def decrypt_slice(encrypted_pak_bin_slice: bytearray, text_offset: int) -> bytearray: pak_key_sjis = pak_key.encode("shift-jis-2004") key_len = len(pak_key_sjis) length = len(encrypted_pak_bin_slice) seed = unpack('b', pack('B', encrypted_pak_bin_slice[length - 1]))[0] rnd = msvcrt_rand(seed) for i in range(text_offset, length - 1, 2): a = rnd() a = a % key_len key_byte = pak_key_sjis[a] encrypted_pak_bin_slice[i] ^= key_byte return encrypted_pak_bin_slice def msvcrt_rand(seed: int) -> Callable: def rand() -> int: nonlocal seed seed = (214013 * seed + 2531011) & 0x7fffffff return seed >> 16 return rand def input_filepath(path: str) -> str: if not os.path.exists(os.path.realpath(path)): raise argparse.ArgumentError return path parser = argparse.ArgumentParser(description='EAGLS idx/pak archive repacking and extraction tool') parser.add_argument('command', choices=["pack", "unpack"], help='operation mode') parser.add_argument('script_path', type=input_filepath, help='folder with SCPACK.{idx,pak}') parser.add_argument('data_path', help='folder for extracted files') parser.add_argument('-t', '--text-mode', action='store_true', default=False, help='dump or pack as utf8 .txt') parser.add_argument('-o', '--offset-mode', action='store_true', default=False, help='EAGLS compatibility setting') args = parser.parse_args() script_idx_path = os.path.join(args.script_path, idx_file) script_pak_path = os.path.join(args.script_path, pak_file) if not os.path.isfile(script_idx_path): sys.exit("%s not found" % script_idx_path) if not os.path.isfile(script_pak_path): sys.exit("%s not found" % script_pak_path) if args.command == "unpack": if not os.path.exists(args.data_path): os.makedirs(args.data_path) sc_unpack(script_idx_path, script_pak_path, args.data_path, args.text_mode, args.offset_mode) elif args.command == "pack": sc_pack(script_idx_path, script_pak_path, args.data_path, args.text_mode, args.offset_mode)