๐ฆ Working with MPQ Archives
February 22, 2026 ยท View on GitHub
Overview
MPQ (Mo'PaQ) archives are Blizzard's archive format used
in World of Warcraft to store game assets. This guide covers working with
MPQ archives using warcraft-rs.
Key Features:
- โ StormLib Compatibility - Cross-implementation support
- โ Blizzard Archive Support - Handles official WoW archives (1.12.1 - 5.4.8)
- โ Bidirectional Compatibility - Archives work with both implementations
- โ Path Conversion - Forward slashes converted to backslashes
Prerequisites
Prerequisites:
- Rust programming knowledge
warcraft-rsinstalled with thempqfeature enabled- Access to World of Warcraft MPQ files (from game installation)
- File I/O knowledge in Rust
Understanding MPQ Archives
MPQ Archives
MPQ archives are file containers that store:
- Game textures (BLP files)
- Models (M2, WMO files)
- Database files (DBC)
- Audio files
- UI resources
- Scripts and configuration files
Key Features
- Compression: Multiple compression algorithms (PKWARE, zlib, bzip2)
- Encryption: Optional file encryption
- Listfiles: Internal file listings (not always present)
- Patches: Incremental updates
- Multi-locale: Language-specific file variations
Instructions
1. Opening an MPQ Archive
use wow_mpq::{Archive, OpenOptions};
fn open_mpq_archive() -> Result<Archive, Box<dyn std::error::Error>> {
// Open an MPQ archive for reading
let mut archive = Archive::open("Data/common.MPQ")?;
// Open with specific options
let options = OpenOptions::new()
.load_tables(true); // Load hash and block tables
let archive = Archive::open_with_options("Data/patch.MPQ", options)?;
Ok(archive)
}
2. Listing Files in an Archive
use wow_mpq::Archive;
fn list_archive_contents(archive: &mut Archive) -> Result<(), Box<dyn std::error::Error>> {
// List files (requires listfile to be present)
match archive.list() {
Ok(entries) => {
println!("Archive contains {} files:", entries.len());
for entry in entries {
println!(" - {} ({} bytes)", entry.name, entry.size);
// Check file attributes using the flags field
if entry.compressed_size < entry.size {
println!(" Compressed: {} -> {} bytes",
entry.compressed_size, entry.size);
}
if entry.flags != 0 {
println!(" Flags: 0x{:08X}", entry.flags);
}
}
}
Err(_) => {
println!("No listfile found in archive");
// Need exact filenames without a listfile
}
}
Ok(())
}
3. Extracting Files
use wow_mpq::Archive;
use std::fs::File;
use std::io::Write;
fn extract_file(archive: &mut Archive, filename: &str) -> Result<(), Box<dyn std::error::Error>> {
// Extract a single file
let data = archive.read_file(filename)?;
// Save to disk
let mut file = File::create(filename)?;
file.write_all(&data)?;
println!("Extracted {} ({} bytes)", filename, data.len());
Ok(())
}
fn extract_all_files(archive: &mut Archive, output_dir: &str) -> Result<(), Box<dyn std::error::Error>> {
use std::fs;
use std::path::Path;
// Create output directory
fs::create_dir_all(output_dir)?;
// Get file list from listfile (or use list_all() to include all files)
let entries = archive.list()?;
for entry in entries {
let filename = &entry.name;
let output_path = Path::new(output_dir).join(filename);
// Create subdirectories if needed
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent)?;
}
// Extract and save
match archive.read_file(filename) {
Ok(data) => {
let mut file = File::create(output_path)?;
file.write_all(&data)?;
println!("Extracted: {}", filename);
}
Err(e) => {
eprintln!("Failed to extract {}: {}", filename, e);
}
}
}
Ok(())
}
4. Working with Multiple Archives using PatchChain
The PatchChain struct provides priority-based file resolution across
multiple MPQ archives.
use wow_mpq::{PatchChain, Archive};
use std::path::PathBuf;
fn work_with_patch_chain() -> Result<(), Box<dyn std::error::Error>> {
// Create a patch chain
let mut chain = PatchChain::new();
// Add archives with priority (higher numbers override lower)
chain.add_archive(PathBuf::from("Data/common.MPQ"), 0)?; // Base content
chain.add_archive(PathBuf::from("Data/expansion.MPQ"), 100)?; // Expansion content
chain.add_archive(PathBuf::from("Data/patch.MPQ"), 200)?; // Patch 1
chain.add_archive(PathBuf::from("Data/patch-2.MPQ"), 300)?; // Patch 2
chain.add_archive(PathBuf::from("Data/patch-3.MPQ"), 400)?; // Patch 3 (highest priority)
// Extract a file - uses the highest priority version
let filename = "Interface/Icons/INV_Misc_QuestionMark.blp";
let data = chain.read_file(filename)?;
println!("Extracted {} ({} bytes)", filename, data.len());
// Find which archive contains a file
if let Some(archive_path) = chain.find_file_archive(filename) {
println!("File found in: {}", archive_path.display());
}
// List unique files across archives
let all_files = chain.list()?;
println!("Total unique files: {}", all_files.len());
// Get information about archives
let chain_info = chain.get_chain_info();
for info in &chain_info {
println!("{} (priority {}): {} files",
info.path.display(),
info.priority,
info.file_count
);
}
Ok(())
}
Manual Archive Management (Legacy Approach)
use wow_mpq::Archive;
fn work_with_multiple_archives_manual() -> Result<(), Box<dyn std::error::Error>> {
// Open archives individually
let mut base = Archive::open("Data/common.MPQ")?;
let mut patch = Archive::open("Data/patch.MPQ")?;
let mut patch2 = Archive::open("Data/patch-2.MPQ")?;
// Search for a file across archives (manual priority handling)
let filename = "Interface/Icons/INV_Misc_QuestionMark.blp";
// Try patch archives first (highest priority)
let data = if let Ok(data) = patch2.read_file(filename) {
println!("Found {} in patch-2.MPQ", filename);
data
} else if let Ok(data) = patch.read_file(filename) {
println!("Found {} in patch.MPQ", filename);
data
} else {
println!("Found {} in common.MPQ", filename);
base.read_file(filename)?
};
println!("Extracted {} ({} bytes)", filename, data.len());
Ok(())
}
5. Creating New Archives
use wow_mpq::{ArchiveBuilder, FormatVersion, ListfileOption};
fn create_simple_archive() -> Result<(), Box<dyn std::error::Error>> {
// Create archive
ArchiveBuilder::new()
.add_file("readme.txt", "README.txt")
.add_file_data(b"Hello World".to_vec(), "hello.txt")
.build("simple.mpq")?;
Ok(())
}
fn create_advanced_archive() -> Result<(), Box<dyn std::error::Error>> {
use wow_mpq::compression::flags;
ArchiveBuilder::new()
// Configure archive settings
.version(FormatVersion::V2)
.block_size(7) // 64KB sectors
.default_compression(flags::ZLIB)
.listfile_option(ListfileOption::Generate)
// Add files with different options
.add_file("data/texture.blp", "Textures/MyTexture.blp")
.add_file_data_with_options(
b"Important data".to_vec(),
"Data/config.ini",
flags::BZIP2, // Better compression
false, // no encryption
0, // default locale
)
.add_file_data_with_options(
b"Secret data".to_vec(),
"Keys/secret.key",
flags::ZLIB,
true, // encrypt
0, // locale
)
// Build the archive
.build("advanced.mpq")?;
Ok(())
}
5a. Modifying Existing Archives
The MutableArchive type allows you to modify existing MPQ archives:
use wow_mpq::{MutableArchive, AddFileOptions, compression::CompressionMethod};
fn modify_archive_example() -> Result<(), Box<dyn std::error::Error>> {
// Open an archive for modification
let mut archive = MutableArchive::open("my_archive.mpq")?;
// Add a file from disk with default options (zlib compression)
archive.add_file("new_file.txt", "data/new_file.txt", AddFileOptions::new())?;
// Add file data directly with custom compression
let options = AddFileOptions::new()
.compression(CompressionMethod::BZip2)
.encrypt()
.replace_existing(true);
archive.add_file_data(
b"Secret content".to_vec(),
"encrypted.dat",
options
)?;
// Remove a file
archive.remove_file("old_file.txt")?;
// Rename a file
archive.rename_file("readme.txt", "README.TXT")?;
// Read files from a mutable archive (convenience method)
let data = archive.read_file("some_file.txt")?;
println!("File content: {} bytes", data.len());
// List files (convenience method)
let files = archive.list()?;
for entry in files {
println!("{}: {} bytes", entry.name, entry.size);
}
// Verify signature (if present)
match archive.verify_signature() {
Ok(status) => println!("Signature status: {:?}", status),
Err(_) => println!("No signature found"),
}
// Flush changes to disk
archive.flush()?;
// Compact the archive to remove deleted files and reclaim space
// This creates a new archive without gaps from deleted files
archive.compact()?;
Ok(())
}
fn batch_modifications() -> Result<(), Box<dyn std::error::Error>> {
let mut archive = MutableArchive::open("patch.mpq")?;
// Add multiple files with different settings
let files_to_add = vec![
("assets/icon1.blp", "Interface/Icons/Icon1.blp", CompressionMethod::Zlib),
("assets/icon2.blp", "Interface/Icons/Icon2.blp", CompressionMethod::BZip2),
("assets/model.m2", "Models/Creature/Model.m2", CompressionMethod::None),
];
for (source, archived_name, compression) in files_to_add {
let options = AddFileOptions::new()
.compression(compression)
.replace_existing(true);
archive.add_file(source, archived_name, options)?;
}
// Remove old versions
let files_to_remove = vec![
"Interface/Icons/OldIcon.blp",
"Models/Deprecated/OldModel.m2",
];
for filename in files_to_remove {
if let Err(_) = archive.remove_file(filename) {
println!("File {} not found, skipping", filename);
}
}
// Batch rename for consistency
let renames = vec![
("interface/icons/spell_fire_01.blp", "Interface/Icons/Spell_Fire_01.blp"),
("interface/icons/spell_frost_01.blp", "Interface/Icons/Spell_Frost_01.blp"),
];
for (old_name, new_name) in renames {
if let Err(_) = archive.rename_file(old_name, new_name) {
println!("Could not rename {} to {}", old_name, new_name);
}
}
// Save all changes
archive.flush()?;
Ok(())
}
6. Rebuilding and Comparing Archives
The warcraft-rs CLI provides tools for rebuilding MPQ archives and
comparing them.
Rebuilding Archives
Archive rebuilding allows you to recreate MPQ archives 1:1 while optionally upgrading formats or changing compression:
use wow_mpq::{rebuild_archive, RebuildOptions, FormatVersion};
fn rebuild_archive_example() -> Result<(), Box<dyn std::error::Error>> {
// Basic rebuild with format preservation
let options = RebuildOptions {
preserve_format: true,
target_format: None,
preserve_order: true,
skip_encrypted: false,
skip_signatures: true,
verify: false,
override_compression: None,
override_block_size: None,
list_only: false,
};
rebuild_archive(
"original.mpq",
"rebuilt.mpq",
options,
None // No progress callback
)?;
println!("Archive rebuilt successfully");
Ok(())
}
fn rebuild_with_upgrade() -> Result<(), Box<dyn std::error::Error>> {
// Rebuild with format upgrade and verification
let options = RebuildOptions {
preserve_format: false,
target_format: Some(FormatVersion::V4),
preserve_order: true,
skip_encrypted: false,
skip_signatures: true,
verify: true,
override_compression: Some(wow_mpq::compression::flags::LZMA),
override_block_size: Some(6), // 32KB sectors
list_only: false,
};
let summary = rebuild_archive(
"old_v1.mpq",
"modern_v4.mpq",
options,
Some(&|current, total, file| {
if current % 100 == 0 {
println!("Processing [{}/{}]: {}", current, total, file);
}
})
)?;
println!("Rebuild completed:");
println!(" Source files: {}", summary.source_files);
println!(" Extracted files: {}", summary.extracted_files);
println!(" Skipped files: {}", summary.skipped_files);
println!(" Target format: {:?}", summary.target_format);
println!(" Verified: {}", summary.verified);
Ok(())
}
Comparing Archives
Archive comparison helps verify rebuilds and analyze differences between archives:
use wow_mpq::{compare_archives, CompareOptions};
fn compare_archives_example() -> Result<(), Box<dyn std::error::Error>> {
// Basic comparison
let result = compare_archives(
"original.mpq",
"rebuilt.mpq",
false, // not detailed
false, // no content check
false, // not metadata only
true, // ignore order
None // no filter
)?;
if result.identical {
println!("โ Archives are identical");
} else {
println!("โ Archives differ");
// Show metadata differences
if !result.metadata.matches {
println!("Metadata differences:");
if result.metadata.format_version.0 != result.metadata.format_version.1 {
println!(" Format: {:?} โ {:?}",
result.metadata.format_version.0,
result.metadata.format_version.1);
}
if result.metadata.file_count.0 != result.metadata.file_count.1 {
println!(" File count: {} โ {}",
result.metadata.file_count.0,
result.metadata.file_count.1);
}
}
// Show file differences
if let Some(files) = &result.files {
if !files.source_only.is_empty() {
println!("Files only in source ({}): {:?}",
files.source_only.len(),
&files.source_only[..files.source_only.len().min(5)]);
}
if !files.target_only.is_empty() {
println!("Files only in target ({}): {:?}",
files.target_only.len(),
&files.target_only[..files.target_only.len().min(5)]);
}
if !files.size_differences.is_empty() {
println!("Files with size differences: {}", files.size_differences.len());
}
}
}
Ok(())
}
fn compare_with_content_verification() -> Result<(), Box<dyn std::error::Error>> {
// Thorough comparison with content verification
let result = compare_archives(
"original.mpq",
"rebuilt.mpq",
true, // detailed
true, // content check
false, // not metadata only
true, // ignore order
Some("*.dbc".to_string()) // only compare DBC files
)?;
if let Some(files) = &result.files {
if !files.content_differences.is_empty() {
println!("โ Content differences found:");
for file in &files.content_differences {
println!(" - {}", file);
}
} else {
println!("โ All file contents match");
}
}
Ok(())
}
CLI Workflow Examples
# Complete rebuild and verification workflow
echo "=== Archive Rebuild and Verification ==="
# 1. Analyze original archive
warcraft-rs mpq info original.mpq
warcraft-rs mpq list original.mpq --long | head -10
# 2. Rebuild archive preserving format
warcraft-rs mpq rebuild original.mpq rebuilt.mpq
# 3. Compare archives
warcraft-rs mpq compare original.mpq rebuilt.mpq --output summary
# 4. Verify content integrity
warcraft-rs mpq compare original.mpq rebuilt.mpq --content-check
# 5. Upgrade to modern format
warcraft-rs mpq rebuild original.mpq modern.mpq --upgrade-to v4 --compression lzma
# 6. Compare format differences
warcraft-rs mpq compare original.mpq modern.mpq --metadata-only
7. Searching for Files
use wow_mpq::Archive;
use regex::Regex;
fn search_files(archive: &mut Archive, pattern: &str) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let re = Regex::new(pattern)?;
// Get listfile (required for file enumeration)
let entries = archive.list()?;
let matches: Vec<String> = entries
.iter()
.filter(|entry| re.is_match(&entry.name))
.map(|entry| entry.name.clone())
.collect();
println!("Found {} files matching '{}':", matches.len(), pattern);
for filename in &matches {
println!(" - {}", filename);
}
Ok(matches)
}
// Example: Find all BLP textures
fn find_textures(archive: &mut Archive) -> Result<Vec<String>, Box<dyn std::error::Error>> {
search_files(archive, r"\.blp$")
}
// Example: Find a specific file if you know part of the name
fn find_specific_file(archive: &mut Archive, partial_name: &str) -> Result<Option<String>, Box<dyn std::error::Error>> {
let entries = archive.list()?;
for entry in entries {
if entry.name.contains(partial_name) {
return Ok(Some(entry.name));
}
}
Ok(None)
}
Code Examples
Complete Example: MPQ Explorer
use wow_mpq::Archive;
use std::io::{self, Write};
struct MpqExplorer {
archive: Archive,
}
impl MpqExplorer {
fn new(path: &str) -> Result<Self, Box<dyn std::error::Error>> {
let archive = Archive::open(path)?;
Ok(Self { archive })
}
fn info(&self) -> Result<(), Box<dyn std::error::Error>> {
let info = self.archive.get_info()?;
println!("Archive Information:");
println!(" Path: {}", info.path.display());
println!(" Format Version: {:?}", info.format_version);
println!(" File Count: {}", info.file_count);
println!(" Archive Size: {:.2} MB", info.file_size as f64 / 1024.0 / 1024.0);
println!(" Sector Size: {} bytes", info.sector_size);
Ok(())
}
fn list(&mut self, filter: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
let entries = self.archive.list()?;
for entry in entries {
let filename = &entry.name;
if let Some(filter) = filter {
if !filename.contains(filter) {
continue;
}
}
println!("{} ({} bytes)", filename, entry.size);
}
Ok(())
}
fn extract(&mut self, filename: &str, output: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
let data = self.archive.read_file(filename)?;
let output_path = output.unwrap_or(filename);
use std::fs::File;
let mut file = File::create(output_path)?;
file.write_all(&data)?;
println!("Extracted {} to {} ({} bytes)", filename, output_path, data.len());
Ok(())
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut explorer = MpqExplorer::new("Data/common.MPQ")?;
loop {
print!("> ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let parts: Vec<&str> = input.trim().split_whitespace().collect();
if parts.is_empty() {
continue;
}
match parts[0] {
"info" => explorer.info()?,
"list" => explorer.list(parts.get(1).copied())?,
"extract" => {
if let Some(filename) = parts.get(1) {
explorer.extract(filename, parts.get(2).copied())?;
} else {
println!("Usage: extract <filename> [output]");
}
}
"quit" => break,
_ => println!("Unknown command. Available: info, list, extract, quit"),
}
}
Ok(())
}
Best Practices
1. Memory Management
// For large files, consider the file size before extraction
use wow_mpq::Archive;
fn extract_with_size_check(archive: &mut Archive, filename: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
// Get file list and check size
let entries = archive.list()?;
for entry in &entries {
if entry.name == filename {
if entry.size > 100 * 1024 * 1024 { // 100MB
println!("Warning: File {} is large ({} bytes)", filename, entry.size);
}
break;
}
}
// Extract the file
archive.read_file(filename)
}
2. Error Handling
use wow_mpq::{Archive, Error};
fn safe_extract(archive: &mut Archive, filename: &str) -> Result<Vec<u8>, String> {
match archive.read_file(filename) {
Ok(data) => Ok(data),
Err(Error::FileNotFound(_)) => {
Err(format!("File '{}' not found in archive", filename))
}
Err(Error::InvalidFormat(msg)) => {
Err(format!("File '{}' has invalid format: {}", filename, msg))
}
Err(Error::Io(e)) => {
Err(format!("I/O error reading '{}': {}", filename, e))
}
Err(e) => Err(format!("Error reading '{}': {}", filename, e)),
}
}
3. Caching Extracted Files
use std::collections::HashMap;
use wow_mpq::Archive;
struct CachedArchive {
archive: Archive,
cache: HashMap<String, Vec<u8>>,
}
impl CachedArchive {
fn new(path: &str) -> Result<Self, Box<dyn std::error::Error>> {
Ok(Self {
archive: Archive::open(path)?,
cache: HashMap::new(),
})
}
fn get(&mut self, filename: &str) -> Result<&[u8], Box<dyn std::error::Error>> {
if !self.cache.contains_key(filename) {
let data = self.archive.read_file(filename)?;
self.cache.insert(filename.to_string(), data);
}
Ok(&self.cache[filename])
}
}
Common Issues and Solutions
Issue: File Not Found
Problem: archive.read_file() returns FileNotFound error.
Solutions:
- Check the exact filename (case-sensitive)
- Check if archive has a listfile (use
archive.list()) - Check if using correct patch archive
- For archives without listfiles, you need to know exact filenames
// Debug file lookup
fn debug_file_lookup(archive: &mut Archive, partial_name: &str) -> Result<(), Box<dyn std::error::Error>> {
let entries = archive.list()?;
println!("Files containing '{}':", partial_name);
for entry in entries {
if entry.name.contains(partial_name) {
println!(" - {}", entry.name);
}
}
Ok(())
}
// Check if file exists before extraction
fn safe_file_check(archive: &mut Archive, filename: &str) -> Result<bool, Box<dyn std::error::Error>> {
let entries = archive.list()?;
for entry in &entries {
if entry.name == filename {
return Ok(true);
}
}
Ok(false)
}
Issue: Missing Listfile
Problem: Cannot enumerate files without listfile.
Solutions:
- Add a listfile to the archive manually
- Extract files by their exact known names
- Some archives don't have listfiles - this is normal
fn handle_missing_listfile(archive: &mut Archive) -> Result<(), Box<dyn std::error::Error>> {
match archive.list() {
Ok(entries) => {
println!("Found {} files with listfile:", entries.len());
for entry in entries.iter().take(10) {
println!(" - {}", entry.name);
}
}
Err(_) => {
println!("No listfile found in archive");
println!("You can:");
println!("1. Add a listfile to the archive");
println!("2. Extract specific files by exact name");
println!("3. Use external listfile references");
}
}
Ok(())
}
Issue: Archive Integrity
Problem: Want to verify archive is not corrupted.
Solutions:
- Check archive information
- Try to read a few files to test basic functionality
fn basic_archive_test(archive: &mut Archive) -> Result<(), Box<dyn std::error::Error>> {
let info = archive.get_info()?;
println!("Archive format: {:?}", info.format_version);
println!("File count: {}", info.file_count);
// Try to list files as a basic integrity test
match archive.list() {
Ok(entries) => println!("Listfile found with {} entries", entries.len()),
Err(_) => println!("No listfile found - cannot enumerate files without exact names"),
}
Ok(())
}
Issue: Blizzard Archive Warnings
Problem: Getting "-28 byte attributes file size mismatch" warnings with official WoW archives.
Solution: This is normal and expected behavior. All Blizzard MPQ archives have exactly 28 extra zero bytes at the end of their attributes files. The warning is informational only - the archives work perfectly.
// The warning looks like:
// "Attributes file size mismatch: actual=X, expected=Y, difference=-28 (tolerating for compatibility)"
// This is handled automatically by wow-mpq and doesn't affect functionality
let archive = Archive::open("Data/patch.mpq")?; // Works despite warning
Patch Chain Management
Understanding Patch Chains
World of Warcraft uses a patch chain system where newer patches override files
in older archives. The PatchChain struct automates this process, returning
the highest priority version of a file.
Critical Loading Order Rules
Based on TrinityCore's implementation and the official WoW client behavior, archives must be loaded in a specific order:
- Base Archives First: Common game data (common.MPQ, common-2.MPQ)
- Expansion Archives: Each expansion adds its archives (expansion.MPQ, lichking.MPQ)
- Locale Archives: Language-specific content that overrides base content
- General Patches: Numbered patches (patch.MPQ, patch-2.MPQ, patch-3.MPQ)
- Locale Patches: Language-specific patches (patch-enUS.MPQ, patch-enUS-2.MPQ)
Important principles:
- Files in later-loaded archives override files with the same path in earlier archives
- Locale-specific files always override their generic counterparts
- Patches are loaded in numerical order (patch-2 overrides patch)
- Custom patches should use higher numbers (patch-4.MPQ+) or letters (patch-x.MPQ)
Advanced PatchChain Usage
use wow_mpq::{PatchChain, ChainInfo};
use std::path::PathBuf;
fn advanced_patch_chain_example() -> Result<(), Box<dyn std::error::Error>> {
let mut chain = PatchChain::new();
// Add archives with descriptive priorities
const BASE_PRIORITY: i32 = 0;
const EXPANSION_PRIORITY: i32 = 1000;
const PATCH_PRIORITY_BASE: i32 = 2000;
chain.add_archive(PathBuf::from("Data/common.MPQ"), BASE_PRIORITY)?;
chain.add_archive(PathBuf::from("Data/expansion.MPQ"), EXPANSION_PRIORITY)?;
// Add patches in order
for (i, patch_file) in vec!["patch.MPQ", "patch-2.MPQ", "patch-3.MPQ"].iter().enumerate() {
let path = PathBuf::from(format!("Data/{}", patch_file));
let priority = PATCH_PRIORITY_BASE + (i as i32 * 100);
chain.add_archive(path, priority)?;
}
// Extract multiple files efficiently
let files_to_extract = vec![
"Interface/Icons/INV_Misc_QuestionMark.blp",
"DBFilesClient/Item.dbc",
"DBFilesClient/Spell.dbc",
];
for filename in &files_to_extract {
match chain.read_file(filename) {
Ok(data) => println!("Extracted {}: {} bytes", filename, data.len()),
Err(e) => eprintln!("Failed to extract {}: {}", filename, e),
}
}
// Get chain information
let chain_info = chain.get_chain_info();
for info in &chain_info {
println!("Archive: {} (priority: {})", info.path.display(), info.priority);
}
Ok(())
}
Patch Chain for Different WoW Versions
โ ๏ธ Important: The loading order below matches the exact order used by the WoW client, as documented by TrinityCore. Archives must be loaded in this specific order for correct file resolution.
use wow_mpq::PatchChain;
use std::path::Path;
/// Setup patch chain for WoW 3.3.5a following TrinityCore's definitive loading order
fn setup_wotlk_3_3_5a(data_path: &Path, locale: &str) -> Result<PatchChain, Box<dyn std::error::Error>> {
let mut chain = PatchChain::new();
// The exact loading order from TrinityCore:
// 1-4: Base and expansion archives
chain.add_archive(data_path.join("common.MPQ").to_path_buf(), 0)?;
chain.add_archive(data_path.join("common-2.MPQ").to_path_buf(), 1)?;
chain.add_archive(data_path.join("expansion.MPQ").to_path_buf(), 2)?;
chain.add_archive(data_path.join("lichking.MPQ").to_path_buf(), 3)?;
// 5-10: Locale and speech archives
chain.add_archive(data_path.join(format!("locale-{}.MPQ", locale)).to_path_buf(), 4)?;
chain.add_archive(data_path.join(format!("speech-{}.MPQ", locale)).to_path_buf(), 5)?;
chain.add_archive(data_path.join(format!("expansion-locale-{}.MPQ", locale)).to_path_buf(), 6)?;
chain.add_archive(data_path.join(format!("lichking-locale-{}.MPQ", locale)).to_path_buf(), 7)?;
chain.add_archive(data_path.join(format!("expansion-speech-{}.MPQ", locale)).to_path_buf(), 8)?;
chain.add_archive(data_path.join(format!("lichking-speech-{}.MPQ", locale)).to_path_buf(), 9)?;
// 11-13: General patches
chain.add_archive(data_path.join("patch.MPQ").to_path_buf(), 10)?;
chain.add_archive(data_path.join("patch-2.MPQ").to_path_buf(), 11)?;
chain.add_archive(data_path.join("patch-3.MPQ").to_path_buf(), 12)?;
// 14-16: Locale patches (in locale subdirectory)
let locale_path = data_path.join(locale);
chain.add_archive(locale_path.join(format!("patch-{}.MPQ", locale)).to_path_buf(), 13)?;
chain.add_archive(locale_path.join(format!("patch-{}-2.MPQ", locale)).to_path_buf(), 14)?;
chain.add_archive(locale_path.join(format!("patch-{}-3.MPQ", locale)).to_path_buf(), 15)?;
Ok(chain)
}
/// Setup patch chain for different WoW versions
fn setup_wow_patch_chain(wow_path: &Path, version: &str, locale: &str) -> Result<PatchChain, Box<dyn std::error::Error>> {
let mut chain = PatchChain::new();
let data_path = wow_path.join("Data");
match version {
"1.12.1" => {
// Vanilla WoW uses categorized archives
let base_priority = 0;
chain.add_archive(data_path.join("dbc.MPQ"), base_priority)?;
chain.add_archive(data_path.join("fonts.MPQ"), base_priority)?;
chain.add_archive(data_path.join("interface.MPQ"), base_priority)?;
chain.add_archive(data_path.join("misc.MPQ"), base_priority)?;
chain.add_archive(data_path.join("model.MPQ"), base_priority)?;
chain.add_archive(data_path.join("sound.MPQ"), base_priority)?;
chain.add_archive(data_path.join("speech.MPQ"), base_priority)?;
chain.add_archive(data_path.join("terrain.MPQ"), base_priority)?;
chain.add_archive(data_path.join("texture.MPQ"), base_priority)?;
chain.add_archive(data_path.join("wmo.MPQ"), base_priority)?;
// Patches override everything
chain.add_archive(data_path.join("patch.MPQ"), 1000)?;
chain.add_archive(data_path.join("patch-2.MPQ"), 1001)?;
}
"2.4.3" => {
// TBC introduced common.MPQ structure
// Base archives
chain.add_archive(data_path.join("common.MPQ"), 0)?;
chain.add_archive(data_path.join("common-2.MPQ"), 1)?;
chain.add_archive(data_path.join("expansion.MPQ"), 2)?;
// Locale archives (override base)
let locale_path = data_path.join(locale);
chain.add_archive(locale_path.join(format!("locale-{}.MPQ", locale)), 100)?;
chain.add_archive(locale_path.join(format!("speech-{}.MPQ", locale)), 101)?;
chain.add_archive(locale_path.join(format!("expansion-locale-{}.MPQ", locale)), 102)?;
chain.add_archive(locale_path.join(format!("expansion-speech-{}.MPQ", locale)), 103)?;
// General patches
chain.add_archive(data_path.join("patch.MPQ"), 1000)?;
chain.add_archive(data_path.join("patch-2.MPQ"), 1001)?;
// Locale patches (highest priority)
chain.add_archive(locale_path.join(format!("patch-{}.MPQ", locale)), 2000)?;
chain.add_archive(locale_path.join(format!("patch-{}-2.MPQ", locale)), 2001)?;
}
"3.3.5a" => {
// Use the definitive loading order function
return setup_wotlk_3_3_5a(&data_path, locale);
}
_ => {
return Err(format!("Unsupported WoW version: {}", version).into());
}
}
Ok(chain)
}
Searching Across Patch Chains
use wow_mpq::PatchChain;
use regex::Regex;
fn search_patch_chain(chain: &mut PatchChain, pattern: &str) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let re = Regex::new(pattern)?;
let all_files = chain.list()?;
let matches: Vec<String> = all_files
.into_iter()
.filter(|entry| re.is_match(&entry.name))
.map(|entry| entry.name)
.collect();
Ok(matches)
}
// Example: Find all spell icons
fn find_spell_icons(chain: &mut PatchChain) -> Result<Vec<String>, Box<dyn std::error::Error>> {
search_patch_chain(chain, r"Interface/Icons/Spell_.*\.blp")
}
Performance Tips
1. Batch Operations
// Extract multiple files efficiently
fn batch_extract(archive: &Archive, filenames: &[&str]) -> Result<Vec<(String, Vec<u8>)>, Box<dyn std::error::Error>> {
let mut results = Vec::with_capacity(filenames.len());
for &filename in filenames {
match archive.read_file(filename) {
Ok(data) => results.push((filename.to_string(), data)),
Err(e) => eprintln!("Failed to extract {}: {}", filename, e),
}
}
Ok(results)
}
2. Efficient File Listing
use wow_mpq::Archive;
fn efficient_file_search(archive: &mut Archive, pattern: &str) -> Result<Vec<String>, Box<dyn std::error::Error>> {
// Get file list once and reuse it
let entries = archive.list()?;
let matches: Vec<String> = entries
.into_iter()
.filter(|entry| entry.name.contains(pattern))
.map(|entry| entry.name)
.collect();
Ok(matches)
}
3. Reuse Archive Objects
use wow_mpq::Archive;
struct ArchivePool {
archives: Vec<Archive>,
}
impl ArchivePool {
fn new(paths: &[&str]) -> Result<Self, Box<dyn std::error::Error>> {
let archives = paths.iter()
.map(|path| Archive::open(path))
.collect::<Result<Vec<_>, _>>()?;
Ok(Self { archives })
}
fn find_and_extract(&mut self, filename: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
for archive in &mut self.archives {
if let Ok(data) = archive.read_file(filename) {
return Ok(data);
}
}
Err(format!("File '{}' not found in any archive", filename).into())
}
}
4. Check File Existence Before Extraction
use wow_mpq::Archive;
fn smart_extract(archive: &mut Archive, filename: &str) -> Result<Option<Vec<u8>>, Box<dyn std::error::Error>> {
// Check if file exists first (cheaper than attempting extraction)
if let Some(file_info) = archive.find_file(filename)? {
Some(file_info) => {
println!("File {} exists ({} bytes), extracting...", filename, file_info.file_size);
Ok(Some(archive.read_file(filename)?))
}
None => {
println!("File {} not found", filename);
Ok(None)
}
}
}
Related Guides
- ๐ DBC Data Extraction - Extract and parse DBC files from MPQ archives
- ๐ผ๏ธ Texture Loading Guide - Load BLP textures from MPQ archives
- ๐ญ Loading M2 Models - Extract and load M2 model files
- ๐๏ธ WMO Rendering Guide - Extract and render WMO files
- ๐ฆ WoW Patch Chain Summary - Guide to patch chaining across all WoW versions
References
- MPQ Format Documentation - Detailed MPQ format specification
- StormLib - Reference C++ implementation
- Blizzard Archive Formats - Technical details
- WoW File Formats - Complete WoW file format documentation