herostats 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.travis.yml +4 -0
  4. data/CODE_OF_CONDUCT.md +13 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE +7 -0
  7. data/README.md +34 -0
  8. data/Rakefile +10 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +7 -0
  11. data/herostats.gemspec +33 -0
  12. data/lib/heroprotocol/LICENSE +19 -0
  13. data/lib/heroprotocol/README.md +91 -0
  14. data/lib/heroprotocol/__init__.py +0 -0
  15. data/lib/heroprotocol/decoders.py +311 -0
  16. data/lib/heroprotocol/heroprotocol.py +136 -0
  17. data/lib/heroprotocol/mpyq/LICENSE +22 -0
  18. data/lib/heroprotocol/mpyq/MANIFEST.in +3 -0
  19. data/lib/heroprotocol/mpyq/README.md +202 -0
  20. data/lib/heroprotocol/mpyq/__init__.py +0 -0
  21. data/lib/heroprotocol/mpyq/mpyq.py +406 -0
  22. data/lib/heroprotocol/mpyq/setup.py +33 -0
  23. data/lib/heroprotocol/protocol29406.py +491 -0
  24. data/lib/heroprotocol/protocol30414.py +492 -0
  25. data/lib/heroprotocol/protocol30509.py +492 -0
  26. data/lib/heroprotocol/protocol30829.py +492 -0
  27. data/lib/heroprotocol/protocol30948.py +492 -0
  28. data/lib/heroprotocol/protocol31090.py +492 -0
  29. data/lib/heroprotocol/protocol31360.py +494 -0
  30. data/lib/heroprotocol/protocol31566.py +494 -0
  31. data/lib/heroprotocol/protocol31726.py +494 -0
  32. data/lib/heroprotocol/protocol31948.py +494 -0
  33. data/lib/heroprotocol/protocol32120.py +494 -0
  34. data/lib/heroprotocol/protocol32253.py +494 -0
  35. data/lib/heroprotocol/protocol32455.py +496 -0
  36. data/lib/heroprotocol/protocol32524.py +496 -0
  37. data/lib/heroprotocol/protocol33182.py +499 -0
  38. data/lib/heroprotocol/protocol33353.py +499 -0
  39. data/lib/heroprotocol/protocol33684.py +502 -0
  40. data/lib/heroprotocol/protocol34053.py +502 -0
  41. data/lib/heroprotocol/protocol34190.py +502 -0
  42. data/lib/heroprotocol/protocol34659.py +502 -0
  43. data/lib/heroprotocol/protocol34846.py +502 -0
  44. data/lib/heroprotocol/protocol35360.py +502 -0
  45. data/lib/heroprotocol/protocol35529.py +502 -0
  46. data/lib/heroprotocol/protocol35634.py +502 -0
  47. data/lib/heroprotocol/protocol35702.py +502 -0
  48. data/lib/heroprotocol/protocol36144.py +502 -0
  49. data/lib/heroprotocol/protocol36280.py +502 -0
  50. data/lib/heroprotocol/protocol36359.py +502 -0
  51. data/lib/heroprotocol/protocol36536.py +502 -0
  52. data/lib/heroprotocol/protocol36693.py +502 -0
  53. data/lib/heroprotocol/protocol37069.py +507 -0
  54. data/lib/heroprotocol/protocol37117.py +507 -0
  55. data/lib/heroprotocol/protocol37274.py +507 -0
  56. data/lib/heroprotocol/protocol37351.py +507 -0
  57. data/lib/heroprotocol/protocol37569.py +507 -0
  58. data/lib/heroprotocol/protocol37795.py +507 -0
  59. data/lib/heroprotocol/protocol38236.py +508 -0
  60. data/lib/heroprotocol/protocol38500.py +508 -0
  61. data/lib/heroprotocol/protocol38593.py +508 -0
  62. data/lib/heroprotocol/protocol38793.py +508 -0
  63. data/lib/heroprotocol/protocol39015.py +508 -0
  64. data/lib/heroprotocol/protocol39153.py +508 -0
  65. data/lib/heroprotocol/protocol39271.py +508 -0
  66. data/lib/heroprotocol/protocol39445.py +508 -0
  67. data/lib/heroprotocol/protocol39595.py +508 -0
  68. data/lib/heroprotocol/protocol39709.py +508 -0
  69. data/lib/heroprotocol/protocol39951.py +508 -0
  70. data/lib/heroprotocol/protocol40087.py +508 -0
  71. data/lib/heroprotocol/protocol40322.py +508 -0
  72. data/lib/heroprotocol/protocol40336.py +526 -0
  73. data/lib/heroprotocol/protocol40431.py +526 -0
  74. data/lib/heroprotocol/protocol40697.py +526 -0
  75. data/lib/heroprotocol/protocol40798.py +526 -0
  76. data/lib/herostats.rb +10 -0
  77. data/lib/herostats/exp_breakdown.rb +24 -0
  78. data/lib/herostats/game.rb +25 -0
  79. data/lib/herostats/init_data_parser.rb +45 -0
  80. data/lib/herostats/player.rb +25 -0
  81. data/lib/herostats/replay_parser.rb +34 -0
  82. data/lib/herostats/team.rb +10 -0
  83. data/lib/herostats/tracker_events_parser.rb +158 -0
  84. data/lib/herostats/version.rb +3 -0
  85. metadata +171 -0
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env python
2
+ #
3
+ # Copyright (c) 2015 Blizzard Entertainment
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+
23
+ import sys
24
+ import argparse
25
+ import pprint
26
+ import json
27
+
28
+ from mpyq import mpyq
29
+ import protocol29406
30
+
31
+ class EventLogger:
32
+ def __init__(self):
33
+ self._event_stats = {}
34
+
35
+ def log(self, output, event):
36
+ # update stats
37
+ if '_event' in event and '_bits' in event:
38
+ stat = self._event_stats.get(event['_event'], [0, 0])
39
+ stat[0] += 1 # count of events
40
+ stat[1] += event['_bits'] # count of bits
41
+ self._event_stats[event['_event']] = stat
42
+ # write structure
43
+ if args.json:
44
+ s = json.dumps(event, encoding="ISO-8859-1");
45
+ print(s);
46
+ else:
47
+ pprint.pprint(event, stream=output)
48
+
49
+ def log_stats(self, output):
50
+ for name, stat in sorted(self._event_stats.iteritems(), key=lambda x: x[1][1]):
51
+ print >> output, '"%s", %d, %d,' % (name, stat[0], stat[1] / 8)
52
+
53
+
54
+ if __name__ == '__main__':
55
+ parser = argparse.ArgumentParser()
56
+ parser.add_argument('replay_file', help='.StormReplay file to load')
57
+ parser.add_argument("--gameevents", help="print game events",
58
+ action="store_true")
59
+ parser.add_argument("--messageevents", help="print message events",
60
+ action="store_true")
61
+ parser.add_argument("--trackerevents", help="print tracker events",
62
+ action="store_true")
63
+ parser.add_argument("--attributeevents", help="print attributes events",
64
+ action="store_true")
65
+ parser.add_argument("--header", help="print protocol header",
66
+ action="store_true")
67
+ parser.add_argument("--details", help="print protocol details",
68
+ action="store_true")
69
+ parser.add_argument("--initdata", help="print protocol initdata",
70
+ action="store_true")
71
+ parser.add_argument("--stats", help="print stats",
72
+ action="store_true")
73
+ parser.add_argument("--json", help="protocol information is printed in json format.",
74
+ action="store_true")
75
+ args = parser.parse_args()
76
+
77
+ archive = mpyq.MPQArchive(args.replay_file)
78
+
79
+ logger = EventLogger()
80
+ logger.args = args;
81
+
82
+ # Read the protocol header, this can be read with any protocol
83
+ contents = archive.header['user_data_header']['content']
84
+ header = protocol29406.decode_replay_header(contents)
85
+ if args.header:
86
+ logger.log(sys.stdout, header)
87
+
88
+ # The header's baseBuild determines which protocol to use
89
+ baseBuild = header['m_version']['m_baseBuild']
90
+ try:
91
+ protocol = __import__('protocol%s' % (baseBuild,))
92
+ except:
93
+ print >> sys.stderr, 'Unsupported base build: %d' % baseBuild
94
+ sys.exit(1)
95
+
96
+ # Print protocol details
97
+ if args.details:
98
+ contents = archive.read_file('replay.details')
99
+ details = protocol.decode_replay_details(contents)
100
+ logger.log(sys.stdout, details)
101
+
102
+ # Print protocol init data
103
+ if args.initdata:
104
+ contents = archive.read_file('replay.initData')
105
+ initdata = protocol.decode_replay_initdata(contents)
106
+ logger.log(sys.stdout, initdata['m_syncLobbyState']['m_gameDescription']['m_cacheHandles'])
107
+ logger.log(sys.stdout, initdata)
108
+
109
+ # Print game events and/or game events stats
110
+ if args.gameevents:
111
+ contents = archive.read_file('replay.game.events')
112
+ for event in protocol.decode_replay_game_events(contents):
113
+ logger.log(sys.stdout, event)
114
+
115
+ # Print message events
116
+ if args.messageevents:
117
+ contents = archive.read_file('replay.message.events')
118
+ for event in protocol.decode_replay_message_events(contents):
119
+ logger.log(sys.stdout, event)
120
+
121
+ # Print tracker events
122
+ if args.trackerevents:
123
+ if hasattr(protocol, 'decode_replay_tracker_events'):
124
+ contents = archive.read_file('replay.tracker.events')
125
+ for event in protocol.decode_replay_tracker_events(contents):
126
+ logger.log(sys.stdout, event)
127
+
128
+ # Print attributes events
129
+ if args.attributeevents:
130
+ contents = archive.read_file('replay.attributes.events')
131
+ attributes = protocol.decode_replay_attributes_events(contents)
132
+ logger.log(sys.stdout, attributes)
133
+
134
+ # Print stats
135
+ if args.stats:
136
+ logger.log_stats(sys.stderr)
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2010, 2011 Aku Kotkavuo. All rights reserved.
2
+
3
+ Redistribution and use in source and binary forms, with or without
4
+ modification, are permitted provided that the following conditions are met:
5
+
6
+ 1. Redistributions of source code must retain the above copyright
7
+ notice, this list of conditions and the following disclaimer.
8
+
9
+ 2. Redistributions in binary form must reproduce the above copyright
10
+ notice, this list of conditions and the following disclaimer in the
11
+ documentation and/or other materials provided with the distribution.
12
+
13
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
17
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
20
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,3 @@
1
+ include README.md
2
+ include LICENSE
3
+ include MANIFEST.in
@@ -0,0 +1,202 @@
1
+ # mpyq
2
+
3
+ mpyq is a Python library for reading MPQ (MoPaQ) archives used in many of
4
+ Blizzard's games. It was originally developed for data mining Starcraft II
5
+ replay files.
6
+
7
+ In addition to being a library, mpyq also has a command line interface that
8
+ exposes some of the library's core functionality such as extracting archives.
9
+
10
+ At this early stage in development only files compressed with DEFLATE or bzip2
11
+ are uncompressed. This means that this library can not be used to extract most
12
+ big game asset archives that Blizzard's games use. More compression formats
13
+ will be supported in the future.
14
+
15
+ Also, as mpyq is so far pure Python code, it might be unfeasible to try to
16
+ extract very large MPQ archives, even if all the compression methods used
17
+ inside the archive were supported.
18
+
19
+ Note that listing files inside an archive does not require full extraction.
20
+ You can safely take a peek inside any MPQ archive with this library.
21
+
22
+ ## Installation
23
+
24
+ A stable version of mpyq is available from PyPI and can be installed with
25
+ either `easy_install` or `pip`.
26
+
27
+ $ easy_install mpyq
28
+ $ pip install mpyq
29
+
30
+ mpyq can be installed manually with the included setup.py script.
31
+
32
+ $ python setup.py install
33
+
34
+ Running this command will install mpyq both as a library and a stand-alone
35
+ script that can be run from anywhere, provided that you have added Python's
36
+ bin directory to your PATH environment variable.
37
+
38
+ Alternative way to install mpyq is to clone this git repository somewhere on
39
+ your filesystem and then either adjust your PYTHONPATH environment variable to
40
+ point to the directory that contains the repository or create a symbolic link
41
+ to your Python's site-packages directory pointing at the repository.
42
+
43
+ Note that the command line interface part of mpyq uses the argparse module,
44
+ which was included into Python's standard library in version 2.7. If you
45
+ didn't install mpyq from PyPI and you wish to use the command line interface
46
+ part with Python 2.6, you should install argparse from PyPI manually.
47
+
48
+ ## Usage
49
+
50
+ ### As a library
51
+
52
+ >>> from mpyq import MPQArchive
53
+ >>> archive = MPQArchive('game.SC2Replay')
54
+
55
+ Now you have a MPQArchive object of the file you opened. One common thing
56
+ to do now is to extract the files from the archive.
57
+
58
+ >>> files = archive.extract()
59
+
60
+ This will extract and return the archive's contents in memory. Be advised
61
+ that you might not want to do this with multi-gigabyte MPQ files from
62
+ World of Warcraft, for example.
63
+
64
+ Files inside the archive can be also extracted and written to disk.
65
+
66
+ >>> archive.extract_to_disk()
67
+
68
+ If you want to skip reading the (listfile) inside the archive, you can do
69
+ so by passing `listfile=False` to the constructor.
70
+
71
+ >>> archive = MPQArchive('bad_listfile.SC2Replay', listfile=False)
72
+
73
+ This might be required if the (listfile) is encrypted or has been tampered
74
+ with. Note that you can't list files or extract the whole archive if you do
75
+ this -- you need to know in advance which files you want to read.
76
+
77
+ >>> archive.read('replay.details')
78
+ '\x05\x1c\x00\x04\x01\x00\x04\x05...'
79
+
80
+ For more information, consult `help(mpyq)` in your Python console.
81
+
82
+ ### From the command line
83
+
84
+ usage: mpyq.py [-h] [-I] [-H] [-b] [-s] [-t] [-x] file
85
+
86
+ mpyq reads and extracts MPQ archives.
87
+
88
+ positional arguments:
89
+ file path to the archive
90
+
91
+ optional arguments:
92
+ -h, --help show this help message and exit
93
+ -I, --headers print header information from the archive
94
+ -H, --hash-table print hash table
95
+ -b, --block-table print block table
96
+ -s, --skip-listfile skip reading (listfile)
97
+ -t, --list-files list files inside the archive
98
+ -x, --extract extract files from the archive
99
+
100
+ You can extract all the files inside the archive with `-x/--extract`.
101
+
102
+ $ mpyq -x game.SC2Replay
103
+
104
+ This will create a directory called 'game' with the files inside.
105
+
106
+ You can print the header information from a given archive with `-I/--headers`.
107
+
108
+ $ mpyq -I game.SC2Replay
109
+ MPQ archive header
110
+ ------------------
111
+ magic 'MPQ\x1a'
112
+ header_size 44
113
+ arhive_size 19801
114
+ format_version 1
115
+ sector_size_shift 3
116
+ hash_table_offset 19385
117
+ block_table_offset 19641
118
+ hash_table_entries 16
119
+ block_table_entries 10
120
+ extended_block_table_offset 0
121
+ hash_table_offset_high 0
122
+ block_table_offset_high 0
123
+ offset 1024
124
+
125
+ MPQ user data header
126
+ --------------------
127
+ magic 'MPQ\x1b'
128
+ user_data_size 512
129
+ mpq_header_offset 1024
130
+ user_data_header_size 60
131
+ content '\x05\x08\x00\x02,StarCraft II replay\x1b
132
+ 11\x02\x05\x0c\x00\t\x02\x02\t\x00\x04\t
133
+ (\x06\t\x00\x08\t\xc8\xfa\x01\n\t\xc8\xf
134
+ a\x01\x04\t\x04\x06\t\xa2\x99\x01'
135
+
136
+ You can display the archive's hash table with `-H/--hash-table`.
137
+
138
+ $ mpyq -H game.SC2Replay
139
+ MPQ archive hash table
140
+ ----------------------
141
+ Hash A Hash B Locl Plat BlockIdx
142
+ D38437CB 07DFEAEC 0000 0000 00000009
143
+ AAC2A54B F4762B95 0000 0000 00000002
144
+ FFFFFFFF FFFFFFFF FFFF FFFF FFFFFFFF
145
+ FFFFFFFF FFFFFFFF FFFF FFFF FFFFFFFF
146
+ FFFFFFFF FFFFFFFF FFFF FFFF FFFFFFFF
147
+ C9E5B770 3B18F6B6 0000 0000 00000005
148
+ 343C087B 278E3682 0000 0000 00000004
149
+ 3B2B1EA0 B72EF057 0000 0000 00000006
150
+ 5A7E8BDC FF253F5C 0000 0000 00000001
151
+ FD657910 4E9B98A7 0000 0000 00000008
152
+ D383C29C EF402E92 0000 0000 00000000
153
+ FFFFFFFF FFFFFFFF FFFF FFFF FFFFFFFF
154
+ FFFFFFFF FFFFFFFF FFFF FFFF FFFFFFFF
155
+ FFFFFFFF FFFFFFFF FFFF FFFF FFFFFFFF
156
+ 1DA8B0CF A2CEFF28 0000 0000 00000007
157
+ 31952289 6A5FFAA3 0000 0000 00000003
158
+
159
+ You can display the archive's block table with `-b/--block-table`.
160
+
161
+ $ mpyq -b game.SC2Replay
162
+ MPQ archive block table
163
+ -----------------------
164
+ Offset ArchSize RealSize Flags
165
+ 0000002C 443 443 81000200
166
+ 000001E7 609 1082 81000200
167
+ 00000448 16077 42859 81000200
168
+ 00004315 94 94 81000200
169
+ 00004373 96 96 81000200
170
+ 000043D3 591 765 81000200
171
+ 00004622 802 1444 81000200
172
+ 00004944 247 580 81000200
173
+ 00004A3B 120 164 81000200
174
+ 00004AB3 262 288 81000200
175
+
176
+ You can list all files inside the archive with `-t/--list-files`.
177
+
178
+ $ mpyq -t game.SC2Replay
179
+ replay.attributes.events 580 bytes
180
+ replay.details 443 bytes
181
+ replay.game.events 42859 bytes
182
+ replay.initData 1082 bytes
183
+ replay.load.info 96 bytes
184
+ replay.message.events 94 bytes
185
+ replay.smartcam.events 1444 bytes
186
+ replay.sync.events 765 bytes
187
+
188
+ You can skip reading the listfile with `-s/--skip-listfile`. This might be
189
+ necessarry if the listfile is encrypted or corrupted. Note that you cannot
190
+ list files or extract the whole archive without the listfile.
191
+
192
+ ## References
193
+
194
+ The following two documents were used as references for the MPQ format:
195
+
196
+ * [http://www.zezula.net/en/mpq/mpqformat.html](http://www.zezula.net/en/mpq/mpqformat.html)
197
+ * [http://wiki.devklog.net/index.php?title=The_MoPaQ_Archive_Format](http://wiki.devklog.net/index.php?title=The_MoPaQ_Archive_Format)
198
+
199
+
200
+ ## Copyright
201
+
202
+ Copyright 2010, 2011 Aku Kotkavuo. See LICENSE for details.
File without changes
@@ -0,0 +1,406 @@
1
+ #!/usr/bin/env python
2
+ # coding: utf-8
3
+
4
+ """
5
+ mpyq is a Python library for reading MPQ (MoPaQ) archives.
6
+ """
7
+
8
+ import bz2
9
+ import cStringIO
10
+ import os
11
+ import struct
12
+ import zlib
13
+ from collections import namedtuple
14
+
15
+
16
+ __author__ = "Aku Kotkavuo"
17
+ __version__ = "0.2.0"
18
+
19
+
20
+ MPQ_FILE_IMPLODE = 0x00000100
21
+ MPQ_FILE_COMPRESS = 0x00000200
22
+ MPQ_FILE_ENCRYPTED = 0x00010000
23
+ MPQ_FILE_FIX_KEY = 0x00020000
24
+ MPQ_FILE_SINGLE_UNIT = 0x01000000
25
+ MPQ_FILE_DELETE_MARKER = 0x02000000
26
+ MPQ_FILE_SECTOR_CRC = 0x04000000
27
+ MPQ_FILE_EXISTS = 0x80000000
28
+
29
+ MPQFileHeader = namedtuple('MPQFileHeader',
30
+ '''
31
+ magic
32
+ header_size
33
+ archive_size
34
+ format_version
35
+ sector_size_shift
36
+ hash_table_offset
37
+ block_table_offset
38
+ hash_table_entries
39
+ block_table_entries
40
+ '''
41
+ )
42
+ MPQFileHeader.struct_format = '<4s2I2H4I'
43
+
44
+ MPQFileHeaderExt = namedtuple('MPQFileHeaderExt',
45
+ '''
46
+ extended_block_table_offset
47
+ hash_table_offset_high
48
+ block_table_offset_high
49
+ '''
50
+ )
51
+ MPQFileHeaderExt.struct_format = 'q2h'
52
+
53
+ MPQUserDataHeader = namedtuple('MPQUserDataHeader',
54
+ '''
55
+ magic
56
+ user_data_size
57
+ mpq_header_offset
58
+ user_data_header_size
59
+ '''
60
+ )
61
+ MPQUserDataHeader.struct_format = '<4s3I'
62
+
63
+ MPQHashTableEntry = namedtuple('MPQHashTableEntry',
64
+ '''
65
+ hash_a
66
+ hash_b
67
+ locale
68
+ platform
69
+ block_table_index
70
+ '''
71
+ )
72
+ MPQHashTableEntry.struct_format = '2I2HI'
73
+
74
+ MPQBlockTableEntry = namedtuple('MPQBlockTableEntry',
75
+ '''
76
+ offset
77
+ archived_size
78
+ size
79
+ flags
80
+ '''
81
+ )
82
+ MPQBlockTableEntry.struct_format = '4I'
83
+
84
+
85
+ class MPQArchive(object):
86
+
87
+ def __init__(self, filename, listfile=True):
88
+ """Create a MPQArchive object.
89
+
90
+ You can skip reading the listfile if you pass listfile=False
91
+ to the constructor. The 'files' attribute will be unavailable
92
+ if you do this.
93
+ """
94
+ if hasattr(filename, 'read'):
95
+ self.file = filename
96
+ else:
97
+ self.file = open(filename, 'rb')
98
+ self.header = self.read_header()
99
+ self.hash_table = self.read_table('hash')
100
+ self.block_table = self.read_table('block')
101
+ if listfile:
102
+ self.files = self.read_file('(listfile)').splitlines()
103
+ else:
104
+ self.files = None
105
+
106
+ def read_header(self):
107
+ """Read the header of a MPQ archive."""
108
+
109
+ def read_mpq_header(offset=None):
110
+ if offset:
111
+ self.file.seek(offset)
112
+ data = self.file.read(32)
113
+ header = MPQFileHeader._make(
114
+ struct.unpack(MPQFileHeader.struct_format, data))
115
+ header = header._asdict()
116
+ if header['format_version'] == 1:
117
+ data = self.file.read(12)
118
+ extended_header = MPQFileHeaderExt._make(
119
+ struct.unpack(MPQFileHeaderExt.struct_format, data))
120
+ header.update(extended_header._asdict())
121
+ return header
122
+
123
+ def read_mpq_user_data_header():
124
+ data = self.file.read(16)
125
+ header = MPQUserDataHeader._make(
126
+ struct.unpack(MPQUserDataHeader.struct_format, data))
127
+ header = header._asdict()
128
+ header['content'] = self.file.read(header['user_data_header_size'])
129
+ return header
130
+
131
+ magic = self.file.read(4)
132
+ self.file.seek(0)
133
+
134
+ if magic == 'MPQ\x1a':
135
+ header = read_mpq_header()
136
+ header['offset'] = 0
137
+ elif magic == 'MPQ\x1b':
138
+ user_data_header = read_mpq_user_data_header()
139
+ header = read_mpq_header(user_data_header['mpq_header_offset'])
140
+ header['offset'] = user_data_header['mpq_header_offset']
141
+ header['user_data_header'] = user_data_header
142
+
143
+ return header
144
+
145
+ def read_table(self, table_type):
146
+ """Read either the hash or block table of a MPQ archive."""
147
+
148
+ if table_type == 'hash':
149
+ entry_class = MPQHashTableEntry
150
+ elif table_type == 'block':
151
+ entry_class = MPQBlockTableEntry
152
+ else:
153
+ raise ValueError("Invalid table type.")
154
+
155
+ table_offset = self.header['%s_table_offset' % table_type]
156
+ table_entries = self.header['%s_table_entries' % table_type]
157
+ key = self._hash('(%s table)' % table_type, 'TABLE')
158
+
159
+ self.file.seek(table_offset + self.header['offset'])
160
+ data = self.file.read(table_entries * 16)
161
+ data = self._decrypt(data, key)
162
+
163
+ def unpack_entry(position):
164
+ entry_data = data[position*16:position*16+16]
165
+ return entry_class._make(
166
+ struct.unpack(entry_class.struct_format, entry_data))
167
+
168
+ return [unpack_entry(i) for i in range(table_entries)]
169
+
170
+ def get_hash_table_entry(self, filename):
171
+ """Get the hash table entry corresponding to a given filename."""
172
+ hash_a = self._hash(filename, 'HASH_A')
173
+ hash_b = self._hash(filename, 'HASH_B')
174
+ for entry in self.hash_table:
175
+ if (entry.hash_a == hash_a and entry.hash_b == hash_b):
176
+ return entry
177
+
178
+ def read_file(self, filename, force_decompress=False):
179
+ """Read a file from the MPQ archive."""
180
+
181
+ def decompress(data):
182
+ """Read the compression type and decompress file data."""
183
+ compression_type = ord(data[0])
184
+ if compression_type == 0:
185
+ return data
186
+ elif compression_type == 2:
187
+ return zlib.decompress(data[1:], 15)
188
+ elif compression_type == 16:
189
+ return bz2.decompress(data[1:])
190
+ else:
191
+ raise RuntimeError("Unsupported compression type.")
192
+
193
+ hash_entry = self.get_hash_table_entry(filename)
194
+ if hash_entry is None:
195
+ return None
196
+ block_entry = self.block_table[hash_entry.block_table_index]
197
+
198
+ # Read the block.
199
+ if block_entry.flags & MPQ_FILE_EXISTS:
200
+ if block_entry.archived_size == 0:
201
+ return None
202
+
203
+ offset = block_entry.offset + self.header['offset']
204
+ self.file.seek(offset)
205
+ file_data = self.file.read(block_entry.archived_size)
206
+
207
+ if block_entry.flags & MPQ_FILE_ENCRYPTED:
208
+ raise NotImplementedError("Encryption is not supported yet.")
209
+
210
+ if not block_entry.flags & MPQ_FILE_SINGLE_UNIT:
211
+ # File consist of many sectors. They all need to be
212
+ # decompressed separately and united.
213
+ sector_size = 512 << self.header['sector_size_shift']
214
+ sectors = block_entry.size / sector_size + 1
215
+ if block_entry.flags & MPQ_FILE_SECTOR_CRC:
216
+ crc = True
217
+ sectors += 1
218
+ else:
219
+ crc = False
220
+ positions = struct.unpack('<%dI' % (sectors + 1),
221
+ file_data[:4*(sectors+1)])
222
+ result = cStringIO.StringIO()
223
+ for i in range(len(positions) - (2 if crc else 1)):
224
+ sector = file_data[positions[i]:positions[i+1]]
225
+ if (block_entry.flags & MPQ_FILE_COMPRESS and
226
+ (force_decompress or block_entry.size > block_entry.archived_size)):
227
+ sector = decompress(sector)
228
+ result.write(sector)
229
+ file_data = result.getvalue()
230
+ else:
231
+ # Single unit files only need to be decompressed, but
232
+ # compression only happens when at least one byte is gained.
233
+ if (block_entry.flags & MPQ_FILE_COMPRESS and
234
+ (force_decompress or block_entry.size > block_entry.archived_size)):
235
+ file_data = decompress(file_data)
236
+
237
+ return file_data
238
+
239
+ def extract(self):
240
+ """Extract all the files inside the MPQ archive in memory."""
241
+ if self.files:
242
+ return dict((f, self.read_file(f)) for f in self.files)
243
+ else:
244
+ raise RuntimeError("Can't extract whole archive without listfile.")
245
+
246
+ def extract_to_disk(self):
247
+ """Extract all files and write them to disk."""
248
+ archive_name, extension = os.path.splitext(os.path.basename(self.file.name))
249
+ if not os.path.isdir(os.path.join(os.getcwd(), archive_name)):
250
+ os.mkdir(archive_name)
251
+ os.chdir(archive_name)
252
+ for filename, data in self.extract().items():
253
+ f = open(filename, 'wb')
254
+ f.write(data)
255
+ f.close()
256
+
257
+ def extract_files(self, *filenames):
258
+ """Extract given files from the archive to disk."""
259
+ for filename in filenames:
260
+ data = self.read_file(filename)
261
+ f = open(filename, 'wb')
262
+ f.write(data)
263
+ f.close()
264
+
265
+ def print_headers(self):
266
+ print "MPQ archive header"
267
+ print "------------------"
268
+ for key, value in self.header.iteritems():
269
+ if key == "user_data_header":
270
+ continue
271
+ print "{0:30} {1!r}".format(key, value)
272
+ if self.header.get('user_data_header'):
273
+ print
274
+ print "MPQ user data header"
275
+ print "--------------------"
276
+ for key, value in self.header['user_data_header'].iteritems():
277
+ print "{0:30} {1!r}".format(key, value)
278
+ print
279
+
280
+ def print_hash_table(self):
281
+ print "MPQ archive hash table"
282
+ print "----------------------"
283
+ print " Hash A Hash B Locl Plat BlockIdx"
284
+ for entry in self.hash_table:
285
+ print '%08X %08X %04X %04X %08X' % entry
286
+ print
287
+
288
+ def print_block_table(self):
289
+ print "MPQ archive block table"
290
+ print "-----------------------"
291
+ print " Offset ArchSize RealSize Flags"
292
+ for entry in self.block_table:
293
+ print '%08X %8d %8d %8X' % entry
294
+ print
295
+
296
+ def print_files(self):
297
+ if self.files:
298
+ print "Files"
299
+ print "-----"
300
+ width = max(len(name) for name in self.files) + 2
301
+ for filename in self.files:
302
+ hash_entry = self.get_hash_table_entry(filename)
303
+ block_entry = self.block_table[hash_entry.block_table_index]
304
+ print "{0:{width}} {1:>8} bytes".format(filename,
305
+ block_entry.size,
306
+ width=width)
307
+
308
+ def _hash(self, string, hash_type):
309
+ """Hash a string using MPQ's hash function."""
310
+ hash_types = {
311
+ 'TABLE_OFFSET': 0,
312
+ 'HASH_A': 1,
313
+ 'HASH_B': 2,
314
+ 'TABLE': 3
315
+ }
316
+ seed1 = 0x7FED7FED
317
+ seed2 = 0xEEEEEEEE
318
+
319
+ for ch in string:
320
+ ch = ord(ch.upper())
321
+ value = self.encryption_table[(hash_types[hash_type] << 8) + ch]
322
+ seed1 = (value ^ (seed1 + seed2)) & 0xFFFFFFFF
323
+ seed2 = ch + seed1 + seed2 + (seed2 << 5) + 3 & 0xFFFFFFFF
324
+
325
+ return seed1
326
+
327
+ def _decrypt(self, data, key):
328
+ """Decrypt hash or block table or a sector."""
329
+ seed1 = key
330
+ seed2 = 0xEEEEEEEE
331
+ result = cStringIO.StringIO()
332
+
333
+ for i in range(len(data) // 4):
334
+ seed2 += self.encryption_table[0x400 + (seed1 & 0xFF)]
335
+ seed2 &= 0xFFFFFFFF
336
+ value = struct.unpack("<I", data[i*4:i*4+4])[0]
337
+ value = (value ^ (seed1 + seed2)) & 0xFFFFFFFF
338
+
339
+ seed1 = ((~seed1 << 0x15) + 0x11111111) | (seed1 >> 0x0B)
340
+ seed1 &= 0xFFFFFFFF
341
+ seed2 = value + seed2 + (seed2 << 5) + 3 & 0xFFFFFFFF
342
+
343
+ result.write(struct.pack("<I", value))
344
+
345
+ return result.getvalue()
346
+
347
+ def _prepare_encryption_table():
348
+ """Prepare encryption table for MPQ hash function."""
349
+ seed = 0x00100001
350
+ crypt_table = {}
351
+
352
+ for i in range(256):
353
+ index = i
354
+ for j in range(5):
355
+ seed = (seed * 125 + 3) % 0x2AAAAB
356
+ temp1 = (seed & 0xFFFF) << 0x10
357
+
358
+ seed = (seed * 125 + 3) % 0x2AAAAB
359
+ temp2 = (seed & 0xFFFF)
360
+
361
+ crypt_table[index] = (temp1 | temp2)
362
+
363
+ index += 0x100
364
+
365
+ return crypt_table
366
+
367
+ encryption_table = _prepare_encryption_table()
368
+
369
+
370
+ def main():
371
+ import argparse
372
+ description = "mpyq reads and extracts MPQ archives."
373
+ parser = argparse.ArgumentParser(description=description)
374
+ parser.add_argument("file", action="store", help="path to the archive")
375
+ parser.add_argument("-I", "--headers", action="store_true", dest="headers",
376
+ help="print header information from the archive")
377
+ parser.add_argument("-H", "--hash-table", action="store_true",
378
+ dest="hash_table", help="print hash table"),
379
+ parser.add_argument("-b", "--block-table", action="store_true",
380
+ dest="block_table", help="print block table"),
381
+ parser.add_argument("-s", "--skip-listfile", action="store_true",
382
+ dest="skip_listfile", help="skip reading (listfile)"),
383
+ parser.add_argument("-t", "--list-files", action="store_true", dest="list",
384
+ help="list files inside the archive")
385
+ parser.add_argument("-x", "--extract", action="store_true", dest="extract",
386
+ help="extract files from the archive")
387
+ args = parser.parse_args()
388
+ if args.file:
389
+ if not args.skip_listfile:
390
+ archive = MPQArchive(args.file)
391
+ else:
392
+ archive = MPQArchive(args.file, listfile=False)
393
+ if args.headers:
394
+ archive.print_headers()
395
+ if args.hash_table:
396
+ archive.print_hash_table()
397
+ if args.block_table:
398
+ archive.print_block_table()
399
+ if args.list:
400
+ archive.print_files()
401
+ if args.extract:
402
+ archive.extract_to_disk()
403
+
404
+
405
+ if __name__ == '__main__':
406
+ main()