Skip to content

Instantly share code, notes, and snippets.

@SirTony
Created May 12, 2017 06:35
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SirTony/6d3e89aaf082113af653da7933b39723 to your computer and use it in GitHub Desktop.
Save SirTony/6d3e89aaf082113af653da7933b39723 to your computer and use it in GitHub Desktop.
RenPy archive unpacker
from pickle import loads as unpickle
from zlib import decompress
from argparse import ArgumentParser
from sys import exit, stderr
import os
def _main():
parser = ArgumentParser( description = "RenPy Arhive (.rpa) unpacker" )
parser.add_argument( "-o", "--output", required = False, type = str, dest = "output", metavar = "dir", help = "The directory to output files to" )
parser.add_argument( "-i", "--input", required = True, type = str, dest = "input", metavar = "path", help = "The archive to unpack" )
args = parser.parse_args()
args.input = os.path.normpath( args.input )
if args.output is not None:
args.output = os.path.normpath( args.output )
else:
base = os.path.dirname( args.input )
name, _ = os.path.splitext( os.path.basename( args.input ) )
args.output = os.path.join( base, f'{name}_data' )
if not _file_exists( args.input ):
_err( "input file not found" )
return 1
if not _dir_exists( args.output ):
os.makedirs( args.output )
with open( args.input, "rb" ) as rpa:
unpack = _compose(
rpa.read,
decompress,
unpickle
)
magic, offset, key = rpa.readline().split()
if magic != b"RPA-3.0":
_err( "invalid RenPy file or version" )
return 2
offset = int( offset, 16 )
key = int( key, 16 )
rpa.seek( offset )
index = unpack()
for name in index.keys():
print( "unpacking {}".format( name ) )
dest = os.path.join( args.output, os.path.normpath( name ) )
dir = os.path.dirname( dest )
if not _dir_exists( dir ):
os.makedirs( dir )
offset = index[name][0][0] ^ key
size = index[name][0][1] ^ key
head = index[name][0][2]
rpa.seek( offset )
with open( dest, "wb" ) as f:
f.write( bytes( head, 'utf-8' ) )
f.write( rpa.read( size ) )
f.flush()
return 0
def _err( *a, **kw ):
print( *a, file = stderr, **kw )
def _file_exists( path ):
return os.path.exists( path ) and os.path.isfile( path )
def _dir_exists( path ):
return os.path.exists( path ) and os.path.isdir( path )
def _compose( *funcs ):
def composed( *args ):
first = funcs[0]
result = first( *args )
for func in funcs[1:]:
if result is not None:
result = func( result )
else:
result = func()
return result
return composed
if __name__ == "__main__":
code = _main()
exit( code )
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment