# pyright: reportAny=false """ A script to work with the packed filesystem used by CardMaker (CardMaker_Data/StreamingAssets/MU3/Data/A000.pac). Usage: - Extract the contents of a pac file: python cmpack.py x <.pac file> - Create a pac file from a base directory: python cmpack.py a <.pac file> - List contents of a .pac file: python cmpack.py l <.pac file> The pac file format: - Header (32 bytes) - Magic: 50 41 43 4B ("PACK") - Reserved: The game fills in 4 random bytes. - Number of entries: 64-bit signed integer - Offset of string table: 64-bit signed integer. The string table is used for storing filenames, and is encoded with UTF-16 LE. - Offset of file data: 64-bit signed integer. - Entries (28 byte * number of entries) - Type: 32-bit signed integer - 0: File - 1: Directory - Flags: 32-bit signed integer, always set to 0. - Offset of filename: 32-bit signed integer, is the *character* offset of the filename in the string. **NOT A BYTE OFFSET!** - Length of filename: 32-bit signed integer - Length of file data: 32-bit signed integer - Offset of file data: 64-bit signed integer, starting from the offset of file data declared in the header. - String table - File data - Adler32 checksum of all of the above """ import argparse import os import random import struct import zlib from collections.abc import Generator from dataclasses import dataclass from io import BufferedIOBase from pathlib import Path from typing import Any, NamedTuple, cast PACK_MAGIC = 0x4B434150 PACK_HEADER_FORMAT = " None: if entry.type != 0: raise ValueError(f"Invalid entry ID {entry.type} (expected 0).") self.name = string_table[ entry.name_offset : entry.name_offset + entry.name_length ] self.offset = entry.data_offset self.length = entry.data_length class PackFolder: def __init__( self, entries: list[PackEntry], entry_id: int, string_table: str ) -> None: entry = entries[entry_id] if entry.type != 1: raise ValueError(f"Invalid entry ID {entry.type} (expected 1).") self.name = string_table[ entry.name_offset : entry.name_offset + entry.name_length ] self.files: dict[str, PackFile] = {} self.folders: dict[str, PackFolder] = {} for i in range(entry.data_length): child_id = entry.data_offset + i child_entry = entries[child_id] if child_entry.type == 0: f = PackFile(child_entry, string_table) self.files[f.name] = f elif child_entry.type == 1: f = PackFolder(entries, child_id, string_table) self.folders[f.name] = f else: raise ValueError( f"Entry {child_id} has unknown type {child_entry.type}." ) @dataclass class Pack: header: PackHeader entries: list[PackEntry] root: PackFolder def decode_pack_meta(f: BufferedIOBase): header = PackHeader._make(struct.unpack(PACK_HEADER_FORMAT, f.read(32))) if header.magic != PACK_MAGIC: raise Exception(f"Invalid pack file: {header.magic:x} != {PACK_MAGIC:x}") _ = f.seek(header.offset_string, os.SEEK_SET) string_table = f.read(header.offset_data - header.offset_string).decode("utf-16-le") _ = f.seek(32, os.SEEK_SET) entries: list[PackEntry] = [] for _ in range(header.num_entries): entries.append(PackEntry._make(struct.unpack(PACK_ENTRY_FORMAT, f.read(28)))) tree = PackFolder(entries, 0, string_table) return Pack(header, entries, tree) def traverse_pack( folder: PackFolder, path: Path ) -> Generator[tuple[Path, PackFile], Any, None]: for file in folder.files.values(): yield (path / file.name, file) for folder in folder.folders.values(): child_path = path / folder.name yield from traverse_pack(folder, child_path) def walk_breadth_first(root: Path) -> Generator[tuple[Path, list[Path]], Any, None]: children = list(root.iterdir()) yield root, children for path in children: if path.is_dir(): yield from walk_breadth_first(path) def pack_extract(args: argparse.Namespace): archive_name = cast(Path, args.archive_name) output_directory = cast(Path, args.output_directory) if output_directory.exists() and not output_directory.is_dir(): raise ValueError(f"Output {output_directory} is not a folder.") if not output_directory.exists(): output_directory.mkdir(parents=True) with archive_name.open("rb") as f: actual_checksum = zlib.adler32(f.read()[:-4]) _ = f.seek(-4, os.SEEK_END) expected_checksum = struct.unpack("