import argparse from collections import OrderedDict import glob import os import shutil from construct import * from tqdm import tqdm setglobalstringencoding(None) ScrapFile = Struct( 'path'/PascalString(Int32ul), 'size'/Int32ul, 'offset'/Int32ul, 'data'/OnDemandPointer(this.offset,Bytes(this.size)), ) DummyFile = Struct( 'path'/PascalString(Int32ul), 'size'/Int32ul, 'offset'/Int32ul, ) PackedHeader = Struct( Const(b'BFPK'), Const(b'\0\0\0\0'), 'files'/PrefixedArray(Int32ul,ScrapFile), ) DummyHeader = Struct( Const(b'BFPK'), Const(b'\0\0\0\0'), 'files'/PrefixedArray(Int32ul,DummyFile), ) parser = argparse.ArgumentParser(description='Unpack and Repack .packed files') parser.add_argument('-u', '--unpack', action='store_true', help='unpack file to \'extracted\' directory') parser.add_argument('-r', '--repack', action='store_true', help='repack file from \'extracted\' directory') parser.add_argument( '--reset', action='store_true', default=False, help='restore backup') parser.add_argument( 'scrap_dir', metavar='Scrapland Directory', type=str, default=".", help='Scrapland installation directory') options = parser.parse_args() scrap_dir = os.path.abspath(options.scrap_dir) if options.reset: print('Restoring Backups and removing extracted folder...') for packed_file in glob.glob(os.path.join(scrap_dir, '*.packed.bak')): outfile = os.path.basename(packed_file) orig_filename = outfile[:-4] if os.path.isfile(outfile): print('deleting', orig_filename) os.remove(orig_filename) print('moving', outfile, '->', orig_filename) shutil.move(outfile, orig_filename) target_folder = os.path.join( 'extracted', os.path.basename(orig_filename)) print('deleting', target_folder) shutil.rmtree(target_folder) if os.path.isdir('extracted'): input('Press enter to remove rest of extracted folder') shutil.rmtree('extracted') exit('Done!') if not (options.unpack or options.repack): parser.print_help() exit() pstatus = '' if options.unpack: if os.path.isdir('extracted'): print("Removing extracted folder") shutil.rmtree('extracted') for packed_file in glob.glob(os.path.join(scrap_dir, '*.packed')): os.chdir(scrap_dir) BN=os.path.basename(packed_file) target_folder = os.path.join( 'extracted', os.path.basename(packed_file)) os.makedirs(target_folder, exist_ok=True) os.chdir(target_folder) print('Unpacking {}'.format(os.path.basename(packed_file))) with open(packed_file, 'rb') as pkfile: data = PackedHeader.parse_stream(pkfile) print("Offset:",hex(pkfile.tell())) for file in tqdm(data.files,ascii=True): folder, filename = os.path.split(file.path) if folder: os.makedirs(folder, exist_ok=True) with open(file.path, 'wb') as outfile: outfile.write(file.data()) print('\r' + ' ' * len(pstatus) + '\r', end='', flush=True) os.chdir(scrap_dir) if (options.unpack and options.repack): input('Press enter to rebuild *.packed files from folders in \'extracted\' dir...') # noqa pass def file_gen(files,offset=0): for real_path,size,path in files: file=dict( path=path, offset=offset, size=size) yield file offset+=file['size'] def make_header(files,offset=0): files_list=list(file_gen(files,offset)) return DummyHeader.build(dict(files=files_list)) if options.repack: for folder in glob.glob(os.path.join(scrap_dir, 'extracted', '*.packed')): data=[] filename=os.path.join(scrap_dir,os.path.basename(folder)) for root,folders,files in os.walk(folder): for file in sorted(files): file=os.path.join(root,file) rel_path=bytes(file.replace(folder, '').replace('\\', '/').lstrip('/'), 'windows-1252') size=os.stat(file).st_size data.append((file,size,rel_path)) print("Found {} files for {}".format(len(data),filename)) offset=len(make_header(data)) print("Writing",filename) header=make_header(data,offset) with open(filename,"wb") as outfile: outfile.write(header) for file,size,rel_path in tqdm(data,ascii=True): outfile.write(open(file,"rb").read()) print('Done!')