📦 Working with MPQ Archives
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
#![allow(unused)]
fn main() {
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
#![allow(unused)]
fn main() {
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
#![allow(unused)]
fn main() {
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.
#![allow(unused)]
fn main() {
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)
#![allow(unused)]
fn main() {
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
#![allow(unused)]
fn main() {
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:
#![allow(unused)]
fn main() {
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:
#![allow(unused)]
fn main() {
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:
#![allow(unused)]
fn main() {
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
#![allow(unused)]
fn main() {
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
#![allow(unused)]
fn main() {
// 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
#![allow(unused)]
fn main() {
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
#![allow(unused)]
fn main() {
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
#![allow(unused)]
fn main() {
// 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
#![allow(unused)]
fn main() {
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
#![allow(unused)]
fn main() {
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.
#![allow(unused)]
fn main() {
// 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
#![allow(unused)]
fn main() {
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.
#![allow(unused)]
fn main() {
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
#![allow(unused)]
fn main() {
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
#![allow(unused)]
fn main() {
// 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
#![allow(unused)]
fn main() {
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
#![allow(unused)]
fn main() {
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
#![allow(unused)]
fn main() {
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