Skip to content

Instantly share code, notes, and snippets.

@Dobby233Liu
Last active May 29, 2022 06:50
Show Gist options
  • Save Dobby233Liu/3c2832664650ae68d2471e7bbb8a78ee to your computer and use it in GitHub Desktop.
Save Dobby233Liu/3c2832664650ae68d2471e7bbb8a78ee to your computer and use it in GitHub Desktop.
"""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