diff --git a/README.md b/README.md index 60a0235..81c0db8 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,13 @@ pip install . Decompile a file: ```sh -rpycdec +rpycdec decompile +``` + +Extract RPA archive + +```sh +rpycdec unrpa ``` ### Library usage @@ -43,6 +49,9 @@ from rpycdec import decompile, translate # decompile a file decompile(input_file, output_file) +# extract RPA archive +extract_rpa(file_steam, output_dir) + # decompile and translate a file translate(input_file, output_file) ``` @@ -53,7 +62,7 @@ Pull requests are welcome. For major changes, please open an issue first to disc ## FAQ -- **Q: It always raise pickle `import ** \nModuleNotFoundError: No module named '***'` error.** +- **Q: It always raise pickle `import ** \nModuleNotFoundError: No module named '**\*'` error.** A: It's because the our fake packages("renpy","store") is not contains the object you want to decompile. Please open an issue and tell us the renpy version and the rpyc file you want to decompile. Join our telegram group to get help also be better. diff --git a/src/rpycdec/__init__.py b/src/rpycdec/__init__.py index cc8717f..aead6df 100644 --- a/src/rpycdec/__init__.py +++ b/src/rpycdec/__init__.py @@ -1,3 +1,4 @@ from .decompile import decompile from .translate import translate from .cli import main +from .rpa import extract_rpa diff --git a/src/rpycdec/cli.py b/src/rpycdec/cli.py index 189b8db..d9599a6 100644 --- a/src/rpycdec/cli.py +++ b/src/rpycdec/cli.py @@ -1,23 +1,55 @@ import argparse import logging from rpycdec.decompile import decompile +from rpycdec.rpa import extract_rpa logger = logging.getLogger(__name__) +def decompile_files(srcs: list[str]): + """ + decompile rpyc file or directory. + """ + for src in srcs: + decompile(src) + + +def extract_rpa_files(srcs: list[str]): + """ + extract rpa archive. + """ + for src in srcs: + with open(src, "rb") as f: + extract_rpa(f) + + def main(): """ command line tool entry. """ logging.basicConfig(level=logging.INFO) + argparser = argparse.ArgumentParser() argparser.add_argument( "--verbose", "-v", action="store_true", help="verbose output" ) - argparser.add_argument("src", nargs=1, help="rpyc file or directory") + subparsers = argparser.add_subparsers( + title="subcommands", dest="command", help="subcommand help" + ) + + decompile_parser = subparsers.add_parser("decompile", help="decompile rpyc file") + decompile_parser.add_argument("src", nargs=1, help="rpyc file or directory") + decompile_parser.set_defaults(func=decompile_files) + + unrpa_parser = subparsers.add_parser("unrpa", help="extract rpa archive") + unrpa_parser.add_argument("src", nargs=1, help="rpa archive") + unrpa_parser.set_defaults(func=extract_rpa_files) + args = argparser.parse_args() if args.verbose: logger.setLevel(logging.DEBUG) - for src in args.src: - decompile(src) + if not args.command: + argparser.print_help() + return + args.func(args.src) diff --git a/src/rpycdec/rpa.py b/src/rpycdec/rpa.py index adbf9e5..86ea410 100644 --- a/src/rpycdec/rpa.py +++ b/src/rpycdec/rpa.py @@ -4,11 +4,11 @@ from io import BufferedIOBase -def read_str(data: BufferedIOBase, eol: int = 0x00) -> bytes: +def read_util(data: BufferedIOBase, util: int = 0x00) -> bytes: content = bytearray() while True: c = data.read(1) - if c[0] == eol: + if c[0] == util: break content += c return content @@ -22,16 +22,13 @@ def to_start(start: str | None | bytes) -> bytes: return start.encode("latin-1") -def extract_rpa(r: BufferedIOBase, dir: str): - header = read_str(r).decode("utf-8") - lines = header.splitlines() - metadata = lines[0].split() - magic, metadata = metadata[0], metadata[1:] - index_offset, metadata = int(metadata[0], 16), metadata[1:] - if magic == "RPA-3.0": - key, metadata = int(metadata[0], 16), metadata[1:0] - else: - raise Exception("magic %s not supported" % magic) +def extract_rpa(r: BufferedIOBase, dir: str | None = None): + magic = read_util(r, 0x20) + if magic != b"RPA-3.0": + print("Not a Ren'Py archive.") + return + index_offset = int(read_util(r, 0x20), 16) + key = int(read_util(r, 0x0A).decode(), 16) # read index r.seek(index_offset) @@ -42,6 +39,8 @@ def extract_rpa(r: BufferedIOBase, dir: str): for offset, dlen, *left in v ] + if not dir: + dir = os.path.splitext(r.name)[0] for filename, entries in index.items(): data = bytearray() for offset, dlen, start in entries: