带源码的MPQ查看器:从格式解析到提取实战
2026/9/2 11:14:39 网站建设 项目流程

简介:面向游戏开发者和C++编程学习者,这份压缩包提供暴风雪公司MPQ文件查看器及完整源码。MPQ是《魔兽争霸》《星际争霸》等暴雪游戏常用的压缩格式,普通文件管理器无法直接查看,该工具能解析并展示MPQ包内数据,开发者可借此理解文件读取、解压、二进制解析和内存管理等实现。压缩包共62个文件,约1.29MB,含14个头文件、7个C++源文件、3个静态库,以及chm帮助文档、html页面、bmp/jpg素材和dsp/dsw工程文件,结构完整。程序涉及mdx、blp等魔兽3模型与贴图资源,也给出三维数据读取和展示的参考。目前已有254人学习下载,适合研究暴雪游戏资源格式、学习C++项目实践或扩展自定义工具的人群。 我大概在2017年左右开始折腾《魔兽争霸3》的地图汉化,那时候最头疼的就是怎么把地图里那一堆模型、贴图和文本文件从 .mpq 后缀的大文件里拆出来。网上下到的MPQ文件查看器要么不开源、要么在64位系统上各种报错,逼得我最后只能去找一套带源码的方案自己改。也就是那段时间,我养成了一个习惯:拿到这种工具第一件事不是直接双击运行,而是先把源码里最核心的解析逻辑读一遍。“暴风雪公司MPQ文件查看器(带源码).zip”这个包,正好就是这种可以用来学习、也可以拿来改成自己顺手的工具的起点。这篇文章我会从MPQ格式本身讲起,逐步拆解一个查看器程序的核心设计,中间穿插一些我实际踩过的坑,希望对想搞懂这类打包格式的朋友有帮助。

1. 为什么一个十年前的格式还值得动手写查看器

1.1 暴雪游戏资产与MPQ的王牌价值

暴雪娱乐从《暗黑破坏神》时代开始用MPQ(Mo’PaQ)作为游戏资源的容器格式,之后的《星际争霸》《魔兽争霸3》《魔兽世界》都在用。它的核心思路其实很朴素:游戏运行时要读成千上万个文件,如果全部散落在磁盘上,碎片化严重、读写效率低,打包成单个大文件之后,操作系统加载的压力会小很多。

MPQ的设计在当时相当超前,它支持高强度加密、文件压缩、按hash快速查找,甚至在《魔兽世界》里还支持“后续补丁直接覆盖旧文件而不需要重新打包整个文件”的能力。这也是为什么暴雪游戏这么多年下来,MOD作者、地图汉化组、模型提取爱好者都绕不开它。一个游戏资源打包格式能活二十多年,说明它的底层设计是经得起推敲的。

如果你现在手里握着一个带源码的MPQ查看器,你其实拥有的不是一个小工具,而是一套理解二进制容器格式的完整范本。懂了MPQ再看其他游戏的资源打包格式,比如Unity的Bundle、虚幻的PAK,思路会顺畅很多。

1.2 带源码的意义:不是工具,是教材

网上能下载到的MPQ工具不少,比如StormLib、MPQEditor,但真正“带源码”且结构清晰的并不多。有源码的价值在于几件事:

  • 你可以改功能,比如把“只能查看”改成“支持批量导出”,甚至加一个命令行接口。
  • 你可以移植到其他平台,比如用C/C++写的解析部分直接编译到Android或Linux。
  • 你可以在读源码的过程中搞懂每一个字节的含义,这种理解是看文档替代不了的。

我个人的建议是,拿到这类源码后不要急着编译,先打开核心文件,找到三个结构体定义:MPQ头、哈希表项、块表项。只要这三个结构体看明白了,MPQ格式你就已经懂了一半。

2. MPQ格式的四个核心数据结构

2.1 文件头:从0x1A51504D开始

任何MPQ文件的前4个字节一定是0x1A 0x51 0x50 0x4D,从内容上看反着读就是“MPQ\x1A”。这个魔数检查通过后,才能继续读后面的字段。

一个标准的MPQ文件头在《魔兽争霸3》那一代通常是32字节:

struct MPQHeader { uint32_t magic; // 0x1A51504D uint32_t headerSize; // 通常为 32,扩容版本可能是 48 uint32_t archiveSize; // 整个MPQ包的实际大小 uint16_t formatVersion; // 0 表示经典格式,1 表示含扩展头 uint16_t blockSize; // 扇区大小偏移量(以512字节为单位) uint32_t hashTableOffset; // 哈希表在文件中的偏移 uint32_t blockTableOffset; // 块表在文件中的偏移 uint32_t hashTableEntries; // 哈希表的条目数量 uint32_t blockTableEntries;// 块表的条目数量 };

这里的blockSize不是直接的字节数,而是表示为512 << blockSize。比如值为3时,真正扇区大小是4096字节。压缩时数据会被切成这么多大小的块,分别压缩。我第一版代码就忽略了这个移位,导致大文件解压出来全是乱码,检查了半天才想起来去看官方定义。

2.2 哈希表:用查找代替遍历

哈希表是整个MPQ格式的精髓。暴雪设计它的原因很直接:如果文件名列表很大,每次查找都读一遍文件目录,性能开销不可接受。所以MPQ使用了一个固定大小的哈希表,通过三个哈希值来定位文件。

哈希表的每一项是16字节:

struct HashTableEntry { uint32_t name1; // 文件名的第一个哈希值 uint32_t name2; // 文件名的第二个哈希值 uint32_t locale; // 语言区域(zhCN、enUS等) uint32_t platform; // 平台标记 uint32_t fileBlockIndex; // 对应块表的索引 };

查找过程的逻辑大致是这样的:

  1. 对文件名做一次哈希,得到哈希索引。
  2. 跳转到哈希表的对应位置,检查name1name2是否匹配。
  3. 如果匹配,再检查fileBlockIndex,如果值等于0xFFFFFFFE,说明该文件已被删除;如果是正常索引,就跳转到块表对应项。
  4. 如果不匹配,线性探测下一项,直到遇到空项(所有字段为0xFFFFFFFF)才算彻底未找到。

为什么哈希索引计算要用hash0 & (entries - 1)而不是取模?因为MPQ头的哈希表大小固定为2的幂,位运算比取模快得多。这个细节看似微小,但对大文件频繁查找来说,性能差异很明显。

2.3 块表:真正记录文件位置的数据结构

哈希表解决的是“文件名到条目”的映射,块表则记录“文件数据如何在MPQ包中存放”。每一项也是16字节:

struct BlockTableEntry { uint32_t filePosition; // 文件数据在MPQ中的绝对偏移 uint32_t compressedSize; // 压缩后大小 uint32_t uncompressedSize; // 解压后大小 uint32_t flags; // 文件属性标志 };

flags是理解MPQ的关键,几个常用位如下:

标志位含义
MPQ_FILE_IMPLODE0x00000100使用PKWARE DCL压缩
MPQ_FILE_COMPRESS0x00000200使用多算法压缩(ZLib/BZip2等)
MPQ_FILE_ENCRYPTED0x00010000文件内容已加密
MPQ_FILE_FIX_KEY0x00020000解密密钥依赖文件偏移
MPQ_FILE_SINGLE_UNIT0x01000000整个文件作为一个压缩单元
MPQ_FILE_DELETE_MARKER0x02000000文件已被删除
MPQ_FILE_EXISTS0x80000000文件真实存在

读取文件时,先根据filePosition把压缩数据读到内存,然后判断压缩方式。如果flagsMPQ_FILE_COMPRESS,数据块的第一个字节是压缩算法组合标识,后面才是真正的压缩流。如果带MPQ_FILE_ENCRYPTED,必须在解压前先解密。

2.4 加密表与密钥:暴雪的反逆向思路

MPQ的加密算法虽然不算特别强,但思路很有意思。它维护一个0x500个DWORD的表,叫做加密表或者Storm表,所有文件名哈希和文件内容加解密都依赖这张表。

加密表的初始化是一个固定的伪随机过程,典型代码如下:

void InitCryptoTable(uint32_t cryptoTable[0x500]) { uint32_t seed = 0x00100001; for (int index1 = 0; index1 < 0x100; index1++) { for (int index2 = index1, index3 = 0; index3 < 5; index3++) { seed = (seed * 125 + 3) % 0x2AAAAB; uint32_t temp1 = (seed & 0xFFFF) << 16; seed = (seed * 125 + 3) % 0x2AAB; uint32_t temp2 = seed & 0xFFFF; cryptoTable[index2] = temp1 | temp2; index2 += 0x100; } } }

文件内容解密的时候,密钥由文件名哈希和可能的偏移量共同决定。解密过程会不断更新两个种子值,让每个8字节或者4字节的解密结果都和前面的结果关联,破解起来比较费劲。

对查看器开发来说,你不需要重新发明这套算法,但一定要保证加密表初始化正确。如果加密表生成错误,最明显的症状就是文件名全是乱码,或者listfile文件解出来是一堆无意义数据。

3. 查看器程序的设计与实现

3.1 技术选型:解析纯二进制,C/C++依然最直接

写MPQ查看器,编程语言的自由度其实很高。C#、Java、Python都能写,但如果你追求性能和二进制操作的直觉,C/C++是最合适的。

C++里可以直接用fstreammmap读取整个文件,然后通过结构体指针直接映射到内存,效率非常高。用Python的话虽然开发速度快,但逐字节操作和大量解压调用会明显慢,尤其是处理像《魔兽世界》动辄几十GB的MPQ文件时,体验差距很大。

我看到过不少带源码的MPQ查看器都是基于C++写的,配合Qt或者Win32做界面。如果你只是学习原理,可以不关心界面,做一个命令行工具,专注实现三个核心接口:

class MPQArchive { public: bool Open(const char* path); bool GetFileList(std::vector<std::string>& outList); bool ExtractFile(const std::string& name, const char* outputPath); };

3.2 解析器类的最小骨架

无论界面长什么样,解析器的骨架基本是一致的:

bool MPQArchive::Open(const char* path) { std::ifstream file(path, std::ios::binary); file.read(reinterpret_cast<char*>(&header_), sizeof(header_)); // 检查魔数 if (header_.magic != 0x1A51504D) { return false; } // 读取哈希表 hashTable_.resize(header_.hashTableEntries); file.seekg(header_.hashTableOffset); file.read(reinterpret_cast<char*>(hashTable_.data()), header_.hashTableEntries * sizeof(HashTableEntry)); // 读取块表 blockTable_.resize(header_.blockTableEntries); file.seekg(header_.blockTableOffset); file.read(reinterpret_cast<char*>(blockTable_.data()), header_.blockTableEntries * sizeof(BlockTableEntry)); return true; }

打开文件这一步其实没什么难度,真正的坑在后面的查找和解压。

3.3 文件列表的获取策略

MPQ里的文件名默认不会直接暴露出去,而是存储在一个名为(listfile)的内部文件里,这个文件本身可能被加密。查看器通常有两种方式获得文件名列表:

  • 优先尝试从(listfile)文件读取。读取流程和普通文件一样,先哈希查找,再读取内容,如果加密则解密,最后按文本行解析。
  • 如果(listfile)不存在或者为空,就只能暴力扫描哈希表。把哈希表里所有有效项遍历一遍,但只能得到哈希值和块表索引,拿不到明文文件名,这时候需要配合外部词典或者人工猜测。

我在做地图汉化时经常遇到的情况是:某些第三方地图的作者故意删掉了(listfile),这时候查看器列表里只有一片“文件1”“文件2”这样的占位名。处理办法是把地图文件和原版游戏目录的同名文件比对,通过文件大小和压缩方式推断内容。

3.4 解压模块需要处理哪些算法

MPQ历史上用过的压缩算法不少,查看器不可能只依赖一种。常见的有:

压缩算法常见标识实现建议
ZLib0x02各语言都有现成库,直接用
BZip20x08也建议直接用库
Huffman0x01暴雪自定义的Huffman变体,需要自己实现
PKWARE DCL0x04老版本暗黑/星际常用,库较少
LZMA0x10《魔兽世界》用得多,可用liblzma

Warcraft III版本的地图通常用的是ZLib和BZip2,配合Huffman。写查看器的时候,我建议先支持ZLib,因为这个覆盖了大多数场景,然后再按需求补充其他算法。

解压时的关键点是:不要对整份数据直接解压,而是先看压缩块的第一个字节,根据压缩标识决定解压策略。比如标识是0x02,直接对剩余数据做ZLib inflate;如果标识是0x03,说明先用Huffman解压,再做ZLib解压,顺序不能反。

4. 实机演示:从War3地图中提取模型与贴图

4.1 准备一个真实存档

理论说再多,不如实际跑一遍。我以一份普通的魔兽争霸3自定义地图为例,文件名叫test.w3x。实际上.w3x本身就是一个MPQ格式的容器,只是后缀名不同,所以直接用查看器打开即可。

打开后先看一下文件列表,你会看到类似这样的内容:

war3map.j war3map.wtg war3map.wts war3map.doo war3map.shd war3mapMap.blp

这些是地图的逻辑脚本、触发数据、文本字符串和地图预览图。对于汉化工作来说,最需要提取的是war3map.wts,里面保存了所有界面文本。提取出来之后用文本编辑器打开,可以直接修改单位名称、技能说明,然后找工具回写到地图里。

4.2 提取指定文件并验证

提取文件的流程分几步走:

  1. HashString计算目标文件名在哈希表里的索引。
  2. 找到匹配的哈希表项,拿到fileBlockIndex
  3. 根据fileBlockIndex查块表,拿到偏移、压缩大小和标记位。
  4. 如果文件加密,用文件名加上块偏移计算密钥,先解密。
  5. 根据压缩标记决定是否解压,写成输出文件。

war3mapMap.blp为例,这是一个贴图文件,提取出来后可以直接用图片查看器打开。如果打开报错,多半是解压或解密步骤出了问题,而不是提取本身的问题。判断方法是用十六进制编辑器看文件前几个字节,BLP贴图的魔数通常是BLP1BLP2,如果文件头不对,说明数据在处理过程中已经损坏。

4.3 实操过程中的小意外

我遇到过一个很典型的问题:列表里有文件名,文件大小也对,但提取出来永远是0字节。后来查了半天发现,那份地图的块表项flags里带有MPQ_FILE_DELETE_MARKER,文件在逻辑上已经被删除了,只是在哈希表里还残留着条目。查看器如果没判断这个标志位,就会试图提取一块压根不存在的数据。

所以说,写提取逻辑时,flags的判断一定要完整,尤其是MPQ_FILE_EXISTSMPQ_FILE_DELETE_MARKERMPQ_FILE_ENCRYPTED这三个,少一个都可能让你调试到怀疑人生。

5. 开发与调试中的五个关键避坑方向

5.1 用二进制编辑器先看再写代码

我强烈建议在动手写任何解析代码之前,先用HxD或者010 Editor打开一个真实的MPQ文件,对着文件头、哈希表、块表逐段查看。只有亲眼看到那些字节的排列,你才能真正理解为什么hashTableOffset是从文件头开始的偏移量,为什么哈希表项在内存里是连续排列的。

很多新手一上来就照着别人的源码抄,抄完发现读出来的数据不对,其实是因为没理解某些字段和偏移的对应关系。纸上得来终觉浅,二进制编辑器就是MPQ入门最好的老师。

5.2 加密表初始化必须逐字校验

加密表初始化这件事,看起来只是一个循环和几个魔数,但里面任何一个常数写错,都会导致后面所有哈希计算和解密全部错误。更难受的是,错误不是立刻显现的,而是隐藏在某些文件里,可能5分钟后才暴露。

我建议把加密表生成代码单独抽出来,写一个单元测试,把生成结果的前几个值和公开源码对比。很多带源码的MPQ项目里都有现成的参考实现,直接对照一目了然。

5.3 压缩标志位不能只判断一位

MPQ的flags是一个32位整数,多种属性可以同时存在:一个文件既可以是加密的,也可以是压缩的,还可以是分块存储的。有些人写代码时只判断某一个位,其他位直接忽略,这会导致在某种文件上偶发解压失败。

正确的做法是每处理一个文件都完整检查所有相关位,形成清晰的流程分支。用现代调试器做条件断点,也会省很多时间。

5.4 ArchiveSize与真实文件大小的差异

MPQ头里的archiveSize理论上表示整个MPQ包的大小,但实际文件可能因为补丁等原因末尾多出几百字节数据。我第一次用archiveSize作为文件读取总长度时,程序报错说“读取越界”,后来改成直接用std::ifstreamseekg到文件末尾获取真实大小,才彻底解决。

所以每次打开文件后,建议读取真实文件大小,并和archiveSize做一次对比。如果差异较大,至少打印一条警告日志,方便后续排查。

5.5 版本兼容:别忽略扩展头

《魔兽争霸3》和《魔兽世界》经典版本用的是32字节文件头,但后来出现了带扩展头的MPQ,headerSize可能大于32,甚至包含64位的大文件偏移。如果你的查看器只按32字节结构体解析,在高版本文件上很可能会定位到错误的位置。

一个比较稳妥的做法是:读头信息时不要直接用固定结构体覆盖,而是先读取前32字节,然后根据formatVersionheaderSize再决定是否继续读扩展部分。

最后再分享一个小技巧

调试MPQ解析最有效的办法不是打断点,而是做一个“对比模式”:用已知好用的工具(比如MPQEditor)导出某个文件的原始数据和属性,再让你的程序输出同样的信息,逐字段对比。差异出现在哪里,问题就在哪里。

这套带源码的MPQ文件查看器,我拿来做的最值的一件事,就是把它改造成了一个支持批量导出和命令行调用的工具,配合写地图MOD的流程,效率提升了不止一倍。对于想深入理解游戏资源格式的人来说,把MPQ吃透,等于拿到了一把通用钥匙,之后再碰其他容器格式都会觉得亲切很多。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询