|
1 | 1 | from __future__ import absolute_import |
2 | 2 |
|
3 | | -from sc2reader.sc import SC2Reader |
| 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 | + The primary interface to the sc2reader library. Acts as a configurable |
| 14 | + factory for :class:`Replay` objects. Maintains a set of registered readers, |
| 15 | + datapacks, and listeners with filterfuncs that allow the factory to apply |
| 16 | + a replay specific context to each replay as it loads. |
| 17 | +
|
| 18 | + #TODO: Include some examples here... |
| 19 | +
|
| 20 | + See the specific functions below for details. |
| 21 | +
|
| 22 | + :param True register_defaults: Automatically registers default readers |
| 23 | + and datapacks. Only disable if you know what you are doing. |
| 24 | +
|
| 25 | + :param True load_events: Enables parsing of game events. If you are do |
| 26 | + not need this information setting to false will reduce replay load |
| 27 | + time. |
| 28 | +
|
| 29 | + :param True autoplay: Enables auto playing of replays after loading game |
| 30 | + events. Playing events triggers enables registered listeners to add |
| 31 | + new data features to replays. Option ignored if load_events is false. |
| 32 | +
|
| 33 | + :param False load_map: Triggers downloading and parsing of map files |
| 34 | + associated with replays as they are loaded. When false, only the map |
| 35 | + url and name are available. |
| 36 | +
|
| 37 | + :param False verbose: Causes many steps in the replay loading process |
| 38 | + to produce more verbose output. |
| 39 | +
|
| 40 | + :param string directory: Specifies a base directory to prepend to all paths |
| 41 | + before attempting to load the replay. |
| 42 | +
|
| 43 | + :param -1 depth: Indicates the maximum search depth when loading replays |
| 44 | + from directories. -1 indicates no limit, 0 turns recursion off. |
| 45 | +
|
| 46 | + :param list exclude: A list of directory names (not paths) to exclude when |
| 47 | + performing recursive searches while loading replays from directories. |
| 48 | +
|
| 49 | + :param False followlinks: Enables symlink following when recursing through |
| 50 | + directories to load replay files. |
| 51 | +
|
| 52 | + """ |
| 53 | + |
| 54 | + default_options = dict( |
| 55 | + # General use |
| 56 | + verbose=False, |
| 57 | + debug=False, |
| 58 | + |
| 59 | + # Related to the SC2Reader class only |
| 60 | + register_defaults=True, |
| 61 | + |
| 62 | + # Related to creating new replay objects |
| 63 | + autoplay=True, |
| 64 | + load_events=True, |
| 65 | + load_map=False, |
| 66 | + |
| 67 | + # Related to passing paths into load_replay(s) |
| 68 | + directory='', |
| 69 | + |
| 70 | + # Related to passing a directory to load_replays |
| 71 | + depth=-1, |
| 72 | + exclude=[], |
| 73 | + followlinks=False |
| 74 | + ) |
| 75 | + |
| 76 | + def __init__(self, **options): |
| 77 | + self.reset() |
| 78 | + self.configure(**options) |
| 79 | + |
| 80 | + if self.options.get('register_defaults',None): |
| 81 | + self.register_defaults() |
| 82 | + |
| 83 | + |
| 84 | + |
| 85 | + def configure(self, **new_options): |
| 86 | + """ |
| 87 | + Update the factory settings with the specified overrides. |
| 88 | +
|
| 89 | + See :class:`SC2Reader` for a list of available options. |
| 90 | +
|
| 91 | + :param new_options: Option values to override current factory settings. |
| 92 | + """ |
| 93 | + self.options.update(new_options) |
| 94 | + |
| 95 | + def reset(self): |
| 96 | + """ |
| 97 | + Resets the current factory to default settings and removes all |
| 98 | + registered readers, datapacks, and listeners. |
| 99 | + """ |
| 100 | + self.options = utils.AttributeDict(self.default_options) |
| 101 | + self.registered_readers = defaultdict(list) |
| 102 | + self.registered_datapacks = list() |
| 103 | + self.registered_listeners = defaultdict(list) |
| 104 | + |
| 105 | + |
| 106 | + def load_replays(self, collection, options=None, **new_options): |
| 107 | + """ |
| 108 | + Loads the specified collection of replays using the current factory |
| 109 | + settings with specified overrides. |
| 110 | +
|
| 111 | + :param collection: Either a directory path or a mixed collection of |
| 112 | + directories, file paths, and open file objects. |
| 113 | +
|
| 114 | + :param None options: When options are passed directly into the options |
| 115 | + parameter the current factory settings are ignored and only the |
| 116 | + specified options are used during replay load. |
| 117 | +
|
| 118 | + :param new_options: Options values to override current factory settings |
| 119 | + for the collection of replays to be loaded. |
| 120 | +
|
| 121 | + :rtype: generator(:class:`Replay`) |
| 122 | + """ |
| 123 | + options = options or utils.merged_dict(self.options, new_options) |
| 124 | + |
| 125 | + # Get the directory and hide it from nested calls |
| 126 | + directory = options.get('directory','') |
| 127 | + if 'directory' in options: del options['directory'] |
| 128 | + |
| 129 | + if isinstance(collection, basestring): |
| 130 | + full_path = os.path.join(directory, collection) |
| 131 | + for replay_path in utils.get_replay_files(full_path, **options): |
| 132 | + with open(replay_path) as replay_file: |
| 133 | + try: |
| 134 | + yield self.load_replay(replay_file, options=options) |
| 135 | + except exceptions.MPQError as e: |
| 136 | + print e |
| 137 | + |
| 138 | + else: |
| 139 | + for replay_file in collection: |
| 140 | + if isinstance(replay_file, basestring): |
| 141 | + full_path = os.path.join(directory, replay_file) |
| 142 | + if os.path.isdir(full_path): |
| 143 | + for replay in self.load_replays(full_path, options=options): |
| 144 | + yield replay |
| 145 | + else: |
| 146 | + yield self.load_replay(full_path, options=options) |
| 147 | + |
| 148 | + else: |
| 149 | + yield self.load_replay(replay_file, options=options) |
| 150 | + |
| 151 | + def load_replay(self, replay_file, options=None, **new_options): |
| 152 | + """ |
| 153 | + Loads the specified replay using current factory settings with the |
| 154 | + specified overrides. |
| 155 | +
|
| 156 | + :param replay: An open file object or a path to a single file. |
| 157 | +
|
| 158 | + :param None options: When options are passed directly into the options |
| 159 | + parameter the current factory settings are ignored and only the |
| 160 | + specified options are used during replay load. |
| 161 | +
|
| 162 | + :param new_options: Options values to override current factory settings |
| 163 | + while loading this replay. |
| 164 | +
|
| 165 | + :rtype: :class:`Replay` |
| 166 | + """ |
| 167 | + options = options or utils.merged_dict(self.options, new_options) |
| 168 | + load_events = options.get('load_events',True) |
| 169 | + load_map = options.get('load_map',False) |
| 170 | + autoplay = options.get('autoplay',True) |
| 171 | + |
| 172 | + if isinstance(replay_file, basestring): |
| 173 | + location = os.path.join(options.get('directory',''), replay_file) |
| 174 | + with open(location, 'rb') as replay_file: |
| 175 | + return self.load_replay(replay_file, options=options) |
| 176 | + |
| 177 | + if options['verbose']: |
| 178 | + print replay_file.name |
| 179 | + |
| 180 | + replay = Replay(replay_file, **options) |
| 181 | + |
| 182 | + for data_file in ('replay.initData', |
| 183 | + 'replay.details', |
| 184 | + 'replay.attributes.events', |
| 185 | + 'replay.message.events',): |
| 186 | + reader = self.get_reader(data_file, replay) |
| 187 | + replay.read_data(data_file, reader) |
| 188 | + |
| 189 | + replay.load_details() |
| 190 | + replay.load_players() |
| 191 | + |
| 192 | + if load_map: |
| 193 | + replay.load_map() |
| 194 | + |
| 195 | + if load_events: |
| 196 | + for data_file in ('replay.game.events',): |
| 197 | + reader = self.get_reader(data_file, replay) |
| 198 | + replay.read_data(data_file, reader) |
| 199 | + |
| 200 | + replay.load_events(self.get_datapack(replay)) |
| 201 | + |
| 202 | + if autoplay: |
| 203 | + replay.listeners = self.get_listeners(replay) |
| 204 | + replay.start() |
| 205 | + |
| 206 | + return replay |
| 207 | + |
| 208 | + |
| 209 | + def get_reader(self, data_file, replay): |
| 210 | + for callback, reader in self.registered_readers[data_file]: |
| 211 | + if callback(replay): |
| 212 | + return reader |
| 213 | + |
| 214 | + def get_datapack(self, replay): |
| 215 | + for callback, datapack in self.registered_datapacks: |
| 216 | + if callback(replay): |
| 217 | + return datapack |
| 218 | + return None |
| 219 | + |
| 220 | + def get_listeners(self, replay): |
| 221 | + listeners = defaultdict(list) |
| 222 | + for event_class in self.registered_listeners.keys(): |
| 223 | + for callback, listener in self.registered_listeners[event_class]: |
| 224 | + if callback(replay): |
| 225 | + listeners[event_class].append(listener) |
| 226 | + return listeners |
| 227 | + |
| 228 | + |
| 229 | + def register_listener(self, events, listener, filterfunc=lambda r: True): |
| 230 | + """ |
| 231 | + Allows you to specify event listeners for adding new features to the |
| 232 | + :class:`Replay` objects on :meth:`~Replay.play`. sc2reader comes with a |
| 233 | + small collection of :class:`Listener` classes that you can apply to your |
| 234 | + replays as needed. |
| 235 | +
|
| 236 | + Events are sent to listeners in registration order as they come up. By |
| 237 | + specifying a parent class you can register a listener to a set of events |
| 238 | + at once instead of listing them out individually. See the tutorials for |
| 239 | + more information. |
| 240 | +
|
| 241 | + :param events: A list of event classes you want sent to this listener. |
| 242 | + Registration to a single event can be done by specifying a single |
| 243 | + event class instead of a list. An isinstance() check is used so |
| 244 | + you can catch sets of classes at once by supplying a parent class. |
| 245 | +
|
| 246 | + :param listener: The :class:`Listener` object you want events sent to. |
| 247 | +
|
| 248 | + :param filterfunc: A function that accepts a partially loaded |
| 249 | + :class:`Replay` object as an argument and returns true if the |
| 250 | + reader should be used on this replay. |
| 251 | + """ |
| 252 | + try: |
| 253 | + for event in events: |
| 254 | + self.registered_listeners[event].append((filterfunc, listener)) |
| 255 | + except TypeError: |
| 256 | + self.registered_listeners[event].append((filterfunc, listener)) |
| 257 | + |
| 258 | + def register_reader(self, data_file, reader, filterfunc=lambda r: True): |
| 259 | + """ |
| 260 | + Allows you to specify your own reader for use when reading the data |
| 261 | + files packed into the .SC2Replay archives. Datapacks are checked for |
| 262 | + use with the supplied filterfunc in reverse registration order to give |
| 263 | + user registered datapacks preference over factory default datapacks. |
| 264 | +
|
| 265 | + Don't use this unless you know what you are doing. |
| 266 | +
|
| 267 | + :param data_file: The full file name that you would like this reader to |
| 268 | + parse. |
| 269 | +
|
| 270 | + :param reader: The :class:`Reader` object you wish to use to read the |
| 271 | + data file. |
| 272 | +
|
| 273 | + :param filterfunc: A function that accepts a partially loaded |
| 274 | + :class:`Replay` object as an argument and returns true if the |
| 275 | + reader should be used on this replay. |
| 276 | + """ |
| 277 | + self.registered_readers[data_file].insert(0,(filterfunc, reader)) |
| 278 | + |
| 279 | + def register_datapack(self, datapack, filterfunc=lambda r: True): |
| 280 | + """ |
| 281 | + Allows you to specify your own datapacks for use when loading replays. |
| 282 | + Datapacks are checked for use with the supplied filterfunc in reverse |
| 283 | + registration order to give user registered datapacks preference over |
| 284 | + factory default datapacks. |
| 285 | +
|
| 286 | + This is how you would add mappings for your favorite custom map. |
| 287 | +
|
| 288 | + :param datapack: A :class:`BaseData` object to use for mapping unit |
| 289 | + types and ability codes to their corresponding classes. |
| 290 | +
|
| 291 | + :param filterfunc: A function that accepts a partially loaded |
| 292 | + :class:`Replay` object as an argument and returns true if the |
| 293 | + datapack should be used on this replay. |
| 294 | + """ |
| 295 | + self.registered_datapacks.insert(0,(filterfunc, datapack)) |
| 296 | + |
| 297 | + |
| 298 | + def register_defaults(self): |
| 299 | + """Registers all factory default objects.""" |
| 300 | + self.register_default_readers() |
| 301 | + self.register_default_datapacks() |
| 302 | + |
| 303 | + def register_default_readers(self): |
| 304 | + """Registers factory default readers.""" |
| 305 | + self.register_reader('replay.details', readers.DetailsReader_Base()) |
| 306 | + self.register_reader('replay.initData', readers.InitDataReader_Base()) |
| 307 | + self.register_reader('replay.message.events', readers.MessageEventsReader_Base()) |
| 308 | + self.register_reader('replay.attributes.events', readers.AttributesEventsReader_Base(), lambda r: r.build < 17326) |
| 309 | + self.register_reader('replay.attributes.events', readers.AttributesEventsReader_17326(), lambda r: r.build >= 17326) |
| 310 | + self.register_reader('replay.game.events', readers.GameEventsReader_Base(), lambda r: r.build < 16561) |
| 311 | + self.register_reader('replay.game.events', readers.GameEventsReader_16561(), lambda r: 16561 <= r.build < 18574) |
| 312 | + self.register_reader('replay.game.events', readers.GameEventsReader_18574(), lambda r: 18574 <= r.build < 19595) |
| 313 | + self.register_reader('replay.game.events', readers.GameEventsReader_19595(), lambda r: 19595 <= r.build) |
| 314 | + |
| 315 | + def register_default_datapacks(self): |
| 316 | + """Registers factory default datapacks.""" |
| 317 | + self.register_datapack(data.Data_16561, lambda r: 16561 <= r.build < 17326) |
| 318 | + self.register_datapack(data.Data_17326, lambda r: 17326 <= r.build < 18317) |
| 319 | + self.register_datapack(data.Data_18317, lambda r: 18317 <= r.build < 19595) |
| 320 | + self.register_datapack(data.Data_19595, lambda r: 19595 <= r.build) |
4 | 321 |
|
5 | 322 | __defaultSC2Reader = SC2Reader() |
6 | 323 |
|
|
0 commit comments