neoLegacy/tools/pck_extract.py
Fireblade 314a453996 chore: correct dlc loading 'regex' + add extra helper tools to the tools folder
msscmp_extract is made by me idk where i got the other two from
2026-05-20 15:52:52 -04:00

232 lines
4.8 KiB
Python

#!/usr/bin/env python3
import struct
import argparse
import zipfile
import os
import sys
# default endian mode
ENDIAN = ">"
def detect_endianness(f):
"""
Detect whether the PCK uses big-endian or little-endian.
"""
global ENDIAN
pos = f.tell()
raw = f.read(4)
if len(raw) != 4:
raise EOFError("File too small")
be = struct.unpack(">I", raw)[0]
le = struct.unpack("<I", raw)[0]
# detect endianness based off of version numbers [usually 3 or 4]
if 0 < be < 100:
ENDIAN = ">"
elif 0 < le < 100:
ENDIAN = "<"
else:
raise ValueError("Could not determine endianness")
f.seek(pos)
print(f"Detected {'Big' if ENDIAN == '>' else 'Little'} Endian")
def read_u32(f):
data = f.read(4)
if len(data) != 4:
raise EOFError("Unexpected EOF while reading uint32")
return struct.unpack(f"{ENDIAN}I", data)[0]
def read_utf16_string(f):
"""
PCK strings are:
uint32 length
UTF-16 bytes
uint32 padding
"""
length = read_u32(f)
if length > 100000:
raise ValueError(f"Unreasonable string length: {length}")
raw = f.read(length * 2)
if len(raw) != length * 2:
raise EOFError("Unexpected EOF while reading string")
encoding = "utf-16-be" if ENDIAN == ">" else "utf-16-le"
text = raw.decode(encoding, errors="replace")
# skip padding
padding = f.read(4)
if len(padding) != 4:
raise EOFError("Unexpected EOF while reading string padding")
return text
def extract_pck_to_zip(input_file, output_zip):
with open(input_file, "rb") as f:
# detect endianness before reading anything
detect_endianness(f)
# ----- HEADER -----
pck_type = read_u32(f)
param_count = read_u32(f)
print(f"PCK Type: {pck_type}")
print(f"Parameter Count: {param_count}")
# ----- PARAMETER LOOKUP TABLE -----
lookup = [None] * param_count
for _ in range(param_count):
idx = read_u32(f)
key = read_utf16_string(f)
if idx >= param_count:
raise ValueError(f"Invalid parameter index: {idx}")
lookup[idx] = key
# Optional XMLVERSION field
if "XMLVERSION" in lookup:
xml_version = read_u32(f)
print(f"XML Version: {xml_version}")
# ----- ASSET TABLE -----
asset_count = read_u32(f)
print(f"Asset Count: {asset_count}")
assets = []
for i in range(asset_count):
size = read_u32(f)
asset_type = read_u32(f)
name = read_utf16_string(f)
name = name.replace("\\", "/")
print(f"[{i+1}/{asset_count}] {name} ({size} bytes)")
assets.append({
"name": name,
"size": size,
"type": asset_type,
})
# ----- ASSET DATA -----
for asset in assets:
asset_param_count = read_u32(f)
params = {}
for _ in range(asset_param_count):
key_index = read_u32(f)
value = read_utf16_string(f)
if key_index < len(lookup):
key = lookup[key_index]
params[key] = value
asset["params"] = params
data = f.read(asset["size"])
if len(data) != asset["size"]:
raise EOFError(
f"Unexpected EOF while reading asset data: {asset['name']}"
)
asset["data"] = data
# ----- WRITE ZIP -----
print(f"\nWriting ZIP: {output_zip}")
with zipfile.ZipFile(
output_zip,
"w",
compression=zipfile.ZIP_DEFLATED
) as zf:
for asset in assets:
zip_name = asset["name"].lstrip("/")
if not zip_name:
continue
print(f"Adding: {zip_name}")
zf.writestr(zip_name, asset["data"])
print("\nDone!")
def main():
parser = argparse.ArgumentParser(
description="Convert Minecraft Legacy Console .pck files to .zip"
)
parser.add_argument(
"input",
help="Input .pck file"
)
parser.add_argument(
"-o",
"--output",
help="Output zip filename"
)
args = parser.parse_args()
input_path = args.input
if not os.path.isfile(input_path):
print(f"Input file not found: {input_path}")
sys.exit(1)
output_path = args.output
if not output_path:
output_path = os.path.splitext(input_path)[0] + ".zip"
try:
extract_pck_to_zip(input_path, output_path)
except Exception as e:
print(f"\nERROR: {e}")
sys.exit(1)
if __name__ == "__main__":
main()