暴雪MPQ文件查看器源码解析:从格式拆解到批量提取实战
简介暴风雪公司MPQ文件查看器是一款面向游戏开发与逆向分析人员的专用工具用于打开和解析暴雪旗下魔兽争霸、星际争霸、暗黑破坏神等游戏采用的MPQ压缩格式文件。普通文件浏览器无法直接读取MPQ内部结构该工具恰好填补这一空白并附带完整源码便于C开发者深入理解文件读取、数据解析与内存管理等底层实现。压缩包共62个文件约1.29MB以14个h头文件、7个cpp源文件为核心辅以lib静态库、ico图标、html帮助文档及jpg预览图等资源结构完整可直接编译研究。程序被标记为三维程序涉及mdx模型、blp纹理等游戏资源的解析与展示对三维数据处理有一定参考价值。目前已有267人学习下载适合具备C基础、希望研究MPQ格式或游戏资源加载机制的开发者通过阅读源码可掌握文件结构解析思路并在此基础上进行功能扩展或平台移植。1. 从一份 MPQ 查看器源码说起为什么老游戏资源解析至今仍是硬需求如果你手头拿到一份名为「暴风雪公司MPQ文件查看器(带源码).zip」的压缩包第一反应大概率是这东西能打开什么、源码能不能编译、里面的 MPQ 格式到底怎么解。MPQ 是暴雪系游戏沿用多年的归档格式从早期的即时战略到后来的大型角色扮演游戏大量贴图、模型、音频、地图脚本都封在.mpq文件里。它不是一个简单的 zip而是带哈希表、块表、压缩标志位和加密位的复合容器。很多做怀旧服工具、地图编辑器、资源提取器、MOD 加载器的开发者绕不开它。这份带源码的查看器价值不在于「能看」而在于「能改」。你可以把它当成一个学习归档格式解析的活体样本怎么读文件头、怎么定位哈希表、怎么处理压缩块、怎么在内存里还原一个文件。适合三类人想给老游戏做资源管线的工具作者、想研究二进制容器格式的安全或逆向爱好者、以及需要批量提取素材做二次创作的美术与关卡设计。下面我按「先立住格式认知再动手跑通解析最后讲坑和进阶」的顺序拆开讲。2. MPQ 格式拆解文件头、哈希表、块表三件套怎么读2.1 文件头里的 magic 与偏移量别把 MPQ 当成 zip 读MPQ 文件开头是 32 字节的文件头前 4 字节是 magicMPQ\x1A注意最后一个字节是0x1A而不是可打印字符很多新手用文本编辑器打开看到乱码就以为文件损坏。紧接着是头大小、归档大小、格式版本、块大小、哈希表偏移、块表偏移等字段。块大小通常是 512 的倍数常见 3 表示 4096 字节这个值决定了后续每个压缩块解压后的对齐粒度。读文件头时最容易翻车的是版本差异。早期版本头只有 32 字节后续版本扩展了 44 字节甚至更多包含HET和BET表偏移。如果你只按 32 字节硬读遇到扩展头就会把哈希表偏移读错后面全盘皆输。稳妥做法是先读前 32 字节判断头大小字段再决定是否继续读扩展部分。import struct def read_mpq_header(fp): fp.seek(0) magic fp.read(4) if magic ! bMPQ\x1a: raise ValueError(不是有效的 MPQ 文件) header_size, archive_size, format_version struct.unpack(III, fp.read(12)) block_size, hash_table_pos, block_table_pos struct.unpack(IIQ, fp.read(16)) # block_size 是 2 的幂次实际字节数 512 * 2^block_size real_block_size 512 * (1 block_size) return { header_size: header_size, archive_size: archive_size, format_version: format_version, block_size: real_block_size, hash_table_pos: hash_table_pos, block_table_pos: block_table_pos, }这段代码只读基础头III和IIQ是小端解析。block_size字段本身是幂次不是字节数必须转换。hash_table_pos和block_table_pos是相对文件开头的绝对偏移。参数上format_version为 0 时是经典格式为 1 时通常带扩展头需要额外读het_table_pos等字段。如果你拿到的查看器源码里直接seek(0)后按固定偏移读遇到版本 1 的文件就会错位这是第一个要检查的点。2.2 哈希表与块表文件名到数据的两次跳转MPQ 用哈希表做文件名索引用块表记录实际数据位置。哈希表每个条目 16 字节包含哈希 A、哈希 B、语言、平台、块索引。块索引指向块表条目块表每个条目也是 16 字节包含数据偏移、压缩后大小、解压后大小、标志位。文件名先经过哈希算法得到两个 32 位值再在哈希表里线性探测直到找到匹配或遇到空条目。哈希算法是 MPQ 的经典部分对文件名做三次不同种子的哈希分别用于哈希表索引、哈希 A 和哈希 B。很多查看器源码里会带一个hash_string函数你要确认它处理了大小写归一化——MPQ 文件名通常转成大写并用反斜杠分隔。如果哈希对不上表现就是「文件明明在但查不到」。def hash_string(name, hash_type): # hash_type: 0 用于哈希表索引, 1 用于哈希A, 2 用于哈希B seeds [0x00100001, 0x00100002, 0x00100003] seed seeds[hash_type] # 实际实现中还有 0x100 长度的种子表这里简化示意 name name.upper().replace(/, \\) h 0x7FED7FED for ch in name: c ord(ch) h (h ^ (c seed)) 0xFFFFFFFF h (h * 0x5F5F5F5F 0xEEEEEEEE) 0xFFFFFFFF return h上面是简化示意真实实现里种子表有 0x500 个预计算值不能省。参数说明name必须统一成大写、反斜杠分隔hash_type决定用哪组种子。哈希表大小通常是 2 的幂索引计算是hash_index (table_size - 1)。如果哈希表条目被占用但哈希不匹配就继续往后找直到遇到空条目。这个线性探测逻辑在源码里如果写成死循环遇到损坏文件就会卡死需要加最大探测次数。2.3 压缩标志位zlib、PKWare、bzip2 与多段压缩块表条目的标志位决定数据怎么解压。常见标志有0x00000200表示 zlib 压缩0x00000100表示 PKWare 压缩0x00000010表示 bzip20x00000008表示加密0x00010000表示多段压缩。多段压缩最容易被忽略数据被切成多个段每段前面有一个字节表示该段使用的压缩方法最后一段可能不压缩。如果你只按单一 zlib 解遇到多段压缩就会解出乱码。处理顺序一般是先判断是否加密如果加密需要先解密再判断是否多段压缩如果是则逐段解压否则按标志位选解压器。解压后的大小要和块表里的解压后大小校验不一致说明解压参数错了。import zlib def decompress_block(data, flags, expected_size): if flags 0x00010000: # 多段压缩 out bytearray() pos 0 while pos len(data): method data[pos] pos 1 if method 0x02: # 该段 zlib但需要知道段长度实际格式更复杂 chunk zlib.decompress(data[pos:]) out.extend(chunk) break else: out.extend(data[pos:]) break return bytes(out[:expected_size]) if flags 0x00000200: return zlib.decompress(data)[:expected_size] return data[:expected_size]这段代码只演示了多段压缩的简化分支真实多段压缩每段有独立的压缩后长度字段需要按格式逐段读。参数上expected_size来自块表用来截断多余数据。如果你发现解压后大小对不上先检查flags是否读错再检查是否漏了加密步骤。很多查看器源码在这里只处理 zlib遇到 PKWare 或 bzip2 就报错这是第二个要检查的点。3. 把查看器源码跑起来编译、加载与文件列表渲染3.1 源码结构速览先找入口和格式解析模块拿到「暴风雪公司MPQ文件查看器(带源码).zip」后先解压看目录。常见结构是一个主程序入口、一个mpq解析模块、一个 UI 层、可能还有资源图标和配置文件。不要急着编译先找解析模块里的read_header、hash_string、decompress_block这几个函数确认它们和上一章讲的逻辑对得上。如果源码里把哈希表大小写死了比如固定 1024那遇到大归档就会索引越界。我一般会先看入口文件里怎么初始化归档对象再跟到文件列表怎么填充。很多查看器在打开文件时一次性读整个哈希表和块表到内存这对小文件没问题但遇到几百 MB 的归档会吃内存。如果你要改成流式读取需要把哈希表和块表缓存起来数据块按需读。3.2 编译环境与依赖Windows 和 Linux 下的最小命令这类查看器源码常见是 C 加 Qt 或者 C# WinForms也有 Python 加 Tkinter 的版本。如果是 C Qt你需要装 Qt 开发库和 CMake如果是 C#需要 .NET SDK如果是 Python直接pip install依赖即可。下面给一个通用 CMake 项目的编译命令具体以源码里的CMakeLists.txt为准。# 假设源码在 mpq_viewer 目录构建目录为 build cd mpq_viewer mkdir -p build cd build cmake .. -DCMAKE_BUILD_TYPERelease cmake --build . --config Release -j 4 # 如果依赖 Qt可能需要指定 Qt 路径 # cmake .. -DCMAKE_PREFIX_PATH/path/to/Qt/6.x/gcc_64参数说明-DCMAKE_BUILD_TYPERelease开优化解析大文件时速度差别明显-j 4按 CPU 核数调整。如果编译报找不到zlib.h装zlib1g-dev或对应平台的开发包。如果报找不到bzip2同样装开发包。Windows 下用 Visual Studio 打开.sln或直接用 CMake 生成工程。注意不要用 Debug 模式跑大归档解压速度会慢到怀疑人生。3.3 加载第一个 MPQ 文件从文件列表到导出单个资源编译成功后运行程序打开一个 MPQ 文件。如果程序能列出文件列表说明哈希表和块表读对了。接下来选一个小的文本或图片文件导出验证解压链路。导出逻辑一般是根据文件名查哈希表得到块索引读块表得到偏移和标志位读数据块解压写文件。def extract_file(fp, header, hash_table, block_table, filename): # 查哈希表 index hash_string(filename, 0) (len(hash_table) - 1) for i in range(len(hash_table)): entry hash_table[(index i) % len(hash_table)] if entry[block_index] 0xFFFFFFFF: raise FileNotFoundError(filename) if entry[hash_a] hash_string(filename, 1) and entry[hash_b] hash_string(filename, 2): block block_table[entry[block_index]] fp.seek(block[offset]) data fp.read(block[compressed_size]) return decompress_block(data, block[flags], block[uncompressed_size]) raise FileNotFoundError(filename)参数说明block_index为0xFFFFFFFF表示空条目遇到就说明文件不存在hash_a和hash_b必须同时匹配。导出时注意compressed_size和uncompressed_size的区别读文件用前者校验用后者。如果导出的图片打不开先看文件头是不是被多段压缩搞乱了再检查是否漏了加密解密。4. 避坑与排查MPQ 解析里最容易翻车的五件事4.1 现象文件列表为空但文件明明有几百 MB原因通常是哈希表偏移读错。扩展头版本的文件哈希表偏移在扩展字段里如果只读基础头就会指到错误位置。解决先判断header_size是否大于 32如果大于继续读扩展字段用扩展里的偏移覆盖基础偏移。另一个可能是哈希表被加密需要先解密再读。4.2 现象解压出来全是乱码大小对不上原因多半是压缩标志位判断顺序错了。多段压缩的标志位优先级高于单一压缩如果先判断 zlib 就会把多段数据当 zlib 解。解决先判断0x00010000再判断其他压缩位。另外加密标志0x00000008如果没处理解压出来也是乱码。检查块表条目的flags是否完整读出。4.3 现象程序打开大归档时内存暴涨或卡死原因是一次性把整个文件读进内存或者哈希表探测没有上限。解决数据块按需读哈希表探测加最大次数限制比如最多探测table_size次。如果源码里用递归做哈希探测改成循环。大归档的块表可能几十万条用数组而不是链表存。4.4 现象导出的音频能播放但时长不对原因是解压后大小被截断或填充。MPQ 块解压后可能按块大小对齐实际有效数据小于解压后大小。解决用块表里的uncompressed_size截断不要用对齐后的大小。如果音频格式本身有头部长度字段以头部为准。另外某些音频是多段压缩每段独立需要按段拼接。4.5 现象编译报错找不到libmpq或类似库原因是源码依赖外部 MPQ 库但压缩包里没带或者版本不匹配。解决先看源码里的 README 或注释确认依赖名称和版本如果没有尝试用系统包管理器装或者把解析逻辑改成自带实现。不要随便下载来路不明的二进制库优先用源码里自带的解析模块。5. 进阶技巧用 Python 批量提取并做资源清单校验如果你不想每次开图形界面可以用 Python 把核心解析逻辑抽出来写一个批量提取脚本。这个脚本不依赖查看器 UI直接读 MPQ 文件遍历哈希表导出所有文件并生成一份清单记录文件名、大小、压缩标志、哈希值。清单可以用来校验提取完整性也方便后续做资源替换。import os, struct, zlib def batch_extract(mpq_path, out_dir): os.makedirs(out_dir, exist_okTrue) with open(mpq_path, rb) as fp: header read_mpq_header(fp) # 读哈希表和块表这里省略具体读取函数 hash_table read_hash_table(fp, header) block_table read_block_table(fp, header) manifest [] for entry in hash_table: if entry[block_index] 0xFFFFFFFF: continue block block_table[entry[block_index]] fp.seek(block[offset]) data fp.read(block[compressed_size]) try: raw decompress_block(data, block[flags], block[uncompressed_size]) except Exception as e: manifest.append((entry[name], FAIL, str(e))) continue out_path os.path.join(out_dir, entry[name].replace(\\, /)) os.makedirs(os.path.dirname(out_path), exist_okTrue) with open(out_path, wb) as f: f.write(raw) manifest.append((entry[name], len(raw), block[flags])) with open(os.path.join(out_dir, manifest.txt), w, encodingutf-8) as f: for item in manifest: f.write(\t.join(str(x) for x in item) \n)这段脚本的关键点entry[name]需要从哈希表反查文件名但哈希表本身不存文件名只有哈希值。所以实际实现里需要先有一份文件名列表或者用已知文件名去匹配。很多查看器能显示文件名是因为它内置了一份文件名哈希对照表或者从其他文件里读到了列表。如果你只有 MPQ 文件没有列表批量提取就只能按块索引导出文件名用索引代替。这是 MPQ 格式的一个硬限制不是脚本能绕过的。参数上out_dir建议按归档名分目录避免不同 MPQ 的同名文件覆盖。manifest.txt用制表符分隔方便后续用 Excel 或脚本分析。如果提取过程中大量失败先检查decompress_block是否覆盖了所有压缩标志再检查是否有加密块。加密块需要密钥密钥通常来自文件名哈希或固定值不同游戏不一样需要单独处理。我自己的习惯是拿到任何 MPQ 相关源码先跑通一个最小文件提取再逐步加功能。不要一上来就改 UI 或加批量先把解析链路验证死。另外MPQ 格式在不同游戏版本里有细微差异源码里的硬编码偏移和标志位不一定通用遇到新归档先 dump 文件头和前几个块表条目肉眼确认结构。希望帮到你。本文还有配套的精品资源点击获取