1- # -*- coding: utf-8 -*-
2- """
3- sc2reader
4- ----------
1+ from __future__ import absolute_import
52
6- A Starcraft II replay parsing library intended to promote innovation in
7- Starcraft tools and communities. Eventually, it will cover all official
8- releases and dump easily to JSON for inter-language portability.
3+ import os
4+ from sc2reader import utils
5+ from sc2reader import readers
6+ from sc2reader import data
7+ from sc2reader import exceptions
8+ from sc2reader .replay import Replay
9+ from collections import defaultdict
10+
11+ class SC2Reader (object ):
12+
13+ default_options = dict (
14+ # General use
15+ verbose = False ,
16+ debug = False ,
17+
18+ # Related to the SC2Reader class only
19+ register_defaults = True ,
20+
21+ # Related to creating new replay objects
22+ autoplay = True ,
23+ complete = True ,
24+
25+ # Related to passing paths into load_replay(s)
26+ directory = '' ,
27+
28+ # Related to passing a directory to load_replays
29+ depth = - 1 ,
30+ exclude = [],
31+ followlinks = False
32+ )
33+
34+ def __init__ (self , ** options ):
35+ self .registered_readers = defaultdict (list )
36+ self .registered_datapacks = list ()
37+ self .registered_listeners = defaultdict (list )
38+
39+ self .options = utils .AttributeDict (utils .merged_dict (self .default_options , options ))
40+
41+ if self .options .register_defaults :
42+ self .register_defaults ()
43+
44+ def load_replays (self , replay_collection , options = None , ** new_options ):
45+ options = options or utils .merged_dict (self .options , new_options )
46+ if isinstance (replay_collection , basestring ):
47+ directory = os .path .join (options .get ('directory' ,'' ), replay_collection )
48+ del options ['directory' ] # don't need this anymore on this request
49+ for replay_path in utils .get_replay_files (directory , ** options ):
50+ with open (replay_path ) as replay_file :
51+ try :
52+ yield self .load_replay (replay_file , options = options )
53+ except exceptions .MPQError as e :
54+ print e
955
10- sc2reader has been carefully written for clarity to serve as a starting
11- point for those who want to write their own parsers, potentially in other
12- languages for native access.
56+ else :
57+ for replay_file in replay_collection :
58+ yield self . load_replay ( replay_file , options = options )
1359
14- Enjoy.
60+ def load_replay (self , replay_file , options = None , ** new_options ):
61+ options = options or utils .merged_dict (self .options , new_options )
62+ complete = options .get ('complete' ,True )
63+ autoplay = options .get ('autoplay' ,True )
1564
16- :copyright: (c) 2011 Graylin Kim
17- :license: MIT, See LICENSE.txt for details
18- """
19- from __future__ import absolute_import
2065
21- __version__ = '0.3.3'
66+ if isinstance (replay_file , basestring ):
67+ location = os .path .join (options .get ('directory' ,'' ), replay_file )
68+ with open (location , 'rb' ) as replay_file :
69+ return self .load_replay (replay_file , options = options )
2270
71+ if options ['verbose' ]:
72+ print replay_file .name
2373
24- #System imports
25- import os
74+ replay = Replay (replay_file , ** options )
2675
27- #PyPi imports
28- import mpyq
29-
30- #Package imports
31- from sc2reader import config , objects , utils , processors , exceptions
32-
33-
34- class Reader (object ):
35- """ The SC2Reader class acts as a factory class for replay objects. The
36- class accepts a key=value list of options to override defaults (see
37- config.py) and exposes a very simple read/configure interface and
38- orchestrates the replay build process.
39- """
40-
41- def __init__ (self , ** user_options ):
42- """ The constructor makes a copy of the default_options to make sure the
43- option configuration doesn't propagate back to the default_options.
44- It should support any arbitrary number of different Reader objects.
45- """
46- self .options = config .default_options .copy ()
47- self .configure (** user_options )
48-
49- def configure (self , ** options ):
50- self .options .update (options )
51-
52- def read (self , location , ** user_options ):
53- """ Read indicated file or recursively read matching files from the
54- specified directory. Returns a replay or a list of replays depending
55- on the context.
56- """
57-
58- # Base the options off a copy to leave the Reader options uneffected.
59- options = self .options .copy ()
60- options .update (user_options )
61-
62- # The directory option allows users to specify file locations relative
63- # to a location other than the present working directory by joining the
64- # location with the directory of their choice.
65- if options .directory :
66- location = os .path .join (options .directory , location )
67-
68- # When passed a directory as the location, the Reader recursively builds
69- # a list of replays to return using the utils.get_files function. This
70- # function respects the following arguments:
71- # * depth: The maximum depth to traverse. Defaults to unlimited (-1)
72- # * follow_symlinks: Boolean for following symlinks. Defaults to True
73- # * exclude_dirs: A list of directory names to skip while recursing
74- # * incldue_regex: A regular expression rule which all returned file
75- # names must match. Defaults to None
76- #
77- replays , files = list (), utils .get_files (location , ** options )
78-
79- # If no files are found, it could be for a variety of reasons
80- # raise a NoMatchingFilesError to alert them to the situation
81- if not files :
82- raise exceptions .NoMatchingFilesError ()
83-
84- for location in files :
85- if options .verbose : print "Reading: %s" % location
76+ for data_file in ('replay.initData' ,
77+ 'replay.details' ,
78+ 'replay.attributes.events' ,
79+ 'replay.message.events' ,):
80+ reader = self .get_reader (data_file , replay )
81+ replay .read_data (data_file , reader )
8682
87- with open (location , 'rb' ) as replay_file :
88- replays .append (self .make_replay (replay_file , ** options ))
89-
90- return replays
91-
92- def make_replay (self , replay_file , ** options ):
93- options = utils .AttributeDict (options )
94-
95- # The Replay constructor scans the header of the replay file for
96- # the build number and stores the options for later use. The
97- # options are copied so subsequent option changes are isolated.
98- replay_file .seek (0 )
99- replay = objects .Replay (replay_file , ** options .copy ())
100-
101- # .SC2Replay files are written in Blizzard's MPQ Archive format.
102- # The format stores a header which contains a block table that
103- # specifies the location of each encrypted file.
104- #
105- # Unfortunately, some replay sites modify the replay contents to
106- # add messages promoting their sites without updating the header
107- # correctly. The listfile option(hack) lets us bypass this issue
108- # by specifying the files we want instead of generating a list.
109- #
110- # In order to wrap mpyq exceptions we have to do this try hack.
111- try :
112- replay_file .seek (0 )
113- archive = mpyq .MPQArchive (replay_file , listfile = False )
114- except KeyboardInterrupt : raise
115- except :
116- raise exceptions .MPQError ("Unable to construct the MPQArchive" )
117-
118- # These files are configured for either full or partial parsing
119- for file in options .files :
120-
121- # To wrap mpyq exceptions we have to do this try hack.
122- try :
123- # Handle the tampering with the message.events file that some sites do
124- if file == 'replay.message.events' :
125- try :
126- filedata = archive .read_file (file , force_decompress = True )
127- except IndexError as e :
128- # Catch decompression errors
129- if str (e ) == "string index out of range" :
130- filedata = archive .read_file (file , force_decompress = False )
131- else :
132- raise
133- else :
134- filedata = archive .read_file (file )
135- except KeyboardInterrupt : raise
136- except :
137- raise exceptions .MPQError ("Unable to extract file: {0}" .format (file ))
138-
139- # For each file, we build a smart buffer object from the
140- # utf-8 encoded bitstream that mpyq extracts.
141- buffer = utils .ReplayBuffer (filedata )
142-
143- # Each version of Starcraft slightly modifies some portions
144- # of the format for some files. To work with this, the
145- # config file has a nested lookup structure of
146- # [build][file]=>reader which returns the appropriate reader
147- #
148- # TODO: Different versions also have different data mappings
149- # sc2reader doesn't yet handle this difficulty.
150- #
151- # Readers use the type agnostic __call__ interface so that
152- # they can be implemented as functions or classes as needed.
153- #
154- # Readers return the extracted information from the buffer
155- # object which gets stored into the raw data dict for later
156- # use in post processing because correct interpretation of
157- # the information often requires data from other files.
158- reader = config .readers [replay .build ][file ]
159- reference_name = '_' .join (file .split ('.' )[1 :])
160- replay .raw [reference_name ] = reader (buffer , replay )
161-
162- # Now that the replay has been loaded with the "raw" data from
163- # the archive files we run the system level post processors to
164- # organize the data into a cross referenced data structure.
165- #
166- # After system level processors have run, call each of the post
167- # processors provided by the user. This would be a good place to
168- # convert the object to a serialized json string for cross
169- # language processes or add custom attributes.
170- #
171- # TODO: Maybe we should switch this to a hook based architecture
172- # Needs to be able to load "contrib" type processors..
173- for process in [processors .Full ]+ options .processors :
174- replay = process (replay )
83+ replay .load_details ()
84+ replay .load_players ()
85+
86+ if complete :
87+ for data_file in ('replay.game.events' ,):
88+ reader = self .get_reader (data_file , replay )
89+ replay .read_data (data_file , reader )
90+
91+ replay .load_events (self .get_datapack (replay ))
92+
93+ if autoplay :
94+ replay .listeners = self .get_listeners (replay )
95+ replay .start ()
17596
17697 return replay
17798
178- def read_file (self , file_in , ** user_options ):
179- # Support file-like objects (with a read method)
180- if hasattr (file_in , 'read' ) and hasattr (file_in , 'seek' ):
18199
182- # Base the options off a copy to leave the Reader options unaffected.
183- options = self .options .copy ()
184- options .update (user_options )
100+ def get_reader (self , data_file , replay ):
101+ for callback , reader in self .registered_readers [data_file ]:
102+ if callback (replay ):
103+ return reader
185104
186- return self .make_replay (file_in , ** options )
105+ def get_datapack (self , replay ):
106+ for callback , datapack in self .registered_datapacks :
107+ if callback (replay ):
108+ return datapack
109+ return None
187110
188- # Also support filepath strings
189- else :
190- replays = self .read (file_in , ** user_options )
111+ def get_listeners (self , replay ):
112+ listeners = defaultdict (list )
113+ for event_class in self .registered_listeners .keys ():
114+ for callback , listener in self .registered_listeners [event_class ]:
115+ if callback (replay ):
116+ listeners [event_class ].append (listener )
117+ return listeners
118+
119+
120+ def register_listener (self , event_class , listener , callback = lambda r : True ):
121+ self .registered_listeners [event_class ].append ((callback , listener ))
122+
123+ def register_reader (self , data_file , reader , callback = lambda r : True ):
124+ self .registered_readers [data_file ].insert (0 ,(callback , reader ))
125+
126+ def register_datapack (self , datapack , callback = lambda r : True ):
127+ self .registered_datapacks .insert (0 ,(callback , datapack ))
128+
129+
130+ def register_defaults (self ):
131+ self .register_default_readers ()
132+ self .register_default_datapacks ()
133+ self .register_default_listeners ()
191134
192- # While normal usage would suggest passing in only filenames, it is
193- # possible that directories could be passed in. Don't fail silently!
194- if len (replays ) > 1 :
195- raise exceptions .MultipleMatchingFilesError (replays )
135+ def register_default_readers (self ):
136+ self .register_reader ('replay.details' , readers .DetailsReader_Base ())
137+ self .register_reader ('replay.initData' , readers .InitDataReader_Base ())
138+ self .register_reader ('replay.message.events' , readers .MessageEventsReader_Base ())
139+ self .register_reader ('replay.attributes.events' , readers .AttributesEventsReader_Base (), lambda r : r .build < 17326 )
140+ self .register_reader ('replay.attributes.events' , readers .AttributesEventsReader_17326 (), lambda r : r .build >= 17326 )
141+ self .register_reader ('replay.game.events' , readers .GameEventsReader_Base (), lambda r : r .build < 16561 )
142+ self .register_reader ('replay.game.events' , readers .GameEventsReader_16561 (), lambda r : 16561 <= r .build < 18574 )
143+ self .register_reader ('replay.game.events' , readers .GameEventsReader_18574 (), lambda r : 18574 <= r .build < 19595 )
144+ self .register_reader ('replay.game.events' , readers .GameEventsReader_19595 (), lambda r : 19595 <= r .build )
196145
197- # Propogate the replay in a singular context
198- return replays [0 ] if len (replays ) > 0 else None
146+ def register_default_datapacks (self ):
147+ self .register_datapack (data .Data_16561 , lambda r : 16561 <= r .build < 17326 )
148+ self .register_datapack (data .Data_17326 , lambda r : 17326 <= r .build < 18317 )
149+ self .register_datapack (data .Data_18317 , lambda r : 18317 <= r .build < 19595 )
150+ self .register_datapack (data .Data_19595 , lambda r : 19595 <= r .build )
199151
152+ def register_default_listeners (self ):
153+ #self.register_listener(Event, PrintEventListener())
154+ pass
200155
201- """sc2reader uses a default SC2Reader class instance to provide a package level
202- interface to its functionality. The package level interface presents the same
203- functional interface, it just saves the hassle of creating the class object.
204- """
205- __defaultReader = Reader ()
206156
207- def read_file (location , ** user_options ):
208- return __defaultReader .read_file (location , ** user_options )
157+ __defaultSC2Reader = SC2Reader ()
209158
210- def read (location , ** user_options ):
211- return __defaultReader .read (location , ** user_options )
159+ register_datapack = __defaultSC2Reader .register_datapack
160+ register_listener = __defaultSC2Reader .register_listener
161+ register_reader = __defaultSC2Reader .register_reader
212162
213- def configure (** options ):
214- config .default_options .update (options )
163+ get_listeners = __defaultSC2Reader .get_listeners
164+ get_datapack = __defaultSC2Reader .get_datapack
165+ get_reader = __defaultSC2Reader .get_reader
215166
216- def reset ():
217- __defaultReader = Reader ()
167+ load_replay = __defaultSC2Reader . load_replay
168+ load_replays = __defaultSC2Reader . load_replays
0 commit comments