翻译前声明:
本翻译对于原文进行了适量删节和修改。
本翻译只做为学习参考使用,不得用于任何商业目的。
原文地址:http://www.campaigncreations.org/starcraft/inside_mopaq/
第一章 关于MPQ的历史
MPQ,也称MoPaQ,是Mike O'Brien发明的一种压缩文件格式。
在1996作为,MPQ应用在Diablo(暗黑破坏神)游戏中。
然而它的版权属于 Blizzard 的父公司 Havas Interactive,并且在Mike O'Brien离开暴雪后继续使用。 正是MPQs由于在Diablo(暗黑破坏神)中的出色表现,使其继续应用在Starcraft(星际争霸), Warcraft 2(魔兽争霸2), Diablo 2(暗黑破坏神2), Lords of Magic(魔法大帝)中。
第二章 关于MPQ的介绍
MPQ内部包含了许多文件,包括坐标算法、声音、动画、字符串、数字数据和故事情节信息。
明显地,MPQ的潜力很大。要想利用MPQ,那么您就需要了解它。
在有MPQ格式之前,一直使用的是WAR格式,在Warcraft 2,甚至在Warcraft1中存放游戏数据。然而WAR格式是简单的,不精制的,是由缺乏经验的程序员所编写的文件格式(相信我,我知道)。文件在档案中仅使用参考序数和是否被压缩做为唯一可选择调用的方法。
尽管如此它仍然完成了它的任务。它提供了压缩格式下的文件调用。但是,很快缺点开始出现。调用时使用参考序数,意味着一长传文件接口的名单必须被保留和被咨询,当程序员需要使用其中一个文件,那么则需要级长的时间,工作变得越来越繁琐。
当时这些问题并没有那么严重,所以有人坚持使用WAR格式,但是一切在使用Battle.net(网络对战)后,问题变得不能接受。
MPQ的特点
如被提及以前,MPQ格式一直被用做修正WAR的设计缺陷。但是现在他们也想增加一些全新的特点到MPQ。在暴雪的游戏中,MPQ格式的特点总结为以下几点:
Security. 安全
暴雪一定不希望在游戏中玩家可以修改数据。或许他们提早知道MPQ格式可以为Starcraft使用。 不管怎样,安全是最重要的,由此他们显然做了级大的努力去维护游戏的安全性。
Efficiency. 效率
MPQs要求执行时先简单预先输入的各种各样的任务数据然后实时放出。对于预先输入数据,时间并不重要。 但是实时放出就是另一件事了,其中的数据必须快速地被解压使用。
Multilinguality.多语言的计算机处理
在最开始的时候,暴雪就计划发布其游戏在全球游戏市场,因此他们尽可能的做到多语言。 在创新时,他们决定设计多语种能写入MPQ格式。
。
Expandability.扩展
显然的,在游戏中需要使用独立的数据。太大的数据不仅是效率低并且减慢游戏速度,如果补丁修改了,也是很麻烦的。暴雪明白这个道理,因而MPQ格式的要求就是有能力完全,高效率的,从多个档案数据中调用需要的数据。
什么是strom
相比在程序模块中复制函数,多数程序员喜欢把相同代码放到shared libraries(共享程序库)里。shared libraries是包含了任意程序功能的函数模块。不仅能避免多余,并且能缩小程序大小。
正因为如此,暴雪使用一个称为Storm的共享程序库(PC机上为Storm.dll,MAC机为Storm.bin)。
所有现代的暴雪游戏中都使用strom存放重要功能,比如读取MPQ,Battle.net和一些图形化例程。
当暴雪要发布新版本的游戏,只需要增加功能到strom,无需改变原有功能。 这意味着旧版本的游戏只用升级新版本strom就可以了,这就是我们俗称的安装补丁。
就像所有共享程序库,任何想使用它的程序都可以访问到它的函数。这就是为什么strom只包含MPQ读取功能。
什么是 MPQ API Library DLL
虽然 Storm 没有包含任何编写MPQ的功能。
但是 StarEdit 包含,因为 SCM/SCX 文件也是 MoPaQ文件。
但是这些函数被加密了,所以只有知识渊博的黑客们才可以使用。
对于Blizzard 来说不幸的是,有一个这样的黑客,他的名字是 Andrey Lelikov(aka Lelik)。
他发现了一种访问这些宝贵的函数的途径,并把这个复杂的过程封装在
LMPQAPI.DLL(Lelik's MPQ API Library DLL)文件中。该文件自动破解
StarEdit,将这些函数展示在所有的程序员面前。
第三章 MPQ的基本原理
通过整个计算机发展史来看,绝大多数的进步都是在求解问题中发生的。
那么在这一章中,我们将采取看看一些涉及到MPQ的问题及其解决办法。
HASH (散列或哈希)
问题:你有一个非常大的字符串数组,和一个字符串
怎么知道字符串是否在数组中?
你可能会开始在数组中与其他字符串比较每个字符串,但是,当进行应用后,你会发现,这种方法在实际使用时是特别慢的。在此之前,你又怎能在没有与其他字符串比较的情况下,确定这个字符串是否存在?
解决方法:hash
hash是规模较小的数据类型(例如数字)能指向其他较大的数据类型(通常是字符串) 。在这种情况下,您可以在数组中先存储hash。然后再计算其他字符串的hash,并比较它存储的hash。通过字符串比较,如果hash在数组相匹配的新的hash,就可以核实存在。这就是所谓的索引查找,可以加快对于不同大小的数组和平均长度的字符串的搜索速度约100倍。
unsigned long HashString(char *lpszString) { |
||
unsigned long ulHash = 0xf1e2d3c4; while (*lpszString != 0) { |
||
ulHash <<= 1; ulHash += *lpszString++; |
||
} return ulHash; |
||
} |
上面的代码,体现了一个很简单的散列算法。
功能是在每个字符添加前,把哈希值向左移动1bit,并总计字符串中的字符。
使用这种算法,字符串“arr\ units.dat ”将散列为0x5a858026,“unit\neutral\ acritter.grp ”将散列为0x694cd020 。
无可否认,这是一个很简单的算法,但是不是非常实用。因为在较低的数字范围内会产生一个相对可预见的输出,以及出现大量的冲突。当多于一个字符串散列为相同值就会出现冲突。
MPQ格式使用一个非常复杂的散列算法(如下所示),产生完全不可预测的哈希值,这个算法十分有效,这就是所谓的单向散列。
就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。从预映射,能够简单迅速的得到散列值,而在计算上不可能构造一个预映射,使其散列结果等于某个特定的散列值。
即构造相应的任意长度明文=固定长度散列值-1(固定长度散列值)不可行。
故此使用特别算法,文件名“arr\ units.dat ”将散列为0xf4e6c69d ,和“unit\neutral\ acritter.grp ”将散列为0xa26067f3 。
unsigned long HashString(char *lpszFileName, unsigned long dwHashType) { |
||
unsigned char *key = (unsigned char *)lpszFileName; unsigned long seed1 = 0x7FED7FED, seed2 = 0xEEEEEEEE; int ch; while(*key != 0) { |
||
ch = toupper(*key++); seed1 = cryptTable[(dwHashType << 8) + ch] ^ (seed1 + seed2); seed2 = ch + seed1 + seed2 + (seed2 << 5) + 3; |
||
} return seed1; |
||
} |
HASH TABLES(散列表或哈希表)
问题:您尝试在前面的示例中使用相同索引,您的程序一定会有中断现象发生,而且不够快。
您能做的只有让程序不去查询数组中的所有散列值。或者 您可以只做一次对比就可以得出在列表 中是否存在字符串。
听起来不错,真的么?
骗你的啦!!!
解决方案:a hash table
哈希表是数组中的一种特殊类型,也就是设定指定字符串的偏移量为那个字符串的散列值。
我的意思是,假如您设置一个字符串列表,使用一个单独的固定大小的数组作为哈希表。
您想查看新的字符串是否在前面的哈希表里。
那么您需要先计算要查看字符串的散列值,然后以哈希表大小的散列值为模求余数。
因此,如果您使用上面列出的简单散列算法,"arr\units.dat"将散列为0x5A858026,使其偏移量
0x26(0x5A858026 divided by 0x400 is 0x16A160, with a remainder of 0x26)。
假如这个地方有字符串,那么就会与被添加的字符串比较。
假如字符串在0x26不匹配,或直接不存在,那说明该添加的字符并不在在数组中。
下面的代码说明了这:
int GetHashTablePos(char *lpszString, SOMESTRUCTURE *lpTable, int nTableSize) { |
||
int nHash = HashString(lpszString), nHashPos = nHash % nTableSize; if (lpTable[nHashPos].bExists && !strcmp(lpTable[nHashPos].pString, lpszString)) |
||
return nHashPos; | ||
else | ||
return -1; //Error value | ||
} |
现在在这方面的解释有一个明显的缺陷。您认为发生冲突时(两个不同的字符串哈希以同等价值的) ?显然,他们不能在哈希表占用相同的接口。通常,解决的方法是在哈希表每个接口作为一个指针到一个链表,然后将链表里所有接口的散列值设置相同。
MPQ使用一个关于文件名称的哈希表记录内部文件,但是这个哈希表的格式与普通哈希表有所不同。
首先,MPQ根本不保存文件名,用三个散列值代替保存散列值的偏移量和为了核查文件名保存真实的文件名。
而是使用三个不同哈希值:一个做为哈希表的偏移量,两个是做为核查。
两个做为核查的哈希值被用来代替真实的文件名称。当然,也有可能两个不同文件名称的散列值相同,
不过这种情况发生的可能性为平均1:18889465931478580854784 ,对于任何人来说这应该足够安全了。
另一种方法:不同于常规的执行情况的mpq哈希表。
代替使用每个接口的链接表。当冲突发生时,把接口移动动下一个序列,并且重复动作,直到找到空闲空间。
下面的代码是在MPQ设置读取的基本方法:
int GetHashTablePos(char *lpszString, MPQHASHTABLE *lpTable, int nTableSize) { |
|||
const int HASH_OFFSET = 0, HASH_A = 1, HASH_B = 2; int nHash = HashString(lpszString, HASH_OFFSET), nHashA = HashString(lpszString, HASH_A), nHashB = HashString(lpszString, HASH_B), nHashStart = nHash % nTableSize, nHashPos = nHashStart; while (lpTable[nHashPos].bExists) { |
|||
if (lpTable[nHashPos].nHashA == nHashA && lpTable[nHashPos].nHashB == nHashB) | |||
return nHashPos; | |||
else | |||
nHashPos = (nHashPos + 1) % nTableSize; |
|||
if (nHashPos == nHashStart) | |||
break; | |||
} return -1; //Error value |
|||
} |
每条代码反复研究,理论的背后是不难的。
它基本上是如下这个过程:
1.计算3个散列值(一个冲突和两个检查)并将其存储在变量。
2.移动冲突散列值的接口
3.接口未使用的吗?如果是的话,停止搜寻,并传回'文件没有被发现' 。
4.两个检查是否匹配检查我们正在寻找文件的散列值呢?如果是的话,停止搜寻,并传回目前的接口。
5.如果在最后一个接口,移动到列表中的下一个接口,(wrapping around to the beginning ??)。
6.刚移动的借口是否和冲突时的散列值相同(是否检查了整个哈希表? ) ?如果是的话,停止搜寻,并传回'文件没有被发现' 。
7.回到第3步。
如果您很仔细的话,您可能会从我的解释和示例代码注意到,是因为mpq的哈希表已保留所有文件接口在MPQ 。那么您认为每一个哈希表项如何得到填补?答案可能出乎您的意料却显而易见:您不能继续添加文件。几个人都问我为什么有一个上限(所谓的档案限制),在一个MPQ中可以有多少档案, ,是否有任何的方式解决这个限制。那么,您已经有了第一个问题的答案。至于第二项;没有,您不能绕开该文件的限制。对这个问题,哈希表,甚至不能调整大小,除非您重新改造MPQ。在哈希表每个接口因为重新设置大小不同位置可能会改变。而且导致无法获得新的地址,因为地址是文件名的散列值,并且我们还可能不知道档案名称。
Compression 压缩
问题:您有一个很大的程序(比如说, 50 megs ) ,您要分发在互联网上。但50 megs是一个非常大的下载量,而且别人未必有兴趣等待四个半小时去下载这个程序。
解决方法:压缩。
压缩是一门艺术。是在更小的内存中重新放置等量的数据。
有数以百计不同的压缩算法,使用不同的方式。
MPQ实际使用的算法是the Data Compression Library, licensed from PKWare (one of the leaders in applied compression),在此解释太过于复杂。相反,我会尝试解释一个更简单的压缩算法的例子。
本章节并不完全 ,因为作者没写完
Encryption 加密
这个世界上总是有喜欢剽窃的人存在,所以我们需要有一个保护资料安全的系统。
千百年来人们一直试图传递信息给他人。从手写的信件进行,信使徒步穿越古希腊,纳粹潜艇的无线电传输,在第二次世界大战,使用信用卡交易,到网络应用的今天,有能力去确保别人无法获得您的信息是必要的。
所谓的加密是复杂的艺术的保护,然而我们不知道设计第一个算法的人,我们也不知道到底有多少的算法。一切从简单的数据加扰,嬗变,甚至算法,其中有解密密钥(有时也称为密码)是不同的加密密钥(在一个方法所谓非对称加密) ,已做了一次又一次。
做为一个全面的权威加密方法,本文章肯定从来没有索赔,也不期望。
您只需要知道加密是你与MPQ直接相关的。
让我们从一个简单的加密算法开始,这是刊登在《Basic Lab Notes》 (为了可读性本人改变了一些变数名称,评论删除) :
void EncryptBlock(void *lpvBlock, int nBlockLen, char *lpszPassword) { |
||
int nPWLen = strlen(lpszPassword), nCount = 0; char *lpsPassBuff = (char *)_alloca(nPWLen); memcpy(lpsPassBuff, lpszPassword, nPWLen); for (int nChar = 0; nCount < nBlockLen; nCount++) { |
||
char cPW = lpsPassBuff[nCount]; lpvBlock[nChar] ^= cPW; lpsPassBuff[nCount] = cPW + 13; nCount = (nCount + 1) % nPWLen; |
||
} return; |
||
} |
这是非常简单的哈希代码,不应被用来在一个实际的程序中使用。
即使代码是隐藏的(没有双关语意),这也是简单的 。
不言而喻,这是通过块进行加密的,把每个字节与相应的字节的密码转换为二进制。然后修改字节的密码,加入13 ( 选择13是因为这是一个素数)。这样做是为了使代码的模式,更难以识别。
那么,用此算法,加密字符串“encryption” ( 65 6E 63 72 79 70 74 69 6F 6E),加密的密码“ mpq ” (4D 50 51 ),这样会得到一个无法读取字符串(28 3E 32 28 24 2E 13 03 04 1A)。
现在,这个算法是对称的。这意味着密码是用来加密有相同密码的块。事实上,由于转换为二进制是一个对称的运作,完全相同的算法可以用来解密。请注意,大多数的对称加密算法是不完全对称,所以他们要求加密和解密的功能有所不同。
好吧,下面就就是关键的地方。
如果您想要编写,就必须在哪里都知道加密算法。
教导给您这个方法是我的使命。
MPQ的加密算法混合其他加密技术。它创建了一个加密表(这也是用在散列函数) ,并使用一个文件的加密密钥,以挑选出某些成员的加密表。然后对表中的成员进行转换成二进制数据加密。现在,用一个相当奇怪的方法来做,所以或许有些代码将显示您it is overcomplicated :-p。以下代码生成密码表数组长度为0x500:
void prepareCryptTable() { |
|||
unsigned long seed = 0x00100001, index1 = 0, index2 = 0, i; for(index1 = 0; index1 < 0x100; index1++) { |
|||
for(index2 = index1, i = 0; i < 5; i++, index2 += 0x100) { |
|||
unsigned long temp1, temp2; seed = (seed * 125 + 3) % 0x2AAAAB; temp1 = (seed & 0xFFFF) << 0x10; seed = (seed * 125 + 3) % 0x2AAAAB; temp2 = (seed & 0xFFFF); cryptTable[index2] = (temp1 | temp2); |
|||
} | |||
} | |||
} |
你是不是越来越觉得暴雪聘请了一名心怀不满的微积分教授写这些算法?还好对与我这不是问题,如果你不明白此代码。如果您想要编写,您需要这些功能,你不一定要了解他们。无论如何,在密码表初始化后,我们可以解密MPQ数据,具有下列功能(不要期望我向您解释,我不想知道如何运作自己! ) :
void DecryptBlock(void *block, long length, unsigned long key) { |
||
unsigned long seed = 0xEEEEEEEE, unsigned long ch; unsigned long *castBlock = (unsigned long *)block; // Round to longs length >>= 2; while(length-- > 0) { |
||
seed += stormBuffer[0x400 + (key & 0xFF)]; ch = *castBlock ^ (key + seed); key = ((~key << 0x15) + 0x11111111) | (key >> 0x0B); seed = ch + seed + (seed << 5) + 3; *castBlock++ = ch; |
||
} | ||
} |
第四章 STROM
称为STROM库函数,或者简称为STROM。
它是对于本身的运行系统,拥有庞大的功能库函数。甚至不需要Microsoft支持。
它本身包含了足够强大的功能,甚至不需要调用本地API函数。
事实上,STROM包含了所有暴雪编写的可以重复使用的功能。
但它也拥有一些操作系统特殊的要求 比如那些在GDI,DirectX,QuickDraw等等。
原因很简单,就是为了减轻从一个系统到另一个系统的接口问题。
毕竟,这就是为什么花成千上万的工作时间把数以千计的操作系统函数从Windows源调用Mac一样,为什么在不花时间去做调用,而去改写功能?
根据STROM的多个版本,大约累计了275个实际有用的功能。
正如您看到的,没更新STROM时,仍然使用STROM库函数。同样,更新后依然是旧的STROM库函数,
只是做了更新。这是为了保证游戏在不同版本的兼容性。
这些275个使用功能分为约20个功能集(通常在Windows环境下称为subsystems,在MAC环境下称为managers。
下面所示部分清单:
Memory Subsystem -记忆体子系统-例行的共同记忆功能,包括分配新的内存,释放分配内存,灌装记忆体,以及更多。STROM没有自己的内存管理,包括内置的错误检查和其他强大的功能。该子系统功能与在PC上与'mem'前缀相等。
String Subsystem - 字符串子系统-功能是使用字符串,如复制,合并,搜索等这些职能是大多数部分,相等于'str'的功能。
File Subsystem - 文件子系统-功能是存取文件系统。有能力读出(但不包括写入)无论是在磁盘上的可靠文件,还是mpq档案。撇开mpq读取的功能,其他功能都是高级系统功能运行方式。
Network Subsystem -网络子系统-功能是接入远端的电脑系统,通过使用IPX,调制解调器, TCP/IP和直接电缆。职能是与服务器或在游戏中玩家的通讯。使用高级系统特殊调用。
Error Subsystem -错误子系统- 功能是捕捉和处理错误。这些职能大部分没有与任何操作系统的等值。
Registry Subsystem -登录子系统- 功能是持久性储存数据到计算机中。使用注册表在Windows系统,或MACS系统上。
Bitmap Subsystem -位图系统-功能是位图文件装载和显示。使用系统特殊调用。
目前为止,大约只记录了40种功能,因为我手边没有足够时间来做,认真来做的话大约需要几个月。
此外这里只只讨论MPQ。
Using the Strom API 使用STROM API函数
说明:其余的这一章是针对Windows平台的!
正如我以前说过,STROM功能任何人都可使用它们。不过,暴雪并不希望如此。
我花了最近两天时间,总结出来:STROM使用一个非常邪恶的方法来对付我们这种想使用它的人。
我花了至少10个小时的努力,试图解除愚蠢的事,而我现在可以很骄傲的说我成功了,我会全力为您解释冗长而复杂的细节。
经过我和Mike O'Brien所谓的the Storm Interface Library(接口库)斗智斗勇。发现这是由一个头文件和导入库组成,所以我做了Storm booby-traps这个工具 。要记得DLLs 101 ,是包含被用来当程序编译连接程序DLL的导入表的导入库 。这意味着什么,就是所有您需要做的就是storm.lib (在STROM接口库)与模块连接在您的程序和#include storm.h头文件。这真令我疯狂,我不得不让您可以轻松使用the Storm Interface Library。
现在,为了以后少点麻烦,让我们现在就看看STROM的功能。
Opening an MPQ Archive- SFileOpenArchive
打开MPQ存档函数—SFileOpenArchive
|
在此之前,您可以先看一个MPQ文件,您必须先打开它。
为此每您必须使用SFileOpenArchive。它会打开一个存档,并给你一个HANDLE,您可以稍后调用SFileOpenFileEx和SFileCloseArchive 。
第一个参数,lpFileName,只不过是mpq公开的名称,绝不能为空。
第二个参数,dwMPQID 是将指派给mpq内部的ID。这并不改变mpq ,目前还不清楚为什么这样做。
第三个参数,dwUnknown,这是唯一我们认为没用的,但是不容忽视。
最后一个参数,lphMPQ,是一个HADLE的指针(您必须先声明)。
如果SFileOpenArchive成功完成,this HANDLE will be that of the MPQ。
如果SFileOpenArchive成功,其返回值将为零。
但是,有几种情况可能导致SFileOpenArchive失败。
如果它失败,将返回一个值是假的。在这种情况下,您可以调用GetLastError获得更进一步的信息,为什么失败。如果lpFileName是一个零长度字符串或phMPQ是Null ,GetLastError 将返回ERROR_INVALID_PARAMETER 。如果该文件lpFileName不存在, GetLastError 将返回 ERROR_FILE_NOT_FOUND。在一些非常罕见的情况下, GetLastError可能会返回其他一些潜错误值。
Closing an Archive - SFileCloseArchive
关闭存档函数 - SFileCloseArchive
BOOL WINAPI SFileCloseArchive(HANDLE hMPQ); |
|||
Parameter | What it is | ||
hMPQ | [in] The HANDLE of the MPQ to close, which was acquired earlier with SFileOpenArchive. SFileCloseArchive will fail (or worse) if this is NULL or a HANDLE not obtained with SFileOpenArchive. |
一旦您开启一个mpq存档,你必须记住它关闭时,您就大功告成了!SFileCloseArchive 是SFileOpenArchive 的产物。
作为与SFileOpenArchive ,SFileCloseArchive返回一个非零值,那就是成功的
如果返回一个假值,那就失败了。
然而,在这种情况下,GetLastError不会提供任何有用的信息。
-所以你只能假设原因是hMPQ参数是无效的。
Opening a File Inside an MPQ - SFileOpenFileEx
打开MPQ内的文件函数 - SFileOpenFileEx
|
只是因为你用SFileOpenArchive并不意味着您就可以从它立即开始读取。请记住,MPQ只不过是包含其他文件的多档案文件。在您可以阅读任何一个mpq ,您必须打开一个(或多个)的MPQ内部档案。 SFileOpenFileEx就是让您使用这一任务的函数;它将打开在一个mpq所请求的文件并对其返回一个HANDLE。
再次, sfileopenfileex将返回一个非零值就成功,是假值的就失败,您可以调用getlasterror获得的原因。如果lpfilename是一个零长度字符串或lphfile是Null , getlasterror将返回error_invalid_parameter 。如果该文件不存在于mpq , getlasterror会报告error_file_not_found 。在一些罕见的情况下, getlasterror可能会报告error_file_invalid ,并就极为罕见的情况下,它可能会返回其他一些模糊的错误值。
重要注意事项:当您调用sfileclosearchive关闭mpq ,同样会关闭所有MPQ内部打开的文件,您获取到来自sfileopenfileex的HANDLEs成为无效。如果您调用sfilereadfile , sfilegetfilesize , sfilesetfilepointer ,或sfileclosefile,这些其中一个无效的HANDL,调用将失败,并且STROM甚至可能崩溃。
Closing a File Inside an MPQ - SFileCloseFile
关闭MPQ内的文件函数 - SFileCloseFile
BOOL WINAPI SFileCloseFile(HANDLE hFile); |
|||
Parameter | What it is | ||
hFile | [in] The HANDLE of the file to close, which was acquired earlier with SFileOpenFileEx. SFileCloseFile will fail (or worse) if this is NULL or a HANDLE not obtained with SFileOpenFileEx. |
就像SFileCloseArchive is to SFileOpenArchive, SFileCloseFile is the natural compliment of SFileOpenFileEx, 用来关闭一个已经打开的文件.
也想sfileclosearchive , sfileclosefile将返回一个非零值说明成功的,假值的就失败, getlasterror将不提供帮助。所幸的是,多于sfileclosearchive , sfileclosefile只在hFlie是NULL或一个无效的HANDLE。
Reading from a File in an MPQ - SFileReadFile
读取MPQ内文件函数 - SFileReadFile
BOOL WINAPI SFileReadFile(HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped); |
|||
Parameter | What it is | ||
hFile | [in] The HANDLE of the file to read from, which was acquired earlier with SFileOpenFileEx. SFileReadFile will crash if this is NULL or a HANDLE not obtained with SFileOpenFileEx. | ||
lpBuffer | [out] A pointer to a buffer in memory where SFileReadFile will place the data read from the file. This buffer must be at least as large as nNumberOfBytesToRead. SFileReadFile will fail if this is NULL. | ||
nNumberOfBytesToRead | [in] The number of bytes for SFileReadFile to read from the file. SFileReadFile may crash if this is larger than the size of lpBuffer. | ||
lpNumberOfBytesRead | [out] A pointer to a DWORD that will hold the number of bytes actually read from the file. The number of bytes read will never be more than nNumberOfBytesToRead, but may be less if the number of unread bytes in the file is less than nNumberOfBytesToRead. It is not recommended to let this be NULL. | ||
lpOverlapped | [in] A pointer to an OVERLAPPED structure. This is used for asynchronous reading of files on a disk, and must be NULL when reading files in MPQs. |
当然,您要先打开再读取文件。
一旦你获得一个有效的文件HANDLE从sfileopenfileex ,这就是这个函数的功能。 sfilereadfile会读取指定的字节数,然后推进文件指针。这意味着,如果您有一个文件,您调用sfilereadfile会读取的一半的档案,当您再次调用sfilereadfile,你会得到另一半的档案。如果您需要再次读取上半部分的文件,你会需要调用sfilesetfilepointer 。
sfilereadfile将提供一个非零的返回值就成功,或虚假的就失败。不过,你必须记住,只是因为它返回一个非零值,并不等于它实际上读取了什么,这只是说明没有错误发生。
如果在该文件hfile有不足未读字节(字节从文件指针到文件结束)比要求数量少, sfilereadfile会读不到的数目要求字节;
如果档案hfile的文件指针是在该文件的末尾, sfilereadfile会读什么,设置lpnumberofbytesread为0 ,返回真。因此,检查lpnumberofbytesread 是非常重要的。
Getting a File's Size - SFileGetFileSize
获得文件大小函数- SFileGetFileSize
|
这是普遍认为是最坏的编程实践,因为完全可以进行修改,在开始读取时让不明长度的文件不会造成崩溃。sfilegetfilesize在这里只是把戏,因为它在用sfileopenfileex打开文件后才可以撷取文件的大小 。
也有一些重大的故障点在sfilegetfilesize 。首先是含糊不清错误。当sfilegetfilesize成功,它将返回文件的大小(可0 ! ) 。但是,当发生错误时,它将返回0xFFFFFFFF的,而且同一件事,它会返回为4294967295字节( 4 GB的) 。所幸的是,这不是一个很大的问题,正如您可能从未真正看到一个文件这么大。第二个问题,更危险的是sfilegetfilesize缺乏错误检查。 sfilegetfilesize不检查是否hfile是有效或是否为零。这意味着,如果你给它一个hfile的无效的HANDLE ,程序崩溃,电脑也会崩溃。因此,底线是这样的:无比谨慎使用此功能。
Moving the File Pointer - SFileSetFilePointer
移动文件指针函数- SFileSetFilePointer
DWORD WINAPI SFileSetFilePointer(HANDLE hFile, long nDistanceToMove, long *lpDistanceToMoveHigh, DWORD dwMoveMethod); |
||||||||
Parameter | What it is | |||||||
hFile | [in] The HANDLE of the file whose file pointer is to be moved. SFileSetFilePointer will crash if this is NULL or a HANDLE not obtained with SFileOpenFileEx. | |||||||
nDistanceToMove | [in] The low-order 32-bits of the number of bytes for SFileSetFilePointer to move the file pointer, with positive numbers moving the pointer forward and negative numbers moving the pointer backward. This value can also be 0. | |||||||
lpDistanceToMoveHigh | [in] A pointer to the high-order 32-bits of the distance for SFileSetFilePointer to move the file pointer. But, because MPQs do not support files this large, this is unused and must be NULL or SFileSetFilePointer will fail. | |||||||
dwMoveMethod | [in] Specifies the relative location the file pointer will be moved to. Must be one of these following values in Windows.h:
|
要想从文件任意位置读取,就要先在该文件提出移动指针,这个功能函数就是sfilesetfilepointer。
文件指针指向了下次读取时的读取位置。每次读取和编写将会移动文件指针到读写区域的最底部。
sfilesetfilepointer实际上并不移动文件指针,只是在文件中对应ndistancetomove。相反, sfilesetfilepointer移动文件指针到一个相对位置,无论是开头或结尾的文件。
举例来说,假设您有一个千字节的文件。当文件第一次打开,它的文件指针设置为0 ,则指的是第一个字节。然后,您读100个字节从该文件。然后,您调用sfilesetfilepointer设置ndistancetomove 500 。如果您调用dwmovemethod 设置为file_begin,文件指针将被设定为500 。如果你曾dwmovemethod作为file_current ,文件指针将是600 ,因为文件指针被转移到了当您从档案读取后100字节。但如果设置dwmovemethod以file_end , sfilesetfilepointer会失败,因为它将尝试设置档案指针一千四百九十九(文件中的最后字节, 999 ,加500 )不存在。如果您多用几次,会发现这是个非常简单的函数。
假如失败, sfilesetfilepointer返回0xFFFFFFFF ,作为sfilegetfilesize也有同样的陷阱 。但是,当圆满完成, sfilesetfilepointer会返回档案hfile新的绝对位置的文件指针 。这意味着您可以简单地获取当前的立场文件指针调用sfilesetfilepointer设置ndistancetomove 0 , dwmovemethod设置为file_current 。事实上,这是为什么说不存在sfilegetfileposition功能。
就像sfilegetfilesize , sfilesetfilepointer会肆无忌惮地使用任何你给它hfile的HANDLE,不会进行任何错误检查。这意味着,确保获得一个有效的文件处理,您必须谨慎,否则,您的电脑又会崩溃。
Choosing a Language - SFileSetLocale
选择语言- SFileSetLocale
LCID WINAPI SFileSetLocale(LCID lcNewLocale); |
||||||||||||||||
Parameter | What it is | |||||||||||||||
lcNewLocale | [in] The language code (LCID) that SFileSetLocale will make the new default. The following codes are ones that I've found in Starcraft MPQs:
|
SFileSetLocale 是功能简单,背后复杂的代表.它使用了暴雪的multilinguality系统。感谢它,一个单一的函数调用,保证所有文件读出一个mpq的语言。它的唯一参数是the entire Storm MPQ subsystem。它绝不会失败,并且它传回的语言代码你给它,不过其返回值毫无价值。
multilinguality系统原理是这样的:每个MPQ档案有一个语言代码,并只要他们有不同的语言代码就可以有多个文件具有相同名称的。当调用sfileopenfileex时, sfileopenfileex寻找一个文件具有相同的语言代码储存,然后调用sfilesetlocale (如果sfilesetlocale从来没有被调用,语言的代码为0 ) 。如果一个文件匹配的语言代码无法找到, sfileopenfileex将打开中立语言(有一个语言代码0 )版本的文件。
第五章
THE STARCRAFT CAMPAIGN EDITOR AND THE MPQ API LIBRARY 星际争霸编辑器和MPQ API LIBRARY Starcraft Campaign Editor 是什么呢?其实就是一个能让您自己作出星际争霸地图的程序。 并把地图以SCM格式保存,或者保存为SCX文件。 可是这些个SCM/SCXs文件 和Warcraft 2是一样的原始文件? 假如您用hex editor 看过这些文件,那么您会得到一个否定的答案。 实际上SCM/SCXs就是MPQ文件!! 那么你也许认为很简单,那你就错了,StarEdit使用了一套难以捉摸的MPQ编写套路。 不过在您仔细阅读下面的内容,您将会得到答案。 Using StarEdit - The MPQ API Library 使用编辑器- The MPQ API Library 注意:这章是针对WINDOWS平台的,适用于THE MPQ API LIBRARY 2.0或更高! 在这里我们并不能很快的编译,因为编辑器有的功能我们并不能直接使用。 不像STROM之类的shared libraries ,StarEdit拥有复杂的操作系统保护机智,而不只是对于文件的保护。就算您是一位很好的程序员,您一样无法直接修改它。对于这种高难度的熟练的对运行系统的改写,还没有人能完成过。 就在这个时候Andrey Lelikov (简称Lelik)横空出世。 Lelik是一位熟悉系统内部工作机制的俄罗斯程序员。他设计了一个能够使用 StarEdit MPQ 的方法。 他把自己写的详细功能放进了MPQ API Library 。 就像STROM,MPQ API Library(又名LMPQAPI),它包含了共享库(可惜的是,像STROM接口库,现在在MAC机上还没有)。LMPQAPI不仅包括了StarEdit的MPQ编写功能,而且提供接口读写STROM。 如果您想同时使用STROM和StarEdit,您不需要同时使用LMPQAPI和STROM接口库。 一个LMPQAPI足已。 好吧。我提醒您一件事,您想在使用LMPQAPI的时候区分是用STROM还是StarEdit么? STROM功能有就像使用STROM接口库时一样有一个前缀'SFile',在使用StarEdit时,前缀是'Mpq'。 这是很重要的,因为这说明了STROM和StarEdit的功能不兼容。 这意味着您无法用SFileOpenArchive获得的MPQ HANDLE去在StarEdit('Mpq'前缀的函数)里调用,反之亦然。 如果您还是调用的话,调用会失败,程序会崩溃。记住这点。 ¿Sé Habla Español? 让人讲西班牙语? 因为75%以上的星际争霸或暗黑的玩家是以英语为母语,所以大多数MPQ开发测试都基于这些游戏的英文版。对于使用英文版游戏的玩家,MPQ会运行良好。但事实上,98%的标准MPQ文件使用的是language-neutral (比如图象文件等等)。甚至有人用全非英语的MPQ玩游戏也没有问题。 不过,很明显这里是有问题的,只是还要等些时间才有人能发现吧。 做为上面两章的解释,MPQ格式具有强大的多国语言功能。 但是,您完全没必要做多语言的SCM/SCXs。 这就是说,您完全不必要让StarEdit支持多语言功能,就连暴雪的程序员都懒的做。 但是我们都有兴趣研究MPQ,除非没必要,其中的许多语言功能还是有用的。 在技术方面说,所有的StarEdit功能都只运行有语言代码为0或language-neutral代码的文件。 也就是说,MpqAddFileToArchive和MpqAddWAVToArchive只增加language- neutral files, MpqDeleteFile只删除language-neutral files, MpqRenameFile 只会重命名language-neutral files. 这种设计决定了,执行结果并不明显。在前一章也提到过,假如在打开有相同名称不同语言的文件时,STROM使用了SFileSetLocale做为语言过滤器,用来决定到底打开哪个文件。 假设在StarEdit使用一个MPQ文件代替patch_rt.mpq ,而且在那个MPQ文件里有英语/language-neutral解析度文件rez\gluAll.tbl (这文件内有多个语言版本),但是不能有葡萄牙语版本(选定任意语言)。 当您运行这个游戏的葡萄牙语版时,程序会查询在您MPQ文件中的该语言版。结果就是失败,而且默认又为英语版本的,对不对? 好吧,不是这样的。STROM允许您同时打开几个MPQ文件,并且STROM会搜索已经加载的MPQ,然后自动加载最新的MPQ到文件里。但是在这个过程中,STROM在系统为language-neutral以前,会检查已经打开的所有特殊语言的MPQ。 这意味着,前面STROM从broodat.mpq 载入葡萄牙语版本,而不是您自己的language-neutral 版本。 很不幸,现在还没有解决的办法,希望新版本的LMPQAPI能被解决吧。 Initializing the MPQ API Library - MpqInitialize 初始化MPQ API Library函数- MpqInitialize BOOL WINAPI MpqInitialize(); 不像STROM,LMPQAPI在控制StarEdit时有巨大的复杂的任务要做。 因此,无法在启动LMPQAPI时做完一切。你必须告诉LMPQAPI什么时候运行。所幸,这很简单。 你要做的就是在您启动程序前调用MpqInitialize,LMPQAPI会自动做完剩下的。 请确定在调用LMPQAPI其他函数前,您调用了MpqInitialize。此外,即使您对STROM接口库什么都不做,也必须在任何STROM函数调用前被调用。 一次调用会同时初始化STROM和STAREIDT。 1.Starcraft/Brood War 1.07 必须已经安装,Storm.dll和StarEdit.exe必须在程序目录中 2.StarEdit不能和LMPQAPI一起运行. 为了调用MpqInitialize初始化上面的两个要求必须满足。尽管还有其他原因,上面两个却是最常见的。 不管什么原因,MpqInitialize调用失败后,都会返回FLASH,您可以检索GetLastError设置一个错误值。 如果LMPQAPI无法在游戏目录或在您程序的目录里无法找到StarEdit.exe ,那么GetLastError会返回MPQ_ERROR_NO_STAREDIT; 如果StarEdit.exe的版本不匹配,GetLastError会返回MPQ_ERROR_BAD_STAREDIT; 如果StarEdit已经运行GetLastError会返回MPQ_ERROR_STAREDIT_RUNNING; 如果是因为其他原因GetLastError通常会返回MPQ_ERROR_INIT_FAILED。 但是,无论为什么GetLastError调用失败,返回了什么,您要做的就是尽快的关闭程序。 不要调用任何LMPQAPI功能(STROM或者StarEdit的功能),当然也不要在调用MpqInitialize了。 Opening an MPQ for Editing - MpqOpenArchiveForUpdate 打开MPQ- MpqOpenArchiveForUpdate
与STROM一样,您在使用前必须先打开它. MpqOpenArchiveForUpdate 打开(或创建)一个存档,这是为了您使用其他StarEdit功能的,并返回存档的HANDLE. 但是不像SFileOpenArchive,MpqOpenArchiveForUpdate需要您在时间上做出选择. 第一选择是dwcreationdisposition参数。它告诉mpqopenarchiveforupdate是否应该创建一个新的存档还是打开一个现有的,或两者之间。其他的决定性参数是dwhashtablesize 。 dwhashtablesize告诉mpqopenarchiveforupdate创建什么大小存档的哈希表(这也是该文件限制),在 mpqopenarchiveforupdate事件中必须创建要一个新的存档。 做为一个存档的哈希表大小差不多是1000,除非你知道存档一定会超过1000个文件。 但同时请记住,每个哈希表项和存档文件将新增16个字节,(见第2章的哈希表,或第5章关于mpq哈希表的更多信息) 。 在上面的进程中MpqOpenArchiveForUpdate可能调用失败. MpqOpenArchiveForUpdate将返回INVALID_HANDLE_VALUE或者NULL. 我们根据调用GetLastError通常可以获取有用的信息,但不是绝对. 如果lpfilename参数为空, getlasterror将返回error_invalid_parameter 。 如果dwcreationdisposition返回moau_open_existing或者lpfilename不存在, getlasterror将返回error_file_not_found 。反过来说,如果dwcreationdisposition返回moau_create_new和档案lpfilename已经存在, getlasterror将返回error_already_exists 。 最后,如果mpq存档存在,但是是无效或损坏的, getlasterror将返回mpq_error_mpq_invalid 。 在一些罕见的情况下, getlasterror将返回其他一些错误代码。 Closing a Modified Archive - MpqCloseUpdatedArchive 关闭修改过的存档- MpqCloseUpdatedArchive
这里和在STROM中一样,您在哪用SFileOpenArchive打开MPQ,那么就在修改它的地方用SFileCloseArchive 关闭. 那么MPQ存档打开时用MpqOpenArchiveForUpdate,关闭时用MpqCloseUpdatedArchive. 然而在这时候有一点区别.STROM不会修改实际MPQ,所以关闭MPQ HANDLE时没有什么特别需要做的. 但是StarEdit 确实修改了MPQ.而且MPQ的散列值和文件表是在没有调用MpqCloseUpdatedArchive关闭MPQ HANDLE前不能写在MPQ分区的. 这意味着要快速关闭StarEdit MPQ HANDLEs,要不然您有可能再次程序崩溃,而无法保存修改的MPQ. Adding a File - MpqAddFileToArchive 添加文件函数- MpqAddFileToArchive
往往大约 95%的时候会在使用 StarEdit MPQ功能时候添加文件. 对于这个任务, 您会用到的函数有MpqAddFileToArchive和它的姊妹功能MpqAddWAVToArchive (稍后讨论). MpqAddFileToArchive在MPQ hMPQ分区添加文件lpSourceFileName 使用名称lpDestFileName, 并在这个过程中压缩 或/和 加密它. 由于一些设计上的疏漏,在您并没有完全明白前,MpqAddFileToArchive会是个大麻烦. 1.MpqAddFileToArchive并不检查lpSourceFileName和lpDestFileName是否为空. 就是说假如任一参数为空,您会再次看到程序崩溃. 2.覆盖MPQ中已有文件时MpqAddFileToArchive的运行机制. 当您调用MpqAddFileToArchive去添加的文件已经存在MPQ中(在这种情况下,你就不得不指定dwflags为mafa_replace_existing ),MpqAddFileToArchive会在不确定文件lpSourceFileName是否存在前就不分青红皂白地删除存在的文件lpDestFileName. 解决的办法很简单:确保lpsourcefilename和lpdestfilename是有效的(非空) ,以及确保lpsourcefilename存在之前,调用mpqaddfiletoarchive 。 现在您应该可以饶过所有的障碍,用mpqaddfiletoarchive成功添加文件了吧. 这时mpqaddfiletoarchive将允许返回TURE. 如果有错误,它将返回FALSE 。在这种情况下,少量的信息可调用getlasterror 。 如果该文件lpsourcefilename不存在, getlasterror将返回error_file_not_found (尽管它的有点晚) 如果哈希表是FULL(见第2章) , getlasterror将返回mpq_error_hash_table_full 。 如果该文件lpdestfilename已经存在 和在dwflags中未指定mafa_replace_existing , getlasterror将返回mpq_error_already_exists 。 但是,在很多情况下getlasterror将返回其他一些错误代码或没有代码。 Adding a File with WAV Compression - MpqAddWAVToArchive 添加WAV压缩文件函数- MpqAddWAVToArchive
毫无疑问,最流行的新功能的lmpqapi 2.0版(就是我做的) ,便有功能 mpqaddwavtoarchive 。 虽然mpqaddfiletoarchive能压缩约80 %的文件(非wav文件) ,它对WAV文件的压缩只有平均约5 % 。这是由于wav数据性质和它的不可压缩性。 在这里,对于wav压缩的压缩是必要的,而 mpqaddwavtoarchive就实现了这个功能。 尽管工作原理不同,MpqAddWAVToArchive的界面与 MpqAddFileToArchive却是几乎相同的。 唯一的区别就是多了一个新函数dwQuality。 参数设置后WAV将会被压缩。 不过不像mpqaddfiletoarchive的标准压缩, mpqaddwavtoarchive的wav压缩,实际上降低了wav的质量 。质量降低多少依赖于dwquality 。 如果您有一个音乐WAV而又想保证质量,那您就使用mawa_quality_high ,因为它最好的保留wav的质量;而同一个声音wav , mawa_quality_low通常是不行的,因为声调比较容易压缩失真; mawa_quality_medium往往是最有效的。 Deleting a File - MpqDeleteFile 删除文件函数 - MpqDeleteFile
少数情况下,您可能需要删除MPQ中的某个文件,那么您就需要用到MpqDeleteFile。 MpqDeleteFile能从已经打开的存档hMPQ中删除文件lpFileName。 不过这不是看上去那么简单的。 mpqdeletefile为lpfilename删除哈希和文件表项 ,使其无法进入。 但是除非文件在MPQ的physical end ,否则MpqDeleteFile无法清除内存。 这就是说,通常情况下MPQ文件是不会减小大小的。 不过空间可以添加新文件循环再利用。稍后为您介绍MpqAddFileToArchive 和MpqAddWAVToArchive. 就像STROM和STAREDIT几乎所有的功能一样,调用MpqDeleteFile成功就返回TURE,失败就返回FALSE。 失败的话,您可以调用GetLastError去获得一些关于失败原因的信息(假如有的话)。 在这种情况下GetLastError是惊人的有效。 因为几乎只有一种会造成MpqDeleteFile失败的原因(除了hMPQ无效): 该文件lpFileName不存在于MPQ,GetLastError将返回MPQ_ERROR_FILE_NOT_FOUND。 Renaming a File - MpqRenameFile 重命名文件函数- MpqRenameFile
在我慢慢研究MPQ后发现StarEdit缺少一对非常有用的功能。 而且那个时候我对与STROM和StarEdit的运做有了相当的了解,所以我决定写个自己的功能。 MpqRenameFile 非常的简单;它重新把hMPQ中的文件名 由lpOldFileName变为lpNewFileName. mpqrenamefile返回给您的依旧是简单的信息。 mpqrenamefile返回true就成功,FLASH失败,并让您调用getlasterror获得失败的原因 。 如果hMPQ HANDLE 是NULL或无效,lpoldfilename或lpnewfilename是Null , getlasterror将返回error_invalid_parameter 。 如果该文件lpoldfilename不存在于MPQ hMPQ中, getlasterror将返回mpq_error_file_not_found 。 如果该文件lpnewfilename已经存在mpq中 , getlasterror将返回mpq_error_already_exists 。 Compacting an MPQ - MpqCompactArchive
原文作者就写了这么多。。 |