Last active
May 29, 2022 06:50
-
-
Save Dobby233Liu/3c2832664650ae68d2471e7bbb8a78ee to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""Tool that helps decompiling a Ren'Py game. Early prototype.""" | |
import glob | |
import unrpyc | |
import unrpa | |
import os | |
import tarfile | |
import traceback | |
import shutil | |
import sys | |
UNNEEDED_DIRS = ["lib/", "renpy/", "game/cache/", "?.app/"] | |
# ?=project name | |
UNNEEDED_FILES = ["?.sh", "?.py", "?.exe", "?-32.exe", "game/script_version.txt"] | |
def get_game_name(fn): | |
ret = fn.split("-")[0] | |
if not ret or not "-" in fn: | |
raise Exception("Cannot find game name") | |
return ret | |
def loop_check(func, args): | |
for i in args: | |
if func(i): | |
return True | |
return False | |
def stripped_members(m, strip, game_name, noisy=True): | |
striplen = len(strip) | |
for tarinfo in m: | |
tpath = "" | |
if tarinfo.path.startswith(strip): | |
tpath = tarinfo.path[striplen:] | |
else: | |
continue | |
if not loop_check(lambda i: tpath.startswith(i.replace("?", game_name)) or tpath == i.replace("?", game_name)[:-1], UNNEEDED_DIRS) and not loop_check(lambda i: tpath == i.replace("?", game_name), UNNEEDED_FILES): | |
tarinfo.path = tpath | |
if noisy: | |
print(tarinfo.path) | |
yield tarinfo | |
def print_path(path, basedir, noisy=True): | |
if noisy: | |
print(os.path.relpath(path, start=basedir)) | |
def interact_question(question): | |
text = f"{question} (y/n)? " | |
try: | |
return input(text).strip().lower() == "y" | |
except KeyboardInterrupt as e: | |
raise e | |
def get_game_interactive(noisy=True): | |
if not interact_question("Already extracted everything"): | |
archive_path = input("Path to the archive of the Linux version of the game: ") | |
os.access(archive_path, os.R_OK) | |
try: | |
game_name = get_game_name(os.path.basename(archive_path)) | |
if noisy and not interact_question("Is \"%s\" the right project name" % game_name): | |
old_game_name = game_name | |
while True: | |
game_name = input(f"Input game name [${old_game_name}]: ") | |
if game_name.strip() == "": | |
print("Invaild name!") | |
game_name = old_game_name | |
else: | |
break | |
except: | |
game_name = input("I don't know what's this game called, so tell me what's it's name (usually from the executable name): ") | |
project_path = os.path.join(os.getcwd(), game_name) | |
if noisy: | |
input("I'm putting stuff into " + project_path + " (Press Enter to continue.) ") | |
os.makedirs(project_path, exist_ok=True) | |
print("\nDecompressing archive...") | |
with tarfile.open(archive_path, 'r|*') as tf: | |
if noisy: | |
print("Looking into game files directory...") | |
basedir = None | |
septype = "" | |
for i in tf: | |
if i.isdir() and not ("/" in i.path or "\\" in i.path): | |
basedir = i | |
break | |
for i in tf: | |
if "/" in i.path or "\\" in i.path: | |
septype = ("/" in i.path) and "/" or "\\" | |
break | |
if basedir is None: | |
raise Exception("Cannot find game files directory, invaild archive?") | |
if noisy: | |
print("I think the game files in the archive are located in:", basedir.path) | |
print("Extracting everything...") | |
tf.extractall(members=stripped_members(tf, basedir.path + septype, game_name), path=project_path) | |
print("Decompression complete.") | |
else: | |
project_path = input("Where? ") | |
os.access(project_path, os.F_OK) | |
game_name = os.path.basename(os.path.dirname(project_path + os.sep)) | |
game_name_new = input("Game name (" + game_name + "): ") | |
if game_name_new: | |
game_name = game_name_new | |
return project_path, game_name | |
def decompile(project_path, game_name, noisy=True, output_info=True): | |
if output_info: | |
print("\nExtracting all rpa files...") | |
gamedir = os.path.join(project_path, "game") | |
success_rpas = [] | |
for rpafile in glob.glob(os.path.join(gamedir, "*.rpa")): | |
try: | |
print_path(rpafile, gamedir, noisy=noisy) | |
rpa = unrpa.UnRPA(rpafile, path=gamedir, mkdir=True) | |
rpa.extract_files() | |
success_rpas.append(rpafile) | |
except Exception: | |
if output_info: | |
traceback.print_exc() | |
else: | |
raise | |
if output_info: | |
print("Extraction complete.\n") | |
if output_info: | |
print("Decompiling .rpyc files...") | |
success_rpycs = [] | |
for rpycfile in glob.glob(os.path.join(gamedir, "**/*.rpy*c"), recursive=True): | |
print_path(rpycfile, gamedir, noisy=noisy) | |
if not noisy: | |
old_stdout = sys.stdout # backup current stdout | |
sys.stdout = open(os.devnull, "w") | |
if unrpyc.decompile_rpyc(rpycfile, overwrite=True): | |
success_rpycs.append(rpycfile) | |
if not noisy: | |
sys.stdout = old_stdout | |
if output_info: | |
print("Decompiliaton complete.\n") | |
if output_info: | |
print("Writing dummy project.json file...") | |
with open(os.path.join(project_path, "project.json"), "w") as pinfo: | |
pinfo.write('{"renamed_all": true, "renamed_steam": true, "force_recompile": true, "build_update": false, "packages": ["market"], "add_from": false}') | |
if output_info: | |
print("Done.\n") | |
if output_info: | |
print("Deleting unneeded files and directories...") | |
del_files = UNNEEDED_FILES + success_rpas + success_rpycs | |
for dir in UNNEEDED_DIRS: | |
adir = os.path.join(project_path, dir.replace("?", game_name)) | |
print_path(adir, project_path, noisy=noisy) | |
if os.path.exists(adir): | |
try: | |
shutil.rmtree(adir) | |
except: | |
if output_info: | |
traceback.print_exc() | |
else: | |
raise | |
for file in del_files: | |
afile = file.replace("?", game_name) | |
if not os.path.isabs(afile): | |
afile = os.path.join(project_path, afile) | |
print_path(afile, project_path, noisy=noisy) | |
if os.path.exists(afile): | |
try: | |
os.remove(afile) | |
except: | |
if output_info: | |
traceback.print_exc() | |
else: | |
raise | |
if output_info: | |
print("Done. Some files may be not deleted.\n") | |
if __name__ == "__main__": | |
try: | |
project_path, game_name = get_game_interactive() | |
decompile(project_path, game_name) | |
print("You should be able to open the new project at " + project_path + " with the Ren'Py launcher now.") | |
except KeyboardInterrupt: | |
print("^C") | |
pass |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment