MPQ技术内幕__
翻譯前聲明:
本翻譯對于原文進行了適量刪節和修改。
本翻譯只做為學習參考使用,不得用于任何商業目的。
原文地址: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
原文作者就寫了這么多。。 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
總結
- 上一篇: Cocos画线
- 下一篇: java---interrupt、int