作者edwar (海边的野孩子)
看板DIABLO
标题MPQ 档格式 (未完成)
时间Tue Feb 7 23:30:24 2006
之前要用 mpq2k 看某些 mpq 档里的东西, 突然发现 mpq2k 几乎看不到什麽档
案了, 尤其在 d2exp 1.10 以後, patch_d2.mpq 让它找只能找到一个档案, 因
为 mpq2k 的作者已经不再做任何维护的动作了, 只能自己手动来, 所以才有想
了解这个档案格式的动力, 想看看能否突破 mpq2k 现有的限制.
(之前有想看, 但就是懒 (-,-))))).
目前针对 mpq2k 的一些嚐试已经告一段落, 想做的事也差不多了, 继续研究的
动力也没了, 所以把目前的东西整理一下, 希望对想研究的人有一些帮助.
* 这篇文章最有用的应该是参考资料, 其他都是废话 XD
这篇文章可自由转载, 但请保留出处(ptt.cc diablo 板)及作者(edwar).
参考资料
--------
Inside MoPaQ
http://www.campaigncreations.org/starcraft/inside_mopaq/
这是 mpq2k 作者所写的网页, 但是似乎没有再更新了.
(听说作者不希望有人烦他 mpq2k 相关的事)
从 Inside MoPaQ 提供的 Stormless MPQ Editor 连结拿到的 MpqView
原始码也很有用. 网页:
http://www.angelfire.com/sc/mpq/
MPQ 档的一些特性
----------------
* Blizzard 所使用的一种档案格式, 存放一些游戏的资讯档, 如声音、动画
* 用於将多个档案放进一个档案里
* 给一个档名, 可以检查一个 MPQ 档有没有那个档, 但是无演算法可从一个
MPQ 档逆推求得该 MPQ 档含有那些档案.
- MPQ 使用三个不同参数得到的 32bit hash 来取代档名这项资讯, 如果
有碰撞产生就认了, 所以无法逆推.
* 档名不分大小写
档头
----
offset |length | content
(hex) | |
---------|-------|-------------------
00000000 | 4 | 4D 50 51 1A ('MPQ'?)
00000004 | 4 | 20 00 00 00
00000008 | 4 | mpq file size ?
0000000C | 4 | unknown (00 00 03 00)
00000010 | 4 | htbl offset: hash table 位置
00000014 | 4 | btbl offset: block table 位置
00000018 | 4 | htbl_len: hash table 大小 / 能容纳的档案数
0000001C | 4 | btbl_len: 现有档案数
依 MpqView source code 来看, 4D 50 51 1A 字串只需位於 4-word aligned
开始的地方, 开档案後要先找到这个字串, 之後的其他资讯都是相对於此字串
的相对位置.
hash table (htbl)
-----------------
hash table 每笔资料含有四个 32bit 的值
offset |length | content
(hex) | |
---------|-------|-------------------
00000000 | 4 | hash2
00000004 | 4 | hash3
00000008 | 4 | unknown (好像都是 00 00 00 00 或 FF FF FF FF)
0000000C | 4 | 位於 block table 的那一笔 entry, 从 0 开始算
共有 htbl_len 笔记录, 如果该笔记录未被使用, 四个栏位似乎都会是
FF FF FF FF. 能容纳多少笔记录在该 MPQ 档建立之初就已决定, 只要开始加
档案就无法变动了, 这也是该档能够容纳的档案数.
若是实际上用 hex editor 去看 MPQ 档, hash table 应该是一堆看起来杂乱
无章的资料, 需要经过 decrypt 的过程转成正常的样子. 参考 encrypt /
decrypt 部份的说明.
由档名得到 block entry
======================
由档名可以求得三个 32bit hash (参考 hash function, 该处提到的 $scrc1
/ $scrc2 / $scrc3 即为所求, 分别当做这里的 hash1, hash2, hash3), 再
由这三个值找出位於 hash table 何处:
1. hash1 除以 htbl_len 的余数: 此数告诉你要从 hash table 的那一笔记
录开始找. (从零开始算)
2. 检查这笔记录是否已被使用, 可能是看 offset 0008h 的数值:
(a) 00000000h 表示有使用, 进行步骤 3.
(b) FFFFFFFFh 则是未被使用, 跳至步骤 4.
3. 检查 hash2 和 hash3 是否均符合:
(a) 是, 均符合: 找到档案了, offset 000Ch 为该档的 block entry.
(b) 否: 还未找到, 跳到步骤 4
4. 看看是否已经整个 hash table 的每笔记录都找过却找不到, 若是, 该档
不存在 -> 跳出.
5. 继续下一笔记录, 跳至步骤 2. 如果是在 hash table 最後一笔, 下一笔
记录则是第 0 笔记录.
由於 mpq 处理 hash collision(碰撞)的方式是继续往後找, 直至有空的位置
可以存放就储存. 如果碰撞後又有做 delete(删除)的动作, 可能会产生空洞.
所以步骤 2-(b) 发现该位置未使用也只是跳过去而已.
接下来
======
* 如果只是要知道该档名存在与否, 那以上的[由档名得到 block entry]应
该就可以了.
* 还要再得到那个档案的内容的话, 由此 block entry, 再到 block table
里找相关的档案储存资讯.
block table (btbl)
------------------
block table 每笔资料含有四个 32bit 的值
offset |length | content
(hex) | |
---------|-------|-------------------
00000000 | 4 | 存放的位置, 相对於档头开始(4D 50 51 1A)
00000004 | 4 | 压缩後的档案大小; 若未压缩, 则同未压缩的档案大小
00000008 | 4 | 未压缩的档案大小
0000000C | 4 | block 属性, 如有无压缩、有无编码等等
共有 btbl_len 笔记录, 从 0 开始算.
若用 hex editor 去看 MPQ 档, block table 应该像 hash table 一样, 看起
来像一堆杂乱无章的资料, 需要经过 decrypt 的过程转成正常的样子. 参考
encrypt / decrypt 部份的说明.
属性
====
目前找到的 block 属性值有:
------
00000000h
80000000h - 未压缩, 未编码
80000100h - from Diablo I MPQ, DCL 压缩
80000200h - Starcraft MPQ (or Diablo II), 要另外读压缩方式
80010200h
80030000h
80030200h
======
bit value | attribute
(hex) |
----------|----------------------------------------
00000000 | 不知, 也许无任何特性
00000100 | PACK, Diablo I MPQ, always compressed with DCL
00000200 | PACK, Starcraft MPQ (or Diablo II), 某处会记录压缩方式
00010000 | coded (是指 encrypted?)
00020000 | coded (是指 encrypted?)
80000000 | 不知
(各属性值意义在 MpqView 的原始档有更详尽的解释)
hash function
-------------
预备知识: hash function 概念
MPQ 档使用一个不错的单向 hash function 将档名转成三个 32bit hash, 此
hash function 也可以用不同的参数就改变输出的结果, 档案转换後的三个 hash
就是三个相异参数得到的.
底下使用 perl 程式码呈现如何产生 hash:
(C 的程式码请参考 MpqView 原始档)
--- get hash -------------
my @massive_base;
sub BuildBaseMassive {
my $s1;
my ($i, $j);
my $rem;
$rem = 0x100001;
for $i (0 .. 0x100-1) {
for $j (0 .. 4) {
$rem = ($rem*125+3) % 0x002AAAAB;
$s1 = ($rem & 0xFFFF) << 0x10;
$rem = ($rem*125+3) % 0x002AAAAB;
$s1 |= ($rem & 0xFFFF);
$massive_base[$i+0x100*$j] = $s1;
}
}
}
sub Crc ($$$) {
my $string = shift;
my $refMassive_base = shift;
my $massive_base_offset = shift;
my $byte;
my $crc = 0x7fed7fed;
my $s1 = 0xEEEEEEEE;
foreach (split(//, $string)) {
$byte = ord;
last if $byte == 0;
if ($byte > 0x60 && $byte < 0x7B) {
$byte -= 0x20;
}
$crc = $$refMassive_base[$massive_base_offset+$byte]^($crc+$s1);
$crc &= 0xFFFFFFFF;
$s1 += $crc+($s1<<5)+$byte+3;
$s1 &= 0xFFFFFFFF;
}
return $crc;
}
&BuildBaseMassive;
my $filename = 'This\is\a\test\file';
my ($scrc1, $scrc2, $scrc3);
$scrc1 = &Crc($filename, \@massive_base, 0);
$scrc2 = &Crc($filename, \@massive_base, 0x100);
$scrc3 = &Crc($filename, \@massive_base, 0x200);
=== get hash =============
$scrc1, $scrc2, $scrc3 就是由档名转换得到的三个 32bit hash, 分别由调整参数
为 0, 0x100, 0x200 得到.
encryption/decryption: hash & block table
-----------------------------------------
如果直接用 hex editors 去看 hash table 和 block table, 看到的资料应该是
很杂乱的, 这两个 table 在写进来 MPQ 档之前, 已经被 Encrypt 过了, 所以想
要看到正确的资料, 就要先 Decrypt.
以下是 Encrypt / Decrypt 的 perl code:
(此处 sub Decode 是 Decrypt 程式; sub Encode 是 Encrypt 程式)
(C 程式码 Decrypt 部分同样参考 MpqView 原始码)
--- Encrypt / Decrypt -----------
sub Decode ($$$$) {
my $refDataIn = shift;
my $refMassive_base = shift;
my $crc = shift;
my $lenght = shift;
my ($i, $dec);
my $s1 = 0xEEEEEEEE;
for $i (0 .. $lenght-1) {
$s1 += $$refMassive_base[0x400+($crc & 0xFF)];
$s1 &= 0xFFFFFFFF;
$dec = $$refDataIn[$i]^($s1+$crc);
$dec &= 0xFFFFFFFF;
$s1 += $dec+($s1<<5)+3;
$s1 &= 0xFFFFFFFF;
$$refDataIn[$i] = $dec;
$crc = ($crc>>0x0b) | ((0x11111111+(($crc^0x7FF)<<0x15)) &
0xFFFFFFFF);
}
}
sub Encode ($$$$) {
my $refDataIn = shift;
my $refMassive_base = shift;
my $crc = shift;
my $lenght = shift;
my ($i, $dec);
my $s1 = 0xEEEEEEEE;
for $i (0 .. $lenght-1) {
$s1 += $$refMassive_base[0x400+($crc & 0xFF)];
$s1 &= 0xFFFFFFFF;
$dec = $$refDataIn[$i];
$$refDataIn[$i] = ($dec^($s1+$crc)) & 0xFFFFFFFF;
$s1 += $dec+($s1<<5)+3;
$s1 &= 0xFFFFFFFF;
$crc = ($crc>>0x0b) | ((0x11111111+(($crc^0x7FF)<<0x15)) &
0xFFFFFFFF);
}
}
my $name_htable = "(hash table)";
my $name_btable = "(block table)";
seek(MPQ, $offset_mpq+$offset_htbl, 0);
read(MPQ, $tmp, 4*4*$lenght_htbl); @hash_table = unpack("L*", $tmp);
seek(MPQ, $offset_mpq+$offset_btbl, 0);
read(MPQ, $tmp, 4*4*$lenght_btbl); @block_table = unpack("L*", $tmp);
$tmp = &Crc($name_htable, \@massive_base, 0x300);
&Decode(\@hash_table, \@massive_base, $tmp, 4*$lenght_htbl);
$tmp = &Crc($name_btable, \@massive_base, 0x300);
&Decode(\@block_table, \@massive_base, $tmp, 4*$lenght_btbl);
# my @tblock_table = @block_table;
# $tmp = &Crc($name_btable, \@massive_base, 0x300);
# &Encode(\@tblock_table, \@massive_base, $tmp, 4*$lenght_btbl);
=== Encrypt / Decrypt ===========
@massive_base 和 &Crc(...) 跟 hash function 用的是一样的.
--
结束了 ^^
--
※ 发信站: 批踢踢实业坊(ptt.cc)
◆ From: 220.138.156.138
1F:推 Forcast:◆ 这一篇文章值 230 银 02/07 23:31
2F:推 jimmylee7775:推 02/07 23:31
3F:推 waterfantasy:我的眼睛花了 02/07 23:32
4F:推 red0210:眼睛都花了 02/07 23:33
5F:推 MeauD2:这篇 很难>"< 应属於大内高手流 02/07 23:33
6F:推 micado:我只看到一推程式设定码 02/07 23:34
7F:推 ericinttu:虽然我看不懂,但真是高手才办得到 Orz 02/07 23:32
8F:推 GGL:@@" 02/07 23:34
9F:推 pocky0511:第一行就看不懂 第二页就按END 02/07 23:35
10F:推 micado:不过我还是想说"帅ㄚ~老皮!" 02/07 23:36
11F:→ edwar:大量中英夹杂, 很难排版 >"< 我自己留的每段分开, 较不伤眼 02/07 23:35
12F:推 omarandy:完全...不懂ˊˋ 02/07 23:43
13F:推 ScorAlan:大师让小弟拜一下Orz 02/08 00:00
14F:推 Joybo:我走到了Programming版了吗 02/08 00:17
15F:推 Joybo:没学过perl,很好奇他怎知道Encode / Decode的方式,组语? 02/08 00:21
16F:→ edwar:perl 的部分是留着我自己以後要看的 XD 02/08 00:24
17F:→ edwar:Encode/Decode 也许是 trace 出来的? 02/08 00:24
18F:→ edwar:楼上有人看不懂没关系, 只要有人能看懂再写好用的工具程式 02/08 00:26
19F:→ edwar:就好了 XD 02/08 00:27