Skip to content

icecap.infrastructure.resource

The module provides tooling to manipulate game assets.

World of Warcraft assets are stored in MPQ archives. Depending on the archive name, the assets are loaded with different priorities.

Currently, the module provides tools to load MPQ archives, list and extract individual files from the archives.

Classes:

MPQArchive

MPQArchive(path: str)

Represents an MPQ archive.

MPQ is a proprietary archive format used by Blizzard Entertainment. Read more about it here: https://wowdev.wiki/MPQ

The implementation is very naive and most likely won't work with post-WOTLK expansions.

Source code in icecap/infrastructure/resource/mpq/archive.py
def __init__(self, path: str):
    self.file_path = path

    self.file = open(path, "rb")
    self.crypt = Crypt()

    self._header: Header | None = None
    self._hash_table: HashTable | None = None
    self._block_table: BlockTable | None = None
    self._file_names: list[str] | None = None

file_exists

file_exists(filename: str) -> bool

Check if a file exists in the archive.

Source code in icecap/infrastructure/resource/mpq/archive.py
def file_exists(self, filename: str) -> bool:
    """Check if a file exists in the archive."""
    return self.get_hash_table_entry(filename) is not None

get_block_table

get_block_table() -> BlockTable

Get the block table of the archive.

The block table contains metadata about the position and size of each file in the archive.

Source code in icecap/infrastructure/resource/mpq/archive.py
def get_block_table(self) -> BlockTable:
    """Get the block table of the archive.

    The block table contains metadata about the position and size of each file in the archive.
    """
    if self._block_table:
        return self._block_table

    header = self.get_header()

    key = self.crypt.hash("(block table)", HashType.TABLE)

    self.file.seek(header.block_table_offset)
    data = self.file.read(header.block_table_size * 16)
    data = self.crypt.decrypt(data, key)

    def unpack_entry(position):
        entry_data = data[position * 16 : position * 16 + 16]
        return BlockTableEntry(*struct.unpack("<4I", entry_data))

    self._block_table = BlockTable([unpack_entry(i) for i in range(header.block_table_size)])
    return self._block_table

get_file_names

get_file_names() -> list[str]

Get a list of file names in the archive.

The list is lazily loaded the first time this method is called.

Source code in icecap/infrastructure/resource/mpq/archive.py
def get_file_names(self) -> list[str]:
    """Get a list of file names in the archive.

    The list is lazily loaded the first time this method is called.
    """
    if self._file_names:
        return self._file_names

    listfile_data = self.read_file("(listfile)")

    if listfile_data is None:
        self._file_names = []
        return self._file_names

    self._file_names = listfile_data.decode().splitlines()

    return self._file_names

get_hash_table

get_hash_table() -> HashTable

Get the hash table of the archive.

This is a classical hash table used to quickly locate files in the archive.

Source code in icecap/infrastructure/resource/mpq/archive.py
def get_hash_table(self) -> HashTable:
    """Get the hash table of the archive.

    This is a classical hash table used to quickly locate files in the archive.
    """
    if self._hash_table:
        return self._hash_table

    header = self.get_header()

    key = self.crypt.hash("(hash table)", HashType.TABLE)

    self.file.seek(header.hash_table_offset)
    data = self.file.read(header.hash_table_size * 16)
    data = self.crypt.decrypt(data, key)

    def unpack_entry(position):
        entry_data = data[position * 16 : position * 16 + 16]
        return HashTableEntry(*struct.unpack("<2I2HI", entry_data))

    self._hash_table = HashTable([unpack_entry(i) for i in range(header.hash_table_size)])
    return self._hash_table

get_hash_table_entry

get_hash_table_entry(filename: str) -> HashTableEntry | None

Get the hash table entry corresponding to a given filename.

Source code in icecap/infrastructure/resource/mpq/archive.py
def get_hash_table_entry(self, filename: str) -> HashTableEntry | None:
    """Get the hash table entry corresponding to a given filename."""
    hash_a = self.crypt.hash(filename, HashType.HASH_A)
    hash_b = self.crypt.hash(filename, HashType.HASH_B)

    for entry in self.get_hash_table().entries:
        if entry.name1 == hash_a and entry.name2 == hash_b:
            return entry

    return None

get_header

get_header() -> Header

Get the MPQ header.

The header contains metadata about the archive. The header is lazily loaded the first time this method is called.

Source code in icecap/infrastructure/resource/mpq/archive.py
def get_header(self) -> Header:
    """Get the MPQ header.

    The header contains metadata about the archive.
    The header is lazily loaded the first time this method is called.
    """
    if self._header:
        return self._header

    magic = self.file.read(4)
    self.file.seek(0)

    if magic == b"MPQ\x1b":
        raise ValueError("MPQ shunts are not supported.")
    elif magic != b"MPQ\x1a":
        raise ValueError("Invalid MPQ header.")

    data = self.file.read(32)
    header = Header(*struct.unpack("<4s2I2H4I", data))
    if header.format_version == 1:
        header_extension_data = self.file.read(12)
        header.extension = HeaderExtension(*struct.unpack("<q2h", header_extension_data))
    if header.format_version > 1:
        raise ValueError("Unsupported MPQ format version.")

    self._header = header
    return self._header

read_file

read_file(filename) -> bytes | None

Read a file from the MPQ archive.

Source code in icecap/infrastructure/resource/mpq/archive.py
def read_file(self, filename) -> bytes | None:
    """
    Read a file from the MPQ archive.
    """
    # Get file metadata
    hash_entry = self.get_hash_table_entry(filename)
    if hash_entry is None:
        return None

    header = self.get_header()
    block_table = self.get_block_table()
    block_entry = block_table.entries[hash_entry.block_index]

    # Check if the file exists and has content
    if not (block_entry.flags & MPQ_FILE_EXISTS):
        return None

    if block_entry.compressed_size == 0:
        return None

    # Read file resource
    self.file.seek(block_entry.file_position)
    file_data = self.file.read(block_entry.compressed_size)

    # Check for encryption
    if block_entry.flags & MPQ_FILE_ENCRYPTED:
        raise NotImplementedError("Encryption is not supported yet.")

    # Process file based on its structure
    if block_entry.flags & MPQ_FILE_SINGLE_UNIT:
        return self._process_single_unit_file(file_data, block_entry)
    else:
        return self._process_multi_sector_file(file_data, block_entry, header.block_size)