Skip to content

Commit 9a4fc60

Browse files
committed
Completely restructures the back end to be configurable and more pluggable. Also fixes several bugs. This should have been broken into about 100 commits
1 parent 5c2ff21 commit 9a4fc60

File tree

7 files changed

+1135
-439
lines changed

7 files changed

+1135
-439
lines changed

sc2reader/__init__.py

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,86 @@
1-
from replay import Replay
1+
from mpyq import MPQArchive
2+
from utils import ByteStream
3+
from objects import Replay
4+
from readers import *
5+
from processors import *
6+
from collections import OrderedDict
27

3-
__version__ = "0.1.0"
8+
__version__ = "0.1.0"
9+
10+
import os
11+
12+
class DefaultConfig(object):
13+
def __init__(self):
14+
self.readers = OrderedDict()
15+
self.readers['replay.initData'] = [ ReplayInitDataReader() ]
16+
self.readers['replay.details'] = [ ReplayDetailsReader() ]
17+
self.readers['replay.attributes.events'] = [ AttributeEventsReader_17326(), AttributeEventsReader() ]
18+
self.readers['replay.message.events'] = [ MessageEventsReader() ]
19+
self.readers['replay.game.events'] = [ GameEventsReader_17326(), GameEventsReader_16561(), GameEventsReader() ]
20+
21+
self.processors = [
22+
PeopleProcessor,
23+
AttributeProcessor,
24+
TeamsProcessor,
25+
MessageProcessor,
26+
RecorderProcessor,
27+
EventProcessor,
28+
ApmProcessor,
29+
ResultsProcessor
30+
]
31+
32+
def read(location,config=DefaultConfig()):
33+
if not os.path.exists(location):
34+
raise ValueError("Location must exist")
35+
36+
if os.path.isdir(location):
37+
replays = list()
38+
for location in os.list_files(location):
39+
replays.extend(read(location,config))
40+
return replays
41+
else:
42+
return read_file(location,config)
43+
44+
def read_file(filename,config=DefaultConfig()):
45+
if(os.path.splitext(filename)[1].lower() != '.sc2replay'):
46+
raise TypeError("Target file must of the SC2Replay file extension")
47+
48+
with open(filename) as replay_file:
49+
release,frames = read_header(replay_file)
50+
replay = Replay(filename,release,frames)
51+
archive = MPQArchive(filename,listfile=False)
52+
53+
#Extract and Parse the relevant files
54+
for file,readers in config.readers.iteritems():
55+
for reader in readers:
56+
if reader.reads(replay.build):
57+
reader.read(archive.read_file(file),replay)
58+
break
59+
else:
60+
raise NotYetImplementedError("No parser was found that accepted the replay file;check configuration")
61+
62+
#Do cleanup and post processing
63+
for process in config.processors:
64+
replay = process(replay)
65+
66+
return replay
67+
68+
def read_header(file):
69+
source = ByteStream(file.read())
70+
71+
#Check the file type for the MPQ header bytes
72+
if source.get_hex(4).upper() != "4D50511B":
73+
raise ValueError("File '%s' is not an MPQ file" % file.name)
74+
75+
#Extract replay header data, we don't actually use this for anything
76+
max_data_size = source.get_little_32() #possibly data max size
77+
header_offset = source.get_little_32() #Offset of the second header
78+
data_size = source.get_little_32() #possibly data size
79+
80+
#Extract replay attributes from the mpq
81+
data = source.parse_serialized_data()
82+
83+
#return the release and frames information
84+
return data[1],data[3]
85+
86+
__all__ = [DefaultConfig,read,read_file]

sc2reader/objects.py

Lines changed: 117 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,53 @@
11
from data import races
2+
from collections import defaultdict
3+
from sc2reader.utils import ByteStream, PersonDict
24

5+
class Replay(object):
6+
7+
def __init__(self, filename, release, frames=0):
8+
#Split and offset for matching indexes if they pass in the release string
9+
if isinstance(release,basestring): release = [None]+release.split('.')
10+
11+
#Assign all the relevant information to the replay object
12+
self.build = release[4]
13+
self.versions = (release[1], release[2], release[3], release[4])
14+
self.release_string = "%s.%s.%s.%s" % self.versions
15+
self.frames, self.seconds = (frames, frames/16)
16+
self.length = (self.seconds/60, self.seconds%60)
17+
18+
self.player_names = list()
19+
self.other_people = set()
20+
self.filename = filename
21+
self.speed = ""
22+
self.type = ""
23+
self.category = ""
24+
self.is_ladder = False
25+
self.is_private = False
26+
self.map = ""
27+
self.realm = ""
28+
self.events = list()
29+
self.results = dict()
30+
self.teams = defaultdict(list)
31+
self.observers = list() #Unordered list of Observer
32+
self.players = list() #Unordered list of Player
33+
self.people = list() #Unordered list of Players+Observers
34+
self.person = PersonDict() #Maps pid to Player/Observer
35+
self.events_by_type = dict()
36+
self.attributes = list()
37+
self.messages = list()
38+
self.recorder = None # Player object
39+
self.winner_known = False
40+
41+
# Set in parsers.DetailParser.load, should we hide this?
42+
self.file_time = None # Probably number milliseconds since EPOCH
43+
44+
# TODO: Test EPOCH differences between MacOsX and Windows
45+
# http://en.wikipedia.org/wiki/Epoch_(reference_date)
46+
# Notice that Windows and Mac have different EPOCHs, I wonder whether
47+
# this is different depending on the OS on which the replay was played.
48+
self.date = None # Date when the game was played in local time
49+
self.utc_date = None # Date when the game was played in UTC
50+
351
class Attribute(object):
452

553
def __init__(self, data):
@@ -92,58 +140,11 @@ def __repr__(self):
92140

93141
def __str__(self):
94142
return "%s: %s" % (self.name, self.value)
95-
96-
97-
98-
99-
class Event(object):
100-
def __init__(self, elapsed_time, event_type, event_code, global_flag, player_id,
101-
location=None, bytes=""):
102-
self.time, self.seconds = (elapsed_time, elapsed_time/16)
103-
self.timestr = "%s:%s" % (self.seconds/60, str(self.seconds%60).rjust(2, "0"))
104-
self.type = event_type
105-
self.code = event_code
106-
self.is_local = (global_flag == 0x0)
107-
self.player = player_id
108-
self.location = location
109-
self.bytes = bytes
110-
self.abilitystr = ""
111-
112-
# Added for convenience
113-
self.is_init = (event_type == 0x00)
114-
self.is_player_action = (event_type == 0x01)
115-
self.is_camera_movement = (event_type == 0x03)
116-
self.is_unknown = (event_type == 0x02 or event_type == 0x04 or event_type == 0x05)
117-
118-
def __call__(self, elapsed_time, event_type, global_flag, player_id, event_code, bytes):
119-
self.time, self.seconds = (elapsed_time, elapsed_time/16)
120-
self.timestr = "%s:%s" % (self.seconds/60, str(self.seconds%60).rjust(2, "0"))
121-
self.type = event_type
122-
self.code = event_code
123-
self.is_local = (global_flag == 0x0)
124-
self.player = player_id
125-
self.bytes = ""
126-
self.abilitystr = ""
127-
128-
# Added for convenience
129-
self.is_init = (event_type == 0x00)
130-
self.is_player_action = (event_type == 0x01)
131-
self.is_camera_movement = (event_type == 0x03)
132-
self.is_unknown = (event_type == 0x02 or event_type == 0x04 or event_type == 0x05)
133-
134-
self.parse(bytes)
135-
return self
136-
137-
def __str__(self):
138-
return "%s - %s - %s" % (self.timestr, self.name, self.abilitystr)
139-
140-
def __repr__(self):
141-
return str(self)
142-
143+
143144
class Message(object):
144145

145-
def __init__(self, time, player, target, text):
146-
self.time, self.sender, self.target, self.text = time, player, target, text
146+
def __init__(self, time, pid, target, text):
147+
self.time, self.sender_id, self.target, self.text = time, pid, target, text
147148
self.seconds = time/16
148149
self.sent_to_all = (self.target == 0)
149150
self.sent_to_allies = (self.target == 2)
@@ -156,31 +157,29 @@ def __repr__(self):
156157
return str(self)
157158

158159
# Actor is a base class for Observer and Player
159-
class Actor(object):
160-
def __init__(self, is_obs):
161-
self.pid = None
162-
self.name = None
163-
self.is_obs = is_obs
160+
class Person(object):
161+
def __init__(self, pid, name):
162+
self.pid = pid
163+
self.name = name
164+
self.is_obs = None
164165
self.messages = list()
165166
self.events = list()
166-
self.recorder = True # Actual recorder will be determined using the replay.message.events file
167+
self.recorder = False # Actual recorder will be determined using the replay.message.events file
167168

168-
class Observer(Actor):
169+
class Observer(Person):
169170
def __init__(self, pid, name):
170-
Actor.__init__(self, True)
171-
self.pid = pid
172-
self.name = name
171+
super(Observer,self).__init__(pid,name)
172+
self.is_obs = True
173173

174-
class Player(Actor):
174+
class Player(Person):
175175

176176
url_template = "http://%s.battle.net/sc2/en/profile/%s/%s/%s/"
177177

178178
def __init__(self, pid, data, realm="us"):
179-
Actor.__init__(self, False)
180179
# TODO: get a map of realm,subregion => region in here
181-
self.pid = pid
180+
super(Player,self).__init__(pid,data[0].decode("hex"))
181+
self.is_obs = False
182182
self.realm = realm
183-
self.name = data[0].decode("hex")
184183
self.uid = data[1][4]
185184
self.subregion = data[1][2]
186185
self.url = self.url_template % (self.realm, self.uid, self.subregion, self.name)
@@ -197,6 +196,8 @@ def __init__(self, pid, data, realm="us"):
197196
['b', data[3][3]],
198197
['a', data[3][0]],
199198
])
199+
200+
self.result = None
200201
self.color_hex = "%02X%02X%02X" % (data[3][1], data[3][2], data[3][3])
201202
self.color_text = "" # The text of the player color (Red, Blue, etc) to be supplied later
202203
self.handicap = data[6]
@@ -211,3 +212,55 @@ def __str__(self):
211212

212213
def __repr__(self):
213214
return str(self)
215+
216+
217+
218+
class Event(object):
219+
def __init__(self, elapsed_time, event_type, event_code, player_id):
220+
self.time, self.seconds = (elapsed_time, elapsed_time/16)
221+
self.timestr = "%s:%s" % (self.seconds/60, str(self.seconds%60).rjust(2, "0"))
222+
self.type = event_type
223+
self.code = event_code
224+
self.is_local = (player_id != 16)
225+
self.player = player_id
226+
self.bytes = bytes
227+
self.abilitystr = ""
228+
229+
# Added for convenience
230+
self.is_init = (event_type == 0x00)
231+
self.is_player_action = (event_type == 0x01)
232+
self.is_camera_movement = (event_type == 0x03)
233+
self.is_unknown = (event_type == 0x02 or event_type == 0x04 or event_type == 0x05)
234+
235+
def __str__(self):
236+
return "%s - %s - %s" % (self.timestr, self.name, self.abilitystr)
237+
238+
def __repr__(self):
239+
return str(self)
240+
241+
class UnknownEvent(Event):
242+
name = 'UnknownEvent'
243+
244+
class PlayerJoinEvent(Event):
245+
name = 'PlayerJoin'
246+
247+
class GameStartEvent(Event):
248+
name = 'GameStart'
249+
250+
class PlayerLeaveEvent(Event):
251+
name = 'PlayerLeave'
252+
253+
class AbilityEvent(Event):
254+
name = 'AbilityEvent'
255+
256+
class ResourceTransferEvent(Event):
257+
name = 'ResourceTransfer'
258+
259+
class HotkeyEvent(Event):
260+
name = 'HotkeyEvent'
261+
262+
class SelectionEvent(Event):
263+
name = 'SelectionEvent'
264+
265+
class CameraMovementEvent(Event):
266+
name = 'CameraMovement'

0 commit comments

Comments
 (0)