But here’s the problem: modern Windows doesn’t open PK2 files. Double-clicking does nothing. So what do you do when you need to extract that one weapon texture or edit a quest script?
# Write to disk with open(out_path, "wb") as out_f: out_f.write(data) print(f"Extracted: file_path")
python pk2_extractor.py game_data.pk2 ./extracted You’ll see output like:
print("Done!") if == " main ": import sys if len(sys.argv) < 3: print("Usage: python pk2_extractor.py <file.pk2> <output_folder>") else: extract_pk2(sys.argv[1], sys.argv[2]) Step 4: Running the Extractor Open a terminal and run: pk2 extractor
If you’ve ever tried to mod a late-90s or early-2000s PC game, you’ve likely encountered a .PK2 file. Used most famously in Sacred (Ascaron Entertainment) and a handful of other titles, the PK2 format is a simple but effective archive that bundles textures, scripts, sounds, and levels.
# Jump to index table f.seek(index_offset)
| Offset | Size (bytes) | Description | |--------|--------------|-------------| | 0 | 4 | Magic header ( PK20 or PK2 ) | | 4 | 4 | Version (usually 2) | | 8 | 4 | Number of files | | 12 | 4 | Offset to file index table | | 16 | 4 | Unknown/Reserved | | 20 | ... | File index entries | But here’s the problem: modern Windows doesn’t open
# Read file data f.seek(file_offset) data = f.read(compressed_size)
– [Your Name]
In this post, I’ll walk through the PK2 format, write a lightweight Python extractor from scratch, and show you how to unpack those archives in seconds. After reversing a few sample PK2 files (and thanks to open-source community notes), the format breaks down like this: # Write to disk with open(out_path, "wb") as out_f: out_f
# Prepare output path out_path = os.path.join(output_dir, file_path) os.makedirs(os.path.dirname(out_path), exist_ok=True)
version, num_files, index_offset = struct.unpack("<III", f.read(12)) print(f"Version: version, Files: num_files, Index at: index_offset")
# Save current position to read file name current_pos = f.tell() f.seek(name_offset) file_path = f.read(256).split(b"\x00")[0].decode("utf-8", errors="ignore") f.seek(current_pos)