MPQ Storm库 源码分析 一

本文详细解析了MPQ文件的四种存储方式、创建MPQ文件、添加资源文件、封装MPQ文件、读取MPQ文件及资源文件的具体步骤。包括FLAT式下创建、添加、封装和读取的详细流程,以及资源文件的加密和压缩操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

MPQ是什么?

        MPQ维基上说的很明白。简而言之,它是暴雪公司用于游戏数据打包的工具,星际争霸,魔兽争霸游戏中都有使用。该工具内含游戏资源加密和压缩等功能。
        git下载地址:https://github.com/stormlib/StormLib

        MPQ文件总共有四种格式:

             第一种:FLAT格式,线性存储。

             第二种:Partial格式,分包的存储。

             第三种:MPQE格式,加密存储。

             第四种:BLOCK格式,模块存储。

        

FLAT格式最简单。不过该格式也支持对单个文件的加密和压缩。

本文主要分析FLAT格式,主要包含以下五点:

        一、创建一个MPQ文件。

        二、添加资源文件。

        三、封装MPQ文件。

        四、读取MPQ文件。

        五、读取资源文件。

        图一、图二、图三为FLAT格式的内部格式。

图一

        一、创建一个MPQ文件。

             主要过程是,调用SFileCreateArchive接口创建MPQ文件,创建成功之后,该接口会返回成功与否,同时将MPQ文件的句柄赋值给传进来的变量。

             创建MPQ文件详细流程是:

             1》在内存中初始化一个加密块StormBuffer;

             2》尝试打开一个同名的MPQ文件。假如已经存在的同名文件不是MPQ文件,MPQ工具就会尝试用MPQ文件的格式读取该文件。为了避免同名的错误,在StormLib的例子中,有先删除同一目录想的同名文件,再创建MPQ文件的逻辑。

             3》创建一个空的MPQ文件;

             4》重置文件大小。

             5》写入一个假的MPQ文件头部,即图一的A指向的区域。MPQ的头部有四种不同的格式,分别拥有不同的长度,不过默认有一个最大长度是208字节。写这个假的MPQ文件头部,最主要的目的是占位,保证第一个数据文件的写入位置是在正确的。

             6》如果是大文件,则创建大文件的索引表HetTable。

             7》创建HashTabl,用来索引文件的。

             以上七个过程,主要实现了两个结果,1、创建了MPQ文件,2、在内存中生成需要用到的MPQ数据。


        二、添加资源文件。

             主要过程是,调用SFileCreateFile接口,在MPQ文件中创建一个新的文件句柄。通过SFileWriteFile接口写入数据。调用SFileCloseFile释放内存资源。

             详细流程:

             1》检查是不是内部文件,MPQ主要有两个内部文件,一个是listfile,另外一个attributes。如果不是内部文件,则向MPQ文件中添加新的文件。

             2》找到MPQ文件中可以写入的位置,如果是第一次写入,则此时是图一B指向的区域。但此时还没有写入。

             3》通过HashString方法,将资源文件名转化为hash索引值,以新的hash索引值从当前的HashTable中取一个TMPQHash数据结构。

             4》假如不是第一次建立新文件。则此时,通过TMPQHash的dwBlockIndex值,在FileTable做dwBlockIndex偏移。获取新文件的入口TFileEntry。假如是第一次建立新文件,则会先到FileTable里面取出一个空闲的位置,分配一个TFileEntry。默认是取FileTable位置做dwFileTableSize偏移,而此时dwFileTableSize值是0,所以就是FileTable表中第一个位置,即图一B指向的区域。同时分配一个TMPQHash(hash索引的取值与文件名有关),分配完之后,与FileEntry关联。在FileEntry里通过dwHashIndex索引到hash表中的位置,TMPQHash通过dwBlockIndex索引到FileEntry。

             5》拿到了FileEntry以后,在SFileAddFile_Init函数中初始化FileEntry的其他值。经过这些逻辑。就可以拿到一个TMPQFile文件指针,用来写入数据。

             6》通过SFileWriteFile接口写入文件数据,可以将资源文件分批写到MPQ文件中,也可以将资源文件一次写入MPQ文件中,两种都不会有问题。


图二


             7》在将要写文件的前,会先写入一个扇区,默认有8字节,扇区与将要写入的数据的加密有关联,然后写入

数据,数据有选项,可以选择是否加密,是否压缩,是否选择数据作为单元文件存储。写入完毕之后,会再次写入当

前扇区,具体为什么要两次写入扇区,下一篇会继续分析。图二中,L指向的是真正的资源文件数据,K、M分别是扇

区。资源文件可选择加密和压缩。


图三


        三、封装MPQ文件。
        封装挺重要的,封装时会将新生成的文件相关的数据更新到MPQ文件头部,覆盖原先的假头部。封装的主要过程是,将与文件相关的HET表和BET表,以及HASH表,文件FileTable,按顺序写到最后一个资源文件的结尾位置。封装逻辑执行完之后,新的MPQ文件内部结构如图三。
        具体流程是:                
             1》确定是否要写入listfile、attributes等文件,如果要,则会在MPQ文件中创建,并写入。
             2》调用SFileCloseArchive,找到可以写入HET、BET、HASH、FILETABLE表的位置,然后将这些表的数据写入到新的MPQ文件中。写入后MPQ文件内部如图三。表的数据会写在第N个数据文件之后。最后,回到头部更新MPQ文件头部。
             3》释放内存中的资源。


        四、读取MPQ文件。
        主要流程是,打开MPQ文件,先读取头部,在读取正确之后,解析各个表的位置。获取数据文件位置。并且任意一个环节出错,都会退出。
        具体流程:
             1》初始化一个StormBuff(与数据加密有关)。然后用208字节的长度,从MPQ文件头部0位置开始读取,一次读取208字节。读取完之后强制转化为TMPQHeader数据结构。若TMPQHeader内部的数据是正确的,则开始读取文件,为了判断文件是否正确,MPQ会有许多检测。不过MPQ文件头部默认有四种格式。每一种格式的长度都不一样。在确定头部之后,会调用memset,将超出的部分置0,确保新生成的MPQHeader数据正确。
             2》通过新获得的头部数据结构,读取HASH表。Hash表通过TMPQHeader的dwHashTablePos属性,找到其在文件中的位置,并读取出。接着将文件中的数据,读取到内存里面,完成Hash表数据的读取。
             3》建立文件数据表。通过读取Hash表,初始化文件数据表FileTable,有了FileTable就可以得到FileEntry,有了文件入口,就能读取数据了。
             4》载入listfile和attributes文件。目前我没用到这两个选项。
             5》返回MPQ文件句柄。

        五、读取资源文件。
        有两种读取方式。第一种是取出其中某一个特定的文件,第二种遍历MPQ文件内部所有的数据文件。
        第一种取出一个文件是比较简单的。
        具体流程是:
             1》调用SFileOpenFileEx接口(Ex即外部文件的意思),传入文件名给该接口,接口内部会将文件名转化为hash数据,通过hash数据的dwBlockIndex属性,算出FileEntry数据,有了FileEntry,就可以获得要读取的文件的数据的位置,并且获取该数据文件的句柄。也可以先调用SFileHasFile判断是否有该文件。
             2》调用SFileGetFileSize获取文件大小,申请内存空间。调用SFileGetFileInfo获取文件信息,不是数据。
             3》调用SFileReadFile获取MPQ文件中特定资源文件的数据。
             4》调用SFileCloseFile释放申请的内存。

        第二种稍微复杂一些。
        具体流程是:
             1》调用SFileFindFirstFile,匹配符为*,则会全部匹配。返回一个hFind值。
             2》执行读取一个文件的流程,上面已经说明,此处不再赘述。
             3》调用SFileFindNextFile获取下一个文件的入口。
             4》循环执行1==>3流程。直至没有文件可以读取。

前言:其实年前研究mpq加密的时候就有这个想法,后来对加密失去兴趣,没有应用而已。 先从mpq读取(和写入)说起。市面上的软件大致有以下几种方法: 0.listfile式。0这个数字说明了它的原始。如果mpq没有用listfile明确叙述自己的文件组成,它就无法读取其中的文件。怎么说呢,这就好像只要犯人不招认,自己也认为犯人无罪的笨蛋侦探样,严谨到无聊。典型例子不是别人,正是大名鼎鼎的WorldEditor地图编辑器。对付这种软件,删掉listfile就切安好。 1.小白式。这种软件基本上用自制的dll(因为暴雪只提供了读mpqstorm.dll,没有写入),按照mpq文件格式非常循规蹈矩地步步读出内容。问题在于mpq数据稍有不对就会导致崩溃。例如header中mpq文件大小这项数据,war3读地图的时候根本不管,所以怎么写都不影响地图工作,但这类工具却会照着此数据读图,然后掉进番茄海的无底深渊。 典型例子是winmpqmpqmaster。 2.storm式。以火龙hke为代表。这类mpq软件用暴雪提供的storm.dll读取mpq,读取方式和暴雪致。由于mpq文件被设计成“知道文件名(含路径)可以很容易读取,但扫描所有文件路径却几乎不可能”的格式,war3在读地图时只用在需要的时候读指定文件就ok,所以这类编辑器也模拟war3读地图的方式,逐渐推算出“需要的文件”从而读出地图中近乎全部文件,只要在物编中涉及到或jass中提及的路径,都会检测对应的文件并列在表中。这是种近乎无敌的方法,不会报错(否则war3也玩不了这图),且war3map.j等固定文件必然被扫描出来(否则war3自己也找不到)。 然后是重点: 但这里有个致命问题——不管是火龙还是war3,不可能预知到游戏过程中全部的文件读取,更确切说,全部的字符串。如果字符串是明文写在脚本中,如“sound\\aaa.mp3”,那么火龙会认为这可能是个文件,然后顺藤摸瓜找到它。但如果写成“sound\\” + “aaa.mp” + I2S(3)等甚至加上存取哈希表动作,火龙或任何软件也无法完全预知。这种不可预测是理论级的,即“图灵机无法预测另台图灵机的全部可能状态”,等价于著名的“停机问题”,而停机问题是“理论不可计算”的。所以在游戏中虽然能正常听到音乐(或看到特效等),但火龙却无法提前知道这个文件的存在。 样例的图中正是这样,隐藏了个2m+的mp3文件,但火龙却只能读出大打war3map.xxx。 然后这种方法也能隐藏其它文件,但无法隐藏在物编中使用到的文件(如被某单位使用的导入模型)、地图必备文件(如j)和覆盖原路径文件(如替换的载入图片)。 3.hash扫描式。但是还没完,还有种方式,某些软件绕过mpq前面的哈希索引表,直接扫描后面的文件,这样虽然不能知道文件名,但能得到完整的文件列表(再怎么说文件也是封在mpq里的吧,把mpq整个扫遍总能发现)。例子是新版mpqeditor,样例图和某人提供的火影图都能打开,能看到堆没有文件名的文件,其中就有隐藏的mp3,改成mp3扩展名就能正常播放。这种方式应该没什么弊端(除了得不到正确文件名),如果和火龙结合,用上述方法隐藏的文件也能以“未知名文件”的形式显示出来,其他文件则完美显示。 所以mpq这种文件格式终究逃不过被拆的厄运,想完美隐藏文件果然是不可能的事情。全文完。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值