PVZ 的 main.pak 文件包含了游戏的贴图、动画和字符串等资源,它使用了宝开自定义的一种简单格式,似乎宝石迷阵也使用了一样的格式。互联网上没有
简明的文档,因此记录如下。
异或加密
文件中所有字节都经过了 0xF7 异或加密。以下描述的是经过 0xF7 异或解密的内容,按顺序排列。
魔法数字
长度: 8 字节。
文件的前 8 字节是固定的内容,内容如下(经过 0xF7 异或解密后):
1 2 3
| +------+------+------+------+------+------+------+------+ | 0xBA | 0xC0 | 0x4A | 0xC0 | 0x00 | 0x00 | 0x00 | 0x00 | +------+------+------+------+------+------+------+------+
|
前四个字节经过 0xF7 异或之后是 CP1252 编码的 7½7M。
后四字节似乎是保留字段,恒为 0。
文件描述区
长度: 所有文件描述数据结构之和
文件描述区由以下结构重复多遍组成,直到读到 0x80 为止。
| offset |
size |
description |
| 0 |
1 |
恒为0,用来判断文件描述区是否结束 |
| 1 |
1 |
文件名字符串长度 |
| 2 |
N |
文件名字符串 |
| N+2 |
4 |
文件大小 |
| N+6 |
8 |
文件时间 |
文件名字符串中没有结尾空字符,包含子目录前缀,路径分隔符为反斜杠 \。
文件大小为小端序,表示字节数。
文件时间见 FILETIME,是小端序的 64
位整数,表示自 1601-01-01 00:00:00 UTC 以来的 100ns 数。
文件描述区结束标记
长度: 1 字节。
内容: 0x80
用来判断文件描述区结束,从下个字节开始是文件数据区。
文件数据区
长度: 所有文件长度之和。
文件数据区中内容与文件描述区中每一记录一一对应。
参考
解码脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
|
import sys import os import time
def main(): filename = sys.argv[1]
with open(filename, 'rb') as f: data = f.read()
print(f'magic number: 0x{int.from_bytes(data[0:4]):08X} ({data[0:4].decode('latin-1')})')
assert data[0:4] == b'\x37\xBD\x37\x4D'
data = bytes(b ^ 0xf7 for b in data)
assert data[4:8] == b'\x00\x00\x00\x00'
print('-----')
i = 8 cnt = 0 files: list[tuple[str, int, float]] = [] while data[i] == 0: fnl = data[i + 1] file_name = data[i+2:i+2+fnl].decode() file_size = int.from_bytes(data[i+2+fnl:i+2+fnl+4], 'little') file_time = ms_filetime_to_unix(int.from_bytes(data[i+2+fnl+4:i+2+fnl+12], 'little')) files.append((file_name, file_size, file_time)) if len(sys.argv) == 2: print(f'name: {file_name}') print(f'size: {file_size} ({format_size(file_size)})') utc_time = time.gmtime(file_time) print(f'time: {time.strftime("%Y-%m-%d %H:%M:%S UTC", utc_time)}') print('-----') i += fnl + 14 cnt += 1 print(f'total: {cnt} files')
assert data[i] == 0x80
i += 1 if len(sys.argv) > 2: dst_dir = sys.argv[2] os.makedirs(dst_dir, exist_ok=True) for name, size, file_time in files: name = name.replace('\\', '/') file_path = os.path.join(dst_dir, name) os.makedirs(os.path.dirname(file_path), exist_ok=True) with open(file_path, 'wb') as fout: fout.write(data[i:i + size]) i += size os.utime(file_path, (file_time, file_time)) print(f'write {file_path}')
def ms_filetime_to_unix(ft): return ft/1e7 - 11644473600
def format_size(size) -> str: if size < 1024: return f'{size}B' elif size < 1024 ** 2: return f'{size/1024:0.1f}K' elif size < 1024 ** 3: return f'{size/1024**2:0.1f}M' else: return f'{size/1024**3:0.1f}G'
main()
|
用法:保存为 pak.py,在命令行中运行 python3 pak.py <path-to-pak> [dst_dir],如果提供了 dst_dir 参数,则会将文件提取到指定目录,
否则只会打印文件列表。