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
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
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 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 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)
|