SHOW:
|
|
- or go back to the newest paste.
| 1 | #!/usr/bin/env python | |
| 2 | # python 2.7 | |
| 3 | ||
| 4 | import argparse | |
| 5 | import struct | |
| 6 | import zlib | |
| 7 | import re | |
| 8 | import io | |
| 9 | ||
| 10 | cimg_ids = [] | |
| 11 | cimg_blocks = dict() | |
| 12 | ||
| 13 | - | # Savefile format: |
| 13 | + | # Savegame format: |
| 14 | # ---------------- | |
| 15 | # 4752 494D 0400 0000 F14E 0B00 789C EC7D ... ... | |
| 16 | # ^^^^ ^^^^ Block name = "GRIM" | |
| 17 | # ^^^^ ^^^^ Block size = 4 (little-endian) | |
| 18 | # ^^^^ ^^^^ Uncompressed data size in bytes (little-endian) | |
| 19 | # ^^^^ Zlib data header (always 789C) | |
| 20 | # ^^^^ ^^^^ ^^^^ Zlib data | |
| 21 | - | def parse_savefile(filename, insert_cimg = False, keep_uncompressed = False): |
| 21 | + | |
| 22 | def parse_savegame(filename, insert_cimg = False, keep_uncompressed = False): | |
| 23 | global cimg_ids, cimg_blocks | |
| 24 | if insert_cimg: | |
| 25 | cimg_ids = [] | |
| 26 | else: | |
| 27 | cimg_blocks = dict() | |
| 28 | ||
| 29 | savefile_in, savefile_out, uncompfile_out = None, None, None | |
| 30 | try: | |
| 31 | savefile_in = open(filename,'rb') | |
| 32 | print "Parsing %s:" % filename | |
| 33 | ||
| 34 | if insert_cimg: | |
| 35 | out_filename = re.sub(r'\.sav', '.edited.sav', filename) | |
| 36 | if out_filename == filename: | |
| 37 | raise Exception("Input savefiles must have the .sav extension")
| |
| 38 | savefile_out = open(out_filename, 'wb') | |
| 39 | ||
| 40 | uncomp_data_out = "" | |
| 41 | ||
| 42 | filetype = savefile_in.read(4) | |
| 43 | if filetype != "GRIM": | |
| 44 | raise Exception("Not a valid savefile")
| |
| 45 | ||
| 46 | block_size= savefile_in.read(8) # Skip the next 8 bytes | |
| 47 | zlib_data = savefile_in.read(-1) # Read all the rest (= the compressed data) | |
| 48 | ||
| 49 | data_stream = io.BytesIO(zlib.decompress(zlib_data)) | |
| 50 | ||
| 51 | while True: | |
| 52 | skip_flag = False | |
| 53 | ||
| 54 | block_name = data_stream.read(4) | |
| 55 | if block_name == "": | |
| 56 | break | |
| 57 | block_size = struct.unpack("<L",data_stream.read(4))[0]
| |
| 58 | block_data = data_stream.read(block_size) | |
| 59 | ||
| 60 | if block_name == "CHAR": | |
| 61 | char_id = struct.unpack("<H",block_data[10:12])[0]
| |
| 62 | name_tag = block_data[12:16] | |
| 63 | name_sz = struct.unpack("<L",block_data[24:28])[0]
| |
| 64 | if name_tag == "CHAM": | |
| 65 | print " Found character %s (ID: %04X)" % (str(block_data[28:28+name_sz]), char_id) | |
| 66 | ||
| 67 | if insert_cimg: | |
| 68 | cimg_ids.append(char_id) | |
| 69 | if char_id not in cimg_blocks: | |
| 70 | raise Exception("No custom portrait for character ID %04X" % char_id)
| |
| 71 | ||
| 72 | elif block_name == "CIMG": | |
| 73 | if insert_cimg: | |
| 74 | skip_flag = True # Skip this block (will be replaced by a new custom img) | |
| 75 | else: | |
| 76 | char_id = struct.unpack("<H",block_data[10:12])[0]
| |
| 77 | print " Found custom portrait (ID: %04X)" % char_id | |
| 78 | cimg_blocks[char_id] = block_name + struct.pack("<L", block_size) + block_data
| |
| 79 | ||
| 80 | elif block_name == "PRTY": | |
| 81 | if insert_cimg: | |
| 82 | for char_id in cimg_ids: | |
| 83 | uncomp_data_out += cimg_blocks[char_id] | |
| 84 | print " Inserted custom portraits" | |
| 85 | ||
| 86 | if not skip_flag: | |
| 87 | uncomp_data_out += block_name + struct.pack("<L", block_size) + block_data
| |
| 88 | ||
| 89 | if insert_cimg: | |
| 90 | savefile_out.write("GRIM" + struct.pack("<LL", 4, len(uncomp_data_out)))
| |
| 91 | savefile_out.write(zlib.compress(uncomp_data_out)) | |
| 92 | ||
| 93 | if keep_uncompressed: | |
| 94 | uncomp_filename = re.sub(r'\.sav', '.uncompressed.sav', filename) | |
| 95 | if uncomp_filename == filename: | |
| 96 | raise Exception("Input savefiles must have the .sav extension")
| |
| 97 | uncompfile_out = open(uncomp_filename, 'wb') | |
| 98 | uncompfile_out.write(uncomp_data_out) | |
| 99 | ||
| 100 | finally: | |
| 101 | if savefile_in: | |
| 102 | savefile_in.close() | |
| 103 | if savefile_out: | |
| 104 | savefile_out.close() | |
| 105 | if uncompfile_out: | |
| 106 | uncompfile_out.close() | |
| 107 | ||
| 108 | # | |
| 109 | # Main() | |
| 110 | # | |
| 111 | - | parser = argparse.ArgumentParser(description='Parse LoG savefiles') |
| 111 | + | |
| 112 | parser = argparse.ArgumentParser(description='Copy custom portraits from one LoG save file to another') | |
| 113 | - | help='Log Save Files (v1.1.4 or above) with .sav extension') |
| 113 | + | |
| 114 | help='LoG save files (v1.1.4 or above) with .sav extension') | |
| 115 | parser.add_argument('-k', dest='keep_compressed', action='store_true',
| |
| 116 | help='Keep the uncompressed data (with extension .uncompressed.sav)') | |
| 117 | ||
| 118 | args = parser.parse_args() | |
| 119 | ||
| 120 | - | parse_savefile(args.savefiles[0]) |
| 120 | + | |
| 121 | parse_savegame(args.savefiles[0]) | |
| 122 | ||
| 123 | - | parse_savefile(args.savefiles[1], True, args.keep_compressed) |
| 123 | + | |
| 124 | parse_savegame(args.savefiles[1], True, args.keep_compressed) | |
| 125 | ||
| 126 | if __name__ == "__main__": | |
| 127 | main() |