Skip to content

Commit 198aa3d

Browse files
committed
sc2reader now supports all production releases of Starcraft II. Well, parsing passes, lots of missing codes and scramble bytes abound. Also reorganizes the logic for handling the slowly evolving replay format
1 parent abc3ec0 commit 198aa3d

File tree

3 files changed

+169
-88
lines changed

3 files changed

+169
-88
lines changed

sc2reader/__init__.py

Lines changed: 49 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
11
import os
22

3-
try:
4-
from collections import OrderedDict
5-
except ImportError:
6-
from ordereddict import OrderedDict
7-
except ImportError:
8-
from sys import exit
9-
exit("OrderedDict required: Upgrade to python2.7 or `pip install ordereddict`")
10-
113
from mpyq import MPQArchive
124
from utils import ReplayBuffer, LITTLE_ENDIAN
135
from objects import Replay
@@ -42,13 +34,48 @@
4234
MessageProcessor(),
4335
RecorderProcessor(),
4436
]
45-
READERS = OrderedDict([
46-
('replay.initData', [ReplayInitDataReader()]),
47-
('replay.details', [ReplayDetailsReader()]),
48-
('replay.attributes.events', [AttributeEventsReader_17326(), AttributeEventsReader()]),
49-
('replay.message.events', [MessageEventsReader()]),
50-
('replay.game.events', [GameEventsReader()]),
51-
])
37+
38+
class ReaderMap(object):
39+
def __getitem__(self,key):
40+
if int(key) in (16117,16195,16223,16291):
41+
return {
42+
'replay.initData': ReplayInitDataReader(),
43+
'replay.details': ReplayDetailsReader(),
44+
'replay.attributes.events': AttributeEventsReader(),
45+
'replay.message.events': MessageEventsReader(),
46+
'replay.game.events': GameEventsReader_16291(),
47+
}
48+
49+
elif int(key) in (16561,16605,16755,16939):
50+
return {
51+
'replay.initData': ReplayInitDataReader(),
52+
'replay.details': ReplayDetailsReader(),
53+
'replay.attributes.events': AttributeEventsReader(),
54+
'replay.message.events': MessageEventsReader(),
55+
'replay.game.events': GameEventsReader(),
56+
}
57+
58+
elif int(key) in (17326,17682,17811,18092,18221,18317):
59+
return {
60+
'replay.initData': ReplayInitDataReader(),
61+
'replay.details': ReplayDetailsReader(),
62+
'replay.attributes.events': AttributeEventsReader_17326(),
63+
'replay.message.events': MessageEventsReader(),
64+
'replay.game.events': GameEventsReader(),
65+
}
66+
67+
elif int(key) in (18574,):
68+
return {
69+
'replay.initData': ReplayInitDataReader(),
70+
'replay.details': ReplayDetailsReader(),
71+
'replay.attributes.events': AttributeEventsReader_17326(),
72+
'replay.message.events': MessageEventsReader(),
73+
'replay.game.events': GameEventsReader_18574(),
74+
}
75+
else:
76+
raise KeyError(key)
77+
78+
READERS = ReaderMap()
5279

5380
def read_header(file):
5481
buffer = ReplayBuffer(file)
@@ -91,20 +118,15 @@ def __init__(self, parse=FULL, directory="", processors=[], debug=False, files=N
91118
def read(self, location):
92119
#account for the directory option
93120
if self.directory: location = os.path.join(self.directory,location)
94-
121+
95122
if not os.path.exists(location):
96123
raise ValueError("Location must exist")
97124

98125
#If its a directory, read each subfile/directory and combine the lists
99126
if os.path.isdir(location):
100-
replays = list()
101-
for filename in os.listdir(location):
102-
replay = self.read(os.path.join(location,filename))
103-
if isinstance(replay,list):
104-
replays.extend(replay)
105-
else:
106-
replays.append(replay)
107-
return replays
127+
read = lambda file: self.read(os.path.join(location,file))
128+
tolist = lambda x: [x] if isinstance(x,Replay) else x
129+
return sum(map(tolist,(read(x) for x in os.listdir(location))),[])
108130

109131
#The primary replay reading routine
110132
else:
@@ -118,14 +140,9 @@ def read(self, location):
118140
archive = MPQArchive(location,listfile=False)
119141

120142
#Extract and Parse the relevant files based on parse level
121-
for file,readers in READERS.iteritems():
122-
if file in self.files:
123-
for reader in readers:
124-
if reader.reads(replay.build):
125-
reader.read(ReplayBuffer(archive.read_file(file)),replay)
126-
break
127-
else:
128-
raise NotYetImplementedError("No parser was found that accepted the replay file;check configuration")
143+
for file in self.files:
144+
reader = READERS[replay.build][file]
145+
reader.read(ReplayBuffer(archive.read_file(file)),replay)
129146

130147
#Do cleanup and post processing
131148
for processor in self.processors:

sc2reader/parsers.py

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from sc2reader.objects import *
44
from sc2reader.utils import BIG_ENDIAN,LITTLE_ENDIAN
55
from collections import defaultdict
6+
67
class SetupParser(object):
78
def parse_join_event(self, buffer, frames, type, code, pid):
89
return PlayerJoinEvent(frames, pid, type, code)
@@ -63,6 +64,7 @@ def parse_ability_event(self, buffer, frames, type, code, pid):
6364
return LocationAbilityEvent(frames, pid, type, code, None, location)
6465

6566
elif flag in (0x04,0x07):
67+
print "Made it!"
6668
h = buffer.read_hex(2)
6769
hinge = buffer.read_byte()
6870
if hinge & 0x20:
@@ -83,15 +85,17 @@ def parse_ability_event(self, buffer, frames, type, code, pid):
8385
buffer.skip(10)
8486
return TargetAbilityEvent(frames, pid, type, code, ability, target)
8587

86-
elif atype < 0x10: #new to patch 1.3.3
88+
elif atype in (0x08,0x0a): #new to patch 1.3.3
8789
#10 bytes total, coordinates have a different format?
8890
#X coordinate definitely is the first byte, with (hopefully) y next
91+
print hex(flag)
8992
event = UnknownAbilityEvent(frames, pid, type, code, None)
9093
event.location1 = buffer.read_coordinate()
9194
buffer.skip(5)
9295
return event
93-
96+
9497
else:
98+
print hex(atype)
9599
print hex(buffer.cursor)
96100
raise TypeError()
97101

@@ -161,7 +165,108 @@ def read_resource(buffer):
161165
buffer.skip(8)
162166

163167
return ResourceTransferEvent(frames, pid, type, code, target, minerals, vespene)
168+
169+
class ActionParser_18574(ActionParser):
170+
def parse_ability_event(self, buffer, frames, type, code, pid):
171+
flag = buffer.read_byte()
172+
atype = buffer.read_byte()
164173

174+
if atype & 0x20: # command card
175+
end = buffer.peek(35)
176+
ability = buffer.read_byte() << 8 | buffer.read_byte()
177+
178+
if flag in (0x29, 0x19, 0x14): # cancels
179+
# creation autoid number / object id
180+
ability = ability << 8 | buffer.read_byte()
181+
created_id = buffer.read_object_id()
182+
# TODO : expose the id
183+
return AbilityEvent(frames, pid, type, code, ability)
184+
185+
else:
186+
ability_flags = buffer.shift(6)
187+
ability = ability << 8 | ability_flags
188+
189+
if ability_flags & 0x10:
190+
# ability(3), coordinates (4), ?? (4)
191+
location = buffer.read_coordinate()
192+
buffer.skip(4)
193+
return LocationAbilityEvent(frames, pid, type, code, ability, location)
194+
195+
elif ability_flags & 0x20:
196+
# ability(3), object id (4), object type (2), ?? (10)
197+
code = buffer.read_short() # code??
198+
obj_id = buffer.read_object_id()
199+
obj_type = buffer.read_object_type()
200+
target = (obj_id, obj_type,)
201+
switch = buffer.read_byte()
202+
buffer.read_hex(9)
203+
return TargetAbilityEvent(frames, pid, type, code, ability, target)
204+
205+
else:
206+
return AbilityEvent(frames,pid,type,code,None)
207+
208+
elif atype & 0x40: # location/move ??
209+
h = buffer.read_hex(2)
210+
hinge = buffer.read_byte()
211+
if hinge & 0x20:
212+
"\t%s - %s" % (hex(hinge),buffer.read_hex(9))
213+
elif hinge & 0x40:
214+
"\t%s - %s" % (hex(hinge),buffer.read_hex(18))
215+
elif hinge < 0x10:
216+
pass
217+
218+
return UnknownLocationAbilityEvent(frames, pid, type, code, None)
219+
220+
elif atype & 0x80: # right-click on target?
221+
# ability (2), object id (4), object type (2), ?? (10)
222+
ability = buffer.read_byte() << 8 | buffer.read_byte()
223+
obj_id = buffer.read_object_id()
224+
obj_type = buffer.read_object_type()
225+
target = (obj_id, obj_type,)
226+
buffer.skip(10)
227+
return TargetAbilityEvent(frames, pid, type, code, ability, target)
228+
229+
elif atype < 0x10: #new to patch 1.3.3, location now??
230+
#10 bytes total, coordinates have a different format?
231+
#X coordinate definitely is the first byte, with (hopefully) y next
232+
location = buffer.read_coordinate()
233+
buffer.skip(5)
234+
return LocationAbilityEvent(frames, pid, type, code, None, location)
235+
236+
else:
237+
print hex(atype)
238+
print hex(buffer.cursor)
239+
raise TypeError()
240+
241+
print "%s - %s" % (hex(atype),hex(flag))
242+
raise TypeError("Shouldn't be here EVER!")
243+
244+
class ActionParser_16291(ActionParser):
245+
def parse_ability_event(self, buffer, frames, type, code, pid):
246+
buffer.skip(7)
247+
switch = buffer.read_byte()
248+
if switch in (0x30,0x50):
249+
buffer.read_byte()
250+
buffer.skip(24)
251+
return AbilityEvent(frames, pid, type, code, None)
252+
253+
def parse_selection_event(self, buffer, frames, type, code, pid):
254+
bank = code >> 4
255+
selFlags = buffer.read_byte()
256+
dsuCount = buffer.read_byte()
257+
buffer.read(bits=dsuCount)
258+
259+
# <count> (<type_id>, <count>,)*
260+
object_types = [ (buffer.read_object_type(read_modifier=True), buffer.read_byte(), ) for i in range(buffer.read_byte()) ]
261+
# <count> (<object_id>,)*
262+
object_ids = [ buffer.read_object_id() for i in range(buffer.read_byte()) ]
263+
264+
# repeat types count times
265+
object_types = chain(*[[object_type,]*count for (object_type, count) in object_types])
266+
objects = zip(object_ids, object_types)
267+
268+
return AbilityEvent(frames, pid, type, code, None)
269+
165270
class Unknown2Parser(object):
166271
def parse_0206_event(self, buffer, frames, type, code, pid):
167272
buffer.skip(8)

sc2reader/readers.py

Lines changed: 13 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,11 @@
33
from sc2reader.parsers import *
44
from sc2reader.objects import *
55
from sc2reader.utils import LITTLE_ENDIAN, BIG_ENDIAN
6-
from sc2reader.utils import key_in_bases, timestamp_from_windows_time
6+
from sc2reader.utils import timestamp_from_windows_time
77

8-
#####################################################
9-
# Metaclass used to help enforce the usage contract
10-
#####################################################
11-
class MetaReader(type):
12-
def __new__(meta, class_name, bases, class_dict):
13-
if class_name != "Reader": #Parent class is exempt from checks
14-
assert 'file' in class_dict or key_in_bases('file',bases), \
15-
"%s must define the name of the file it reads" % class_name
16-
17-
assert 'reads' in class_dict or key_in_bases('reads',bases), \
18-
"%s must define the 'boolean reads(self, build)' member" % class_name
19-
20-
assert 'read' in class_dict or key_in_bases('read',bases), \
21-
"%s must define the 'void read(self, buffer, replay)' member" % class_name
22-
23-
return type.__new__(meta, class_name, bases, class_dict)
24-
25-
class Reader(object):
26-
__metaclass__ = MetaReader
27-
288
#################################################
299

30-
class ReplayInitDataReader(Reader):
31-
file = 'replay.initData'
32-
33-
def reads(self, build):
34-
return True
35-
10+
class ReplayInitDataReader(object):
3611
def read(self, buffer, replay):
3712

3813
# Game clients
@@ -57,11 +32,7 @@ def read(self, buffer, replay):
5732

5833
#################################################
5934

60-
class AttributeEventsReader(Reader):
61-
file = 'replay.attributes.events'
62-
def reads(self, build):
63-
return build < 17326
64-
35+
class AttributeEventsReader(object):
6536
def read(self, buffer, replay):
6637
self.load_header(replay, buffer)
6738

@@ -78,20 +49,12 @@ def load_header(self, replay, buffer):
7849
buffer.read_chars(4)
7950

8051
class AttributeEventsReader_17326(AttributeEventsReader):
81-
def reads(self, build):
82-
return build >= 17326
83-
8452
def load_header(self, replay, buffer):
8553
buffer.read_chars(5)
8654

8755
##################################################
8856

89-
class ReplayDetailsReader(Reader):
90-
file = 'replay.details'
91-
92-
def reads(self, build):
93-
return True
94-
57+
class ReplayDetailsReader(object):
9558
def read(self, buffer, replay):
9659
data = buffer.read_data_struct()
9760

@@ -143,12 +106,7 @@ def read(self, buffer, replay):
143106

144107
##################################################
145108

146-
class MessageEventsReader(Reader):
147-
file = 'replay.message.events'
148-
149-
def reads(self, build):
150-
return True
151-
109+
class MessageEventsReader(object):
152110
def read(self, buffer, replay):
153111
replay.messages, time = list(), 0
154112

@@ -183,10 +141,7 @@ def read(self, buffer, replay):
183141

184142
####################################################
185143

186-
class GameEventsBase(Reader):
187-
file = 'replay.game.events'
188-
def reads(self, build): return False
189-
144+
class GameEventsBase(object):
190145
def read(self, buffer, replay):
191146
replay.events, frames = list(), 0
192147

@@ -206,7 +161,7 @@ def read(self, buffer, replay):
206161
#print frames
207162
pid = buffer.shift(5)
208163
type, code = buffer.shift(3), buffer.read_byte()
209-
164+
#print "%s - %s" % (hex(type),hex(code))
210165
parser = PARSERS[type](code)
211166

212167
if parser == None:
@@ -250,6 +205,10 @@ def get_unknown4_parser(self, code):
250205
elif code & 0x0F == 0x0C: return self.parse_04XC_event
251206

252207
class GameEventsReader(GameEventsBase,Unknown2Parser,Unknown4Parser,ActionParser,SetupParser,CameraParser):
208+
pass
209+
210+
class GameEventsReader_16291(GameEventsBase,Unknown2Parser,Unknown4Parser,ActionParser_16291,SetupParser,CameraParser):
211+
pass
253212

254-
def reads(self, build):
255-
return True
213+
class GameEventsReader_18574(GameEventsBase,Unknown2Parser,Unknown4Parser,ActionParser_18574,SetupParser,CameraParser):
214+
pass

0 commit comments

Comments
 (0)