1010class Replay (object ):
1111
1212 def __init__ (self , replay , partial_parse = True , full_parse = True ):
13- self .file = ""
13+ self .filename = replay
1414 self .speed = ""
1515 self .release_string = ""
1616 self .build = ""
1717 self .type = ""
1818 self .category = ""
1919 self .map = ""
20+ self .realm = ""
2021 self .events = list ()
2122 self .results = dict ()
22- self .teams = dict ()
23- self .players = list ()
23+ self .teams = defaultdict (list )
24+ self .players = list () #Unordered list of Player
25+ self .player = dict () #Maps pid to Player
2426 self .events_by_type = dict ()
2527 self .attributes = list ()
2628 self .length = None # (minutes, seconds) tuple
@@ -29,44 +31,54 @@ def __init__(self, replay, partial_parse=True, full_parse=True):
2931 self .versions = None # (number,number,number,number) tuple
3032 self .recorder = None # Player object
3133 self .frames = None # Integer representing FPS
34+
35+ # Set in parsers.DetailParser.load, should we hide this?
36+ self .file_time = None # Probably number milliseconds since EPOCH
37+ # Marked as private in case people want raw file access
38+ self ._files = dict () # Files extracted from mpyq
39+
40+ #Used internally to ensure parse ordering
41+ self .__parsed = dict (details = False , attributes = False , messages = False , events = False , initdata = False )
3242
3343 # TODO: Change to something better
3444 self .date = "" # Date when the game was played
35- # TODO: hide this?
36- self .files = dict () # From the mpq lib
37- # TODO: investigate where this gets set
38- self .file_time # Probably number of seconds or milliseconds since EPOCH (the 1970 date)
39- # TODO: This is used for internal purposes, better hide it!
40- self .parsed = dict ()
41-
42- #Make sure the file exists and is readable
43- if not os .access (replay , os .F_OK ):
44- raise ValueError ("File at '%s' cannot be found" % replay )
45- elif not os .access (replay , os .R_OK ):
46- raise ValueError ("File at '%s' cannot be read" % replay )
47-
48- self .file = replay
45+
46+
47+ #Make sure the file exists and is readable, first and foremost
48+ if not os .access (self .filename , os .F_OK ):
49+ raise ValueError ("File at '%s' cannot be found" % self .filename )
50+ elif not os .access (self .filename , os .R_OK ):
51+ raise ValueError ("File at '%s' cannot be read" % self .filename )
52+
53+ #Always parse the header first, the extract the files
4954 self ._parse_header ()
50- self . files = MPQArchive ( replay ). extract ()
51- self .parsed = dict ( details = False , attributes = False , messages = False , events = False )
55+ #Extract the available file from the MPQArchive
56+ self ._files = MPQArchive ( replay ). extract ( )
5257
5358 #These are quickly parsed files that contain most of the game information
5459 #The order is important, I need some way to reinforce it in the future
5560 if partial_parse or full_parse :
61+ self ._parse_initdata ()
5662 self ._parse_details ()
5763 self ._parse_attributes ()
5864 self ._parse_messages ()
5965
66+ #Parsing events takes forever, so only do this on request
6067 if full_parse :
6168 self ._parse_events ()
6269
70+
71+ def add_player (self ,player ):
72+ self .players .append (player )
73+ self .player [player .pid ] = player
74+
6375 def _parse_header (self ):
6476 #Open up a ByteStream for its contents
65- source = ByteStream (open (self .file ).read ())
77+ source = ByteStream (open (self .filename ).read ())
6678
6779 #Check the file type for the MPQ header bytes
68- if source .getBig (4 ) != "4D50511B" :
69- raise TypeError ("File '%s' is not an MPQ file" % self .file )
80+ if source .get_big (4 ) != "4D50511B" :
81+ raise TypeError ("File '%s' is not an MPQ file" % self .filename )
7082
7183 #Extract replay header data
7284 max_data_size = source .get_little_int (4 ) #possibly data max size
@@ -83,39 +95,45 @@ def _parse_header(self):
8395 self .frames ,self .seconds = (data [3 ], data [3 ]/ 16 )
8496 self .length = (self .seconds / 60 , self .seconds % 60 )
8597
98+ def _parse_initdata (self ):
99+ parsers .get_initdata_parser (self .build ).load (self , self ._files ['replay.initData' ])
100+ self .__parsed ['initdata' ] = True
101+
86102 def _parse_details (self ):
103+ if not self .__parsed ['initdata' ]:
104+ raise ValueError ("The replay initdata must be parsed before parsing details" )
105+
87106 #Get player and map information
88- parsers .get_detail_parser (self .build ).load (self , self .files ['replay.details' ])
89- self .parsed ['details' ] = True
107+ parsers .get_detail_parser (self .build ).load (self , self ._files ['replay.details' ])
108+ self .__parsed ['details' ] = True
90109
91110 def _parse_attributes (self ):
92111 #The details file data is required for parsing
93- if not self .parsed ['details' ]:
112+ if not self .__parsed ['details' ]:
94113 raise ValueError ("The replay details must be parsed before parsing attributes" )
95114
96- parsers .get_attribute_parser (self .build ).load (self , self .files ['replay.attributes.events' ])
97- self .parsed ['attributes' ] = True
115+ parsers .get_attribute_parser (self .build ).load (self , self ._files ['replay.attributes.events' ])
116+ self .__parsed ['attributes' ] = True
98117
99118 #We can now create teams
100- self .teams = defaultdict (list )
101- for player in self .players [1 :]: #Skip the 'None' player 0
119+ for player in self .players : #Skip the 'None' player 0
102120 self .teams [player .team ].append (player )
103121
104122 def _parse_messages (self ):
105123 #The details file data is required for parsing
106- if not self .parsed ['details' ]:
124+ if not self .__parsed ['details' ]:
107125 raise ValueError ("The replay details must be parsed before parsing messages" )
108126
109- parsers .get_message_parser (self .build ).load (self , self .files ['replay.message.events' ])
110- self .parsed ['messages' ] = True
127+ parsers .get_message_parser (self .build ).load (self , self ._files ['replay.message.events' ])
128+ self .__parsed ['messages' ] = True
111129
112130 def _parse_events (self ):
113131 #The details file data is required for parsing
114- if not self .parsed ['details' ]:
132+ if not self .__parsed ['details' ]:
115133 raise ValueError ("The replay details must be parsed before parsing events" )
116134
117- parsers .get_event_parser (self .build ).load (self , self .files ['replay.game.events' ])
118- self .parsed ['events' ] = True
135+ parsers .get_event_parser (self .build ).load (self , self ._files ['replay.game.events' ])
136+ self .__parsed ['events' ] = True
119137
120138 #We can now sort events by type and get results
121139 self .events_by_type = defaultdict (list )
@@ -126,15 +144,15 @@ def _parse_events(self):
126144
127145 def _process_results (self ):
128146 #The details,attributes, and events are required
129- if not (self .parsed ['details' ] and self .parsed ['attributes' ] and self .parsed ['events' ]):
147+ if not (self .__parsed ['details' ] and self .__parsed ['attributes' ] and self .__parsed ['events' ]):
130148 raise ValueError ("The replay details must be parsed before parsing attributes" )
131149
132150 #Remove players from the teams as they drop out of the game
133151 self .results = dict ([team , len (players )] for team , players in self .teams .iteritems ())
134152 for event in self .events_by_type ['leave' ]:
135153 #Some spectator actions seem to be recorded, they aren't on teams anyway
136154 if event .player < len (self .players ):
137- team = self .players [event .player ].team
155+ team = self .player [event .player ].team
138156 self .results [team ] -= 1
139157
140158 #mark all teams with no players left as losing, save the rest of the teams
@@ -165,7 +183,7 @@ def _process_results(self):
165183 if len (remaining ) == 1 :
166184 self .results [remaining .pop ()] = "Won"
167185
168- for player in self .players [ 1 :] :
186+ for player in self .players :
169187 player .result = self .results [player .team ]
170188
171189if __name__ == '__main__' :
0 commit comments