Created
August 24, 2024 18:29
-
-
Save a-sakharov/5ccb418f8037855eeeecac6fc0345ed6 to your computer and use it in GitHub Desktop.
Plants vs Zombies main.pak unpacker
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| # Plants vs Zombies main.pak unpacker | |
| import os | |
| from calendar import timegm | |
| from datetime import datetime, timezone | |
| INPUT_FILE="main.pak" | |
| OUTPUT_DIR="main_unpacked" | |
| HEADER_MAGICK=0xBAC04AC0 | |
| HEADER_VERSION=0x00000000 | |
| EPOCH_AS_FILETIME = 116444736000000000 # January 1, 1970 as filetime | |
| HUNDREDS_OF_NS = 10000000 | |
| def windows_filetime_to_datetime(filetime: int) -> datetime: | |
| """ | |
| Converts a Windows filetime number to a Python datetime. The new | |
| datetime object is timezone-naive but is equivalent to tzinfo=utc. | |
| """ | |
| # Get seconds and remainder in terms of Unix epoch | |
| s, ns100 = divmod(filetime - EPOCH_AS_FILETIME, HUNDREDS_OF_NS) | |
| # Convert to datetime object, with remainder as microseconds. | |
| return datetime.fromtimestamp(s, timezone.utc).replace(microsecond=(ns100 // 10)) | |
| if not os.path.exists(OUTPUT_DIR): | |
| os.makedirs(OUTPUT_DIR) | |
| in_data = open(INPUT_FILE, 'rb').read() | |
| in_data = list(map(lambda byte: byte^0xF7, in_data)) # decode xor "cypher" | |
| # open(OUTPUT_DIR + "/main.pak.decoded", 'wb').write(bytes(in_data)) # save decoded file | |
| data_header = bytes(in_data[:8]) | |
| in_data = in_data[8:] | |
| data_magick = int.from_bytes(data_header[0:4], byteorder='little') | |
| data_version = int.from_bytes(data_header[4:8], byteorder='little') | |
| if data_magick != HEADER_MAGICK: | |
| print("Invalid header. Magick is {:x}, should be {:x}".format(data_magick, HEADER_MAGICK)) | |
| exit(-1) | |
| if data_version != HEADER_VERSION: | |
| print("Invalid header. Version is {:x}, should be {:x}".format(data_version, HEADER_VERSION)) | |
| exit(-1) | |
| class DataItemRecord: | |
| name = "" | |
| file_size = 0 | |
| date = 0 | |
| def __init__(self, name: str, file_size: int, date: datetime): | |
| self.name = name | |
| self.file_size = file_size | |
| self.date = date | |
| item_records = [] | |
| catalog_size = 0 | |
| while True: | |
| record_type = in_data[catalog_size] | |
| if record_type != 0: | |
| catalog_size += 1 | |
| break | |
| name_length = in_data[catalog_size+1] | |
| name = ''.join([chr(x) for x in in_data[catalog_size+2:catalog_size+2+name_length]]) | |
| file_size = int.from_bytes(bytes(in_data[catalog_size+2+name_length+0:catalog_size+2+name_length+4]), 'little') | |
| file_date = int.from_bytes(bytes(in_data[catalog_size+2+name_length+4:catalog_size+2+name_length+4+8]), 'little') | |
| item_records.append(DataItemRecord(name, file_size, windows_filetime_to_datetime(file_date))) | |
| catalog_size+=2+name_length+4+8 | |
| content_offset = 0 | |
| for record in item_records: | |
| print("{:08x} {} {}".format(record.file_size, record.date, record.name)) | |
| output_name = OUTPUT_DIR + '/' + record.name.replace('\\', '/') | |
| dirname = os.path.dirname(os.path.abspath(output_name)) | |
| if not os.path.exists(dirname): | |
| os.makedirs(dirname) | |
| res_file = open(output_name, 'wb') | |
| res_file.write(bytes(in_data[catalog_size+content_offset:catalog_size+content_offset+record.file_size])) | |
| res_file.close() | |
| os.utime(output_name, (record.date.timestamp(), record.date.timestamp())) | |
| content_offset += record.file_size |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment