Skip to content

Commit bb58a9f

Browse files
committed
Fixes parsing of replays taken by spectators and improves 1.2 support
1 parent 84ad88e commit bb58a9f

File tree

4 files changed

+115
-61
lines changed

4 files changed

+115
-61
lines changed

sc2reader/eventparsers.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,11 @@ def load(self,event,bytes):
448448
class UnknownEventParser_04C6(object):
449449
def load(self,event,bytes):
450450
event.name = 'unknown04C6'
451-
event.bytes += bytes.skip(16,byteCode=True)
451+
event.bytes += bytes.peek(16)
452+
block1 = bytes.getBig(4)
453+
block2 = bytes.getBig(4)
454+
block3 = bytes.getBig(4)
455+
block4 = bytes.getBig(4)
452456
return event
453457

454458
class UnknownEventParser_041C(object):
@@ -457,10 +461,11 @@ def load(self,event,bytes):
457461
event.bytes += bytes.skip(15,byteCode=True)
458462
return event
459463

460-
class UnknownEventParser_0418or87(object):
464+
class UnknownEventParser_0487(object):
461465
def load(self,event,bytes):
462466
event.name = 'unknown0418-87'
463-
event.bytes += bytes.skip(4,byteCode=True)
467+
event.data, databytes = bytes.getBig(4,byteCode=True) #Always 00 00 00 01??
468+
event.bytes += databytes
464469
return event
465470

466471
class UnknownEventParser_0400(object):
@@ -469,9 +474,10 @@ def load(self,event,bytes):
469474
event.bytes += bytes.skip(10,byteCode=True)
470475
return event
471476

472-
class UnknownEventParser_043C(object):
477+
class UnknownEventParser_04XC(object):
473478
def load(self,event,bytes):
474-
event.name = 'unknown043C'
479+
event.name = 'unknown04XC'
480+
print bytes.peek(20)
475481
return event
476482

477483
#####################################################

sc2reader/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class ParseError(Exception):
2+
def __init__(self,message,replay,event,bytes):
3+
self.message = message
4+
self.replay = replay
5+
self.event = event
6+
self.bytes = bytes

sc2reader/parsers.py

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
from objects import Attribute,Message,Player,Event
55
from eventparsers import *
66
from utils import ByteStream
7+
from exceptions import ParseError
8+
9+
from pprint import PrettyPrinter
10+
pprint = PrettyPrinter(indent=2).pprint
711

812
#################################################
913
# Parser Dispatch Functions
@@ -148,9 +152,13 @@ def load(self,replay,filecontents):
148152
replay.messages.append(Message(time,playerId,target,text))
149153

150154
recorders = [player for player in replay.players if player and player.recorder==True]
151-
if len(recorders) != 1:
155+
if len(recorders) >1:
152156
raise ValueError("There should be 1 and only 1 recorder; %s were found" % len(recorders))
153-
replay.recorder = recorders[0]
157+
elif len(recorders) == 0:
158+
#If there are no recorders, then the recorder must not be a player, spectator or referee then
159+
replay.recorder = None
160+
else:
161+
replay.recorder = recorders[0]
154162

155163
####################################################
156164
# replay.game.events parsing classes
@@ -179,7 +187,7 @@ class EventParser(object):
179187
(UnknownEventParser_04X2(), lambda e: e.code & 0x0F == 2 ),
180188
(UnknownEventParser_0416(), lambda e: e.code == 0x16 ),
181189
(UnknownEventParser_04C6(), lambda e: e.code == 0xC6 ),
182-
(UnknownEventParser_0418or87(), lambda e: e.code == 0x18 or e.code == 0x87 ),
190+
(UnknownEventParser_0487(), lambda e: e.code == 0x87 ),
183191
(UnknownEventParser_0400(), lambda e: e.code == 0x00 ),],
184192
0x05: [
185193
(UnknownEventParser_0589(), lambda e: e.code == 0x89 ),],
@@ -203,20 +211,24 @@ def load(self,replay,filecontents):
203211
#The following byte completes the unique eventObjectidentifier
204212
first,eventCode = bytes.getBigInt(1),bytes.getBigInt(1)
205213
eventType,globalFlag,playerId = first >> 5,first & 0x10,first & 0xF
206-
214+
207215
#Create a barebones event from the gathered information
208216
event = Event(elapsedTime,eventType,eventCode,
209217
globalFlag,playerId,location,eventBytes)
210218

211-
#Get the parser and load the data into the event
212-
replay.events.append(self.getParser(event).load(event,bytes))
219+
try:
220+
#Get the parser and load the data into the event
221+
replay.events.append(self.getParser(event).load(event,bytes))
222+
except TypeError as e:
223+
raise ParseError(e.message,replay,event,bytes)
213224

214225
def getParser(self,event):
215226
if event.type not in self.parserMap.keys():
216227
raise TypeError("Unknown eventType: %s at location %s" % (hex(event.type),event.location))
217228

218229
for parser,accept in self.parserMap[event.type]:
219-
if accept(event): return parser
230+
if accept(event):
231+
return parser
220232

221233
raise TypeError("Unknown event: %s - %s at %s" % (hex(event.type),hex(event.code),event.location))
222234

@@ -241,15 +253,25 @@ class EventParser_16561(EventParser):
241253
(CameraMovementEventParser_18(), lambda e: e.code == 0x18 ),
242254
(CameraMovementEventParser_X1(), lambda e: e.code & 0x0F == 1 ),],
243255
0x04: [
244-
(UnknownEventParser_04X2(), lambda e: e.code & 0x0F == 2 ),
245-
(UnknownEventParser_0416(), lambda e: e.code == 0x16 ),
256+
(UnknownEventParser_0487(), lambda e: e.code == 0x87 ),
246257
(UnknownEventParser_04C6(), lambda e: e.code == 0xC6 ),
247-
(UnknownEventParser_0418or87(), lambda e: e.code == 0x18 or e.code == 0x87 ),
248-
(UnknownEventParser_0400(), lambda e: e.code == 0x00 ),],
258+
(UnknownEventParser_04XC(), lambda e: e.code & 0x0F == 0x0C and e.code >> 4 <= 0x04 ),],
249259
0x05: [
250260
(UnknownEventParser_0589(), lambda e: e.code == 0x89 ),],
251261
}
252262

263+
"""
264+
(UnknownEventParser_04X2(), lambda e: e.code & 0x0F == 2 ),
265+
(UnknownEventParser_0416(), lambda e: e.code == 0x16 ),
266+
(UnknownEventParser_0400(), lambda e: e.code == 0x00 ),
267+
268+
269+
270+
(UnknownEventParser_04X2(), lambda e: e.code & 0x0F == 2 ),
271+
(UnknownEventParser_0416(), lambda e: e.code == 0x16 ),
272+
(UnknownEventParser_0400(), lambda e: e.code == 0x00 ),
273+
"""
274+
253275
class EventParser_17326(EventParser):
254276
parserMap = {
255277
0x00: [
@@ -271,12 +293,9 @@ class EventParser_17326(EventParser):
271293
(CameraMovementEventParser_18(), lambda e: e.code == 0x18 ),
272294
(CameraMovementEventParser_X1(), lambda e: e.code & 0x0F == 1 ),],
273295
0x04: [
274-
(UnknownEventParser_04X2(), lambda e: e.code & 0x0F == 2 ),
275-
(UnknownEventParser_0416(), lambda e: e.code == 0x16 ),
296+
(UnknownEventParser_0487(), lambda e: e.code == 0x87 ),
276297
(UnknownEventParser_04C6(), lambda e: e.code == 0xC6 ),
277-
(UnknownEventParser_0418or87(), lambda e: e.code == 0x18 or e.code == 0x87 ),
278-
(UnknownEventParser_0400(), lambda e: e.code == 0x00 ),
279-
(UnknownEventParser_041C(), lambda e: e.code == 0x1C ),],
298+
(UnknownEventParser_04XC(), lambda e: e.code & 0x0F == 0x0C and e.code >> 4 <= 0x04 ),],
280299
0x05: [
281300
(UnknownEventParser_0589(), lambda e: e.code == 0x89 ),],
282301
}

sc2reader/replay.py

Lines changed: 63 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -9,81 +9,104 @@
99

1010
class Replay(object):
1111

12-
def __init__(self,replay):
12+
def __init__(self,replay,partialParse=True,fullParse=True):
1313
#Make sure the file exists and is readable
1414
if not os.access(replay,os.F_OK):
1515
raise ValueError("File at '%s' cannot be found" % replay)
1616
elif not os.access(replay,os.R_OK):
1717
raise ValueError("File at '%s' cannot be read" % replay)
18-
18+
19+
self.file = replay
20+
self._parseHeader()
21+
self.files = MPQArchive(replay).extract()
22+
self.parsed = dict(details=False,attributes=False,messages=False,events=False)
23+
24+
#These are quickly parsed files that contain most of the game information
25+
#The order is important, I need some way to reinforce it in the future
26+
if partialParse or fullparse:
27+
self._parseDetails()
28+
self._parseAttributes()
29+
self._parseMessages()
30+
31+
if fullParse:
32+
self._parseEvents()
33+
34+
def _parseHeader(self):
1935
#Open up a ByteStream for its contents
20-
source = ByteStream(open(replay).read())
36+
source = ByteStream(open(self.file).read())
2137

2238
#Check the file type for the MPQ header bytes
2339
if source.getBig(4) != "4D50511B":
24-
raise TypeError("File '%s' is not an MPQ file" % replay)
40+
raise TypeError("File '%s' is not an MPQ file" % self.file)
2541

2642
#Extract replay header data
27-
self.parseMPQHeader(source)
28-
29-
#Parse the rest of the archive
30-
self.parseMPQArchive(MPQArchive(replay))
31-
32-
#Use the parsed information to reorganize the data
33-
self.setDerivedValues()
34-
35-
def parseMPQHeader(self,source):
36-
#Read the MPQ file header
3743
max_data_size = source.getLittleInt(4) #possibly data max size
3844
header_offset = source.getLittleInt(4) #Offset of the second header
3945
data_size = source.getLittleInt(4) #possibly data size
4046

4147
#Extract replay attributes from the mpq
4248
data = source.parseSerializedData()
4349

50+
#Assign all the relevant information to the replay object
4451
self.build = data[1][4]
4552
self.versions = (data[1][1],data[1][2],data[1][3],self.build)
4653
self.releaseString = "%s.%s.%s.%s" % self.versions
4754
self.frames,self.seconds = (data[3],data[3]/16)
4855
self.length = (self.seconds/60,self.seconds%60)
4956

50-
def parseMPQArchive(self,archive):
51-
#Extract the archive files
52-
files = archive.extract()
53-
eventsFile = files['replay.game.events']
54-
detailsFile = files['replay.details']
55-
messageFile = files['replay.message.events']
56-
attributesFile = files['replay.attributes.events']
57-
58-
#Load the details file first to get player information
59-
parsers.getDetailParser(self.build).load(self,detailsFile)
60-
61-
#Next load the attributes file to fill out players and get team information
62-
parsers.getAttributeParser(self.build).load(self,attributesFile)
63-
64-
#Finally load the events file to get gameplay data and APM
65-
parsers.getEventParser(self.build).load(self,eventsFile)
57+
def _parseDetails(self):
58+
#Get player and map information
59+
parsers.getDetailParser(self.build).load(self,self.files['replay.details'])
60+
self.parsed['details'] = True
6661

67-
#We'll also load up the messages for a peak at what was going on
68-
parsers.getMessageParser(self.build).load(self,messageFile)
62+
def _parseAttributes(self):
63+
#The details file data is required for parsing
64+
if not self.parsed['details']:
65+
raise ValueError("The replay details must be parsed before parsing attributes")
66+
67+
parsers.getAttributeParser(self.build).load(self,self.files['replay.attributes.events'])
68+
self.parsed['attributes'] = True
6969

70-
def setDerivedValues(self):
70+
#We can now create teams
7171
self.teams = defaultdict(list)
7272
for player in self.players[1:]: #Skip the 'None' player 0
7373
self.teams[player.team].append(player)
74-
74+
75+
def _parseMessages(self):
76+
#The details file data is required for parsing
77+
if not self.parsed['details']:
78+
raise ValueError("The replay details must be parsed before parsing messages")
79+
80+
parsers.getMessageParser(self.build).load(self,self.files['replay.message.events'])
81+
self.parsed['messages'] = True
82+
83+
def _parseEvents(self):
84+
#The details file data is required for parsing
85+
if not self.parsed['details']:
86+
raise ValueError("The replay details must be parsed before parsing events")
87+
88+
parsers.getEventParser(self.build).load(self,self.files['replay.game.events'])
89+
self.parsed['events'] = True
90+
91+
#We can now sort events by type and get results
7592
self.eventsByType = defaultdict(list)
7693
for event in self.events:
7794
self.eventsByType[event.name].append(event)
95+
96+
self._processResults()
7897

79-
self.processResults()
80-
81-
def processResults(self):
98+
def _processResults(self):
99+
#The details,attributes, and events are required
100+
if not (self.parsed['details'] and self.parsed['attributes'] and self.parsed['events']):
101+
raise ValueError("The replay details must be parsed before parsing attributes")
102+
82103
#Remove players from the teams as they drop out of the game
83104
self.results = dict([team,len(players)] for team,players in self.teams.iteritems())
84105
for event in self.eventsByType['leave']:
85-
team = self.players[event.player].team
86-
self.results[team] -= 1
106+
#Some spectator actions seem to be recorded, they aren't on teams anyway
107+
if event.player < len(self.players):
108+
team = self.players[event.player].team
109+
self.results[team] -= 1
87110

88111
#mark all teams with no players left as losing, save the rest of the teams
89112
remaining = set()
@@ -96,7 +119,7 @@ def processResults(self):
96119
#If, at the end, only one team remains then that team has won
97120
if len(remaining) == 1:
98121
self.results[remaining.pop()] = "Won"
99-
else:
122+
elif self.recorder:
100123
#The other results are unknown except in the (common) case that the
101124
#recorder is the last one on his team to leave. In this case, the
102125
#result for his team can be known

0 commit comments

Comments
 (0)