SirTony
5/12/2017 - 6:35 AM

RenPy archive unpacker

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 )