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,49 +31,56 @@ 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 # http://en.wikipedia.org/wiki/Epoch_(reference_date)
3545 # Notice that Windows and Mac have different EPOCHs, I wonder whether
3646 # this is different depending on the OS on which the replay was played.
3747 self .date = "" # Date when the game was played
3848
39-
40- # TODO: hide this?
41- self .files = dict () # From the mpq lib
42- # TODO: investigate where this gets set
43- self .file_time # Probably number of seconds or milliseconds since EPOCH (the 1970 date)
44- # TODO: This is used for internal purposes, better hide it!
45- self .parsed = dict ()
46-
47- #Make sure the file exists and is readable
48- if not os .access (replay , os .F_OK ):
49- raise ValueError ("File at '%s' cannot be found" % replay )
50- elif not os .access (replay , os .R_OK ):
51- raise ValueError ("File at '%s' cannot be read" % replay )
52-
53- self .file = replay
49+ #Make sure the file exists and is readable, first and foremost
50+ if not os .access (self .filename , os .F_OK ):
51+ raise ValueError ("File at '%s' cannot be found" % self .filename )
52+ elif not os .access (self .filename , os .R_OK ):
53+ raise ValueError ("File at '%s' cannot be read" % self .filename )
54+
55+ #Always parse the header first, the extract the files
5456 self ._parse_header ()
55- self . files = MPQArchive ( replay ). extract ()
56- self .parsed = dict ( details = False , attributes = False , messages = False , events = False )
57+ #Extract the available file from the MPQArchive
58+ self ._files = MPQArchive ( replay ). extract ( )
5759
5860 #These are quickly parsed files that contain most of the game information
5961 #The order is important, I need some way to reinforce it in the future
6062 if partial_parse or full_parse :
63+ self ._parse_initdata ()
6164 self ._parse_details ()
6265 self ._parse_attributes ()
6366 self ._parse_messages ()
6467
68+ #Parsing events takes forever, so only do this on request
6569 if full_parse :
6670 self ._parse_events ()
6771
72+
73+ def add_player (self ,player ):
74+ self .players .append (player )
75+ self .player [player .pid ] = player
76+
6877 def _parse_header (self ):
6978 #Open up a ByteStream for its contents
70- source = ByteStream (open (self .file ).read ())
79+ source = ByteStream (open (self .filename ).read ())
7180
7281 #Check the file type for the MPQ header bytes
73- if source .getBig (4 ) != "4D50511B" :
74- raise TypeError ("File '%s' is not an MPQ file" % self .file )
82+ if source .get_big (4 ) != "4D50511B" :
83+ raise TypeError ("File '%s' is not an MPQ file" % self .filename )
7584
7685 #Extract replay header data
7786 max_data_size = source .get_little_int (4 ) #possibly data max size
@@ -88,39 +97,45 @@ def _parse_header(self):
8897 self .frames , self .seconds = (data [3 ], data [3 ]/ 16 )
8998 self .length = (self .seconds / 60 , self .seconds % 60 )
9099
100+ def _parse_initdata (self ):
101+ parsers .get_initdata_parser (self .build ).load (self , self ._files ['replay.initData' ])
102+ self .__parsed ['initdata' ] = True
103+
91104 def _parse_details (self ):
105+ if not self .__parsed ['initdata' ]:
106+ raise ValueError ("The replay initdata must be parsed before parsing details" )
107+
92108 #Get player and map information
93- parsers .get_detail_parser (self .build ).load (self , self .files ['replay.details' ])
94- self .parsed ['details' ] = True
109+ parsers .get_detail_parser (self .build ).load (self , self ._files ['replay.details' ])
110+ self .__parsed ['details' ] = True
95111
96112 def _parse_attributes (self ):
97113 #The details file data is required for parsing
98- if not self .parsed ['details' ]:
114+ if not self .__parsed ['details' ]:
99115 raise ValueError ("The replay details must be parsed before parsing attributes" )
100116
101- parsers .get_attribute_parser (self .build ).load (self , self .files ['replay.attributes.events' ])
102- self .parsed ['attributes' ] = True
117+ parsers .get_attribute_parser (self .build ).load (self , self ._files ['replay.attributes.events' ])
118+ self .__parsed ['attributes' ] = True
103119
104120 #We can now create teams
105- self .teams = defaultdict (list )
106- for player in self .players [1 :]: #Skip the 'None' player 0
121+ for player in self .players : #Skip the 'None' player 0
107122 self .teams [player .team ].append (player )
108123
109124 def _parse_messages (self ):
110125 #The details file data is required for parsing
111- if not self .parsed ['details' ]:
126+ if not self .__parsed ['details' ]:
112127 raise ValueError ("The replay details must be parsed before parsing messages" )
113128
114- parsers .get_message_parser (self .build ).load (self , self .files ['replay.message.events' ])
115- self .parsed ['messages' ] = True
129+ parsers .get_message_parser (self .build ).load (self , self ._files ['replay.message.events' ])
130+ self .__parsed ['messages' ] = True
116131
117132 def _parse_events (self ):
118133 #The details file data is required for parsing
119- if not self .parsed ['details' ]:
134+ if not self .__parsed ['details' ]:
120135 raise ValueError ("The replay details must be parsed before parsing events" )
121136
122- parsers .get_event_parser (self .build ).load (self , self .files ['replay.game.events' ])
123- self .parsed ['events' ] = True
137+ parsers .get_event_parser (self .build ).load (self , self ._files ['replay.game.events' ])
138+ self .__parsed ['events' ] = True
124139
125140 #We can now sort events by type and get results
126141 self .events_by_type = defaultdict (list )
@@ -131,15 +146,15 @@ def _parse_events(self):
131146
132147 def _process_results (self ):
133148 #The details,attributes, and events are required
134- if not (self .parsed ['details' ] and self .parsed ['attributes' ] and self .parsed ['events' ]):
149+ if not (self .__parsed ['details' ] and self .__parsed ['attributes' ] and self .__parsed ['events' ]):
135150 raise ValueError ("The replay details must be parsed before parsing attributes" )
136151
137152 #Remove players from the teams as they drop out of the game
138153 self .results = dict ([team , len (players )] for team , players in self .teams .iteritems ())
139154 for event in self .events_by_type ['leave' ]:
140155 #Some spectator actions seem to be recorded, they aren't on teams anyway
141156 if event .player < len (self .players ):
142- team = self .players [event .player ].team
157+ team = self .player [event .player ].team
143158 self .results [team ] -= 1
144159
145160 #mark all teams with no players left as losing, save the rest of the teams
@@ -170,7 +185,7 @@ def _process_results(self):
170185 if len (remaining ) == 1 :
171186 self .results [remaining .pop ()] = "Won"
172187
173- for player in self .players [ 1 :] :
188+ for player in self .players :
174189 player .result = self .results [player .team ]
175190
176191if __name__ == '__main__' :
0 commit comments