Skip to content

Commit 4d1e4c8

Browse files
committed
Added observers support, currently doesn't work with teams
1 parent 751704d commit 4d1e4c8

File tree

5 files changed

+82
-52
lines changed

5 files changed

+82
-52
lines changed

sc2reader/objects.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ def __init__(self, elapsed_time, event_type, event_code, global_flag, player_id,
105105
self.player = player_id
106106
self.location = location
107107
self.bytes = bytes
108+
self.abilitystr = ""
108109

109110
# Added for convenience
110111
self.is_init = (event_type == 0x00)
@@ -137,6 +138,7 @@ def __str__(self):
137138
def __repr__(self):
138139
return str(self)
139140

141+
# TODO: Refactor message.player to message.sender
140142
class Message(object):
141143

142144
def __init__(self, time, player, target, text):
@@ -152,13 +154,30 @@ def __str__(self):
152154
def __repr__(self):
153155
return str(self)
154156

157+
# Actor is a base class for Observer and Player
158+
class Actor(object):
159+
def __init__(self, is_obs):
160+
self.pid = None
161+
self.name = None
162+
self.team = None
163+
self.is_obs = is_obs
164+
self.messages = list()
165+
self.events = list()
166+
self.recorder = True # Actual recorder will be determined using the replay.message.events file
155167

168+
class Observer(Actor):
169+
def __init__(self, pid, name):
170+
Actor.__init__(self, True)
171+
self.pid = pid
172+
self.name = name
173+
self.team = 0 # Observers share the fictitious team 0
156174

157-
class Player(object):
175+
class Player(Actor):
158176

159177
url_template = "http://%s.battle.net/sc2/en/profile/%s/%s/%s/"
160178

161179
def __init__(self, pid, data, realm="us"):
180+
Actor.__init__(self, False)
162181
# TODO: get a map of realm,subregion => region in here
163182
self.pid = pid
164183
self.realm = realm
@@ -176,11 +195,9 @@ def __init__(self, pid, data, realm="us"):
176195
])
177196
self.color_hex = "%02X%02X%02X" % (data[3][1], data[3][2], data[3][3])
178197
self.color_text = "" # The text of the player color (Red, Blue, etc) to be supplied later
179-
self.recorder = True # Actual recorder will be determined using the replay.message.events file
180198
self.handicap = data[6]
181199
self.team = None # A number to be supplied later
182200
self.type = "" # Human or Computer
183-
self.events = list()
184201
self.avg_apm = 0
185202
self.aps = dict() # Doesn't contain seconds with zero actions
186203
self.apm = dict() # Doesn't contain minutes with zero actions

sc2reader/parsers.py

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from datetime import datetime
33
from collections import defaultdict
44

5-
from objects import Attribute, Message, Player, Event
5+
from objects import Attribute, Message, Player, Observer, Event
66
from eventparsers import *
77
from utils import ByteStream
88
from exceptions import ParseError
@@ -41,12 +41,17 @@ def get_initdata_parser(build):
4141
return InitdataParser()
4242

4343
class InitdataParser(object):
44-
def load(self,replay,filecontents):
44+
def load(self, replay, filecontents):
4545
bytes = ByteStream(filecontents)
4646
num_players = bytes.get_big_8()
47-
for p in range(0,num_players):
47+
for p in range(0, num_players):
4848
name_length = bytes.get_big_8()
4949
name = bytes.get_string(name_length)
50+
51+
# Add every player as observer for now
52+
if name_length > 0:
53+
replay.observers.append(Observer(p+1, name))
54+
5055
bytes.skip(5)
5156

5257
bytes.skip(5) # Unknown
@@ -107,10 +112,10 @@ def load(self, replay, filecontents):
107112

108113
replay.type = data[16]['Game Type']
109114

110-
#Set player attributes as available, requires already populated player list
115+
#Set player attributes as available, requires already populated player list
111116
for pid, attributes in data.iteritems():
112117
if pid == 16: continue
113-
player = replay.player[pid]
118+
player = replay.actor[pid]
114119
player.color_text = attributes['Color']
115120
player.team = attributes['Teams'+replay.type]
116121
player.choosen_race = attributes['Race']
@@ -131,10 +136,17 @@ def load_header(self, replay, bytes):
131136
class DetailParser(object):
132137
def load(self, replay, filecontents):
133138
data = ByteStream(filecontents).parse_serialized_data()
134-
139+
140+
pids = []
135141
for pid, pdata in enumerate(data[0]):
136-
replay.add_player(Player(pid+1, pdata, replay.realm)) #shift the id to start @ 1
137-
142+
pids.append(pid+1)
143+
replay.add_actor(Player(pid+1, pdata, replay.realm)) #shift the id to start @ 1
144+
145+
# Remove actual players from the observers we saved during parsing of initdata.
146+
replay.observers = [obs for obs in replay.observers if not obs.pid in pids]
147+
for observer in replay.observers:
148+
replay.add_actor(observer)
149+
138150
replay.map = data[1].decode("hex")
139151
replay.file_time = data[5]
140152

@@ -170,10 +182,7 @@ def load(self, replay, filecontents):
170182
#some sort of header code
171183
elif flags & 0x0F == 0:
172184
bytes.skip(4)
173-
if player_id <= len(replay.players):
174-
replay.player[player_id].recorder = False
175-
else:
176-
pass #This "player" is an observer or something
185+
replay.actor[player_id].recorder = False
177186

178187
elif flags & 0x80 == 0:
179188
target = flags & 0x03
@@ -187,20 +196,20 @@ def load(self, replay, filecontents):
187196

188197
text = bytes.get_string(length)
189198
try:
190-
replay.messages.append(Message(time, replay.player[player_id], target, text))
199+
replay.messages.append(Message(time, replay.actor[player_id], target, text))
191200
except KeyError:
192201
# This was added because some replay sites added their own tampered
193202
# messages to replays with non-existent player_id.
194203
#
195204
# This will simply ignore and fail silently if such message is
196205
# found.
197206
pass
198-
199-
recorders = [player for player in replay.players if player and player.recorder==True]
207+
208+
recorders = [actor for actor in replay.actors if actor and actor.recorder==True]
200209
if len(recorders) > 1:
201210
raise ValueError("There should be 1 and only 1 recorder; %s were found" % len(recorders))
202211
elif len(recorders) == 0:
203-
#If there are no recorders, then the recorder must not be a player, spectator or referee then
212+
#If there are no recorders, then the recorder must not be a player, spectator or referee then
204213
replay.recorder = None
205214
else:
206215
replay.recorder = recorders[0]

sc2reader/replay.py

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from mpyq import MPQArchive
55

66
import parsers
7-
from utils import ByteStream, PlayerDict
7+
from utils import ByteStream, ActorDict
88

99
class Replay(object):
1010

@@ -22,8 +22,10 @@ def __init__(self, replay, partial_parse=True, full_parse=True):
2222
self.events = list()
2323
self.results = dict()
2424
self.teams = defaultdict(list)
25+
self.observers = list() #Unordered list of Observer
2526
self.players = list() #Unordered list of Player
26-
self.player = PlayerDict() #Maps pid to Player
27+
self.actors = list() #Unordered list of Players+Observers
28+
self.actor = ActorDict() #Maps pid to Player/Observer
2729
self.events_by_type = dict()
2830
self.attributes = list()
2931
self.length = None # (minutes, seconds) tuple
@@ -78,9 +80,14 @@ def __init__(self, replay, partial_parse=True, full_parse=True):
7880
if full_parse:
7981
self._parse_events()
8082

81-
def add_player(self,player):
82-
self.players.append(player)
83-
self.player[player.pid] = player
83+
def add_actor(self, actor):
84+
# Observers were added to self.observers already
85+
86+
if not actor.is_obs:
87+
self.players.append(actor)
88+
89+
self.actor[actor.pid] = actor
90+
self.actors.append(actor)
8491

8592
def _parse_header(self):
8693
#Open up a ByteStream for its contents
@@ -151,27 +158,25 @@ def _parse_events(self):
151158
self.events_by_type[event.name].append(event)
152159

153160
if event.is_local:
154-
# TODO: This will probably break with observers because events
155-
# are recorded for observers but they are not added to self.players
156-
player = self.player[event.player]
157-
player.events.append(event)
161+
actor = self.actor[event.player]
162+
actor.events.append(event)
158163

159164
# Calculate APS, APM and average
160-
if event.is_player_action:
161-
if event.seconds in player.aps:
162-
player.aps[event.seconds] += 1
165+
if not actor.is_obs and event.is_player_action:
166+
if event.seconds in actor.aps:
167+
actor.aps[event.seconds] += 1
163168
else:
164-
player.aps[event.seconds] = 1
169+
actor.aps[event.seconds] = 1
165170

166171
minute = event.seconds/60
167-
if minute in player.apm:
168-
player.apm[minute] += 1
172+
if minute in actor.apm:
173+
actor.apm[minute] += 1
169174
else:
170-
player.apm[minute] = 1
175+
actor.apm[minute] = 1
171176

172-
player.avg_apm += 1
177+
actor.avg_apm += 1
173178

174-
# Average the APM
179+
# Average the APM for actual players
175180
for player in self.players:
176181
player.avg_apm /= player.events[-1].seconds/60.0
177182

@@ -187,7 +192,7 @@ def _process_results(self):
187192
for event in self.events_by_type['leave']:
188193
#Some spectator actions seem to be recorded, they aren't on teams anyway
189194
if event.player <= len(self.players):
190-
team = self.player[event.player].team
195+
team = self.actor[event.player].team
191196
self.results[team] -= 1
192197

193198
#mark all teams with no players left as losing, save the rest of the teams

sc2reader/utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
class PlayerDict(dict):
1+
class ActorDict(dict):
22
"""Delete is supported on the pid index only"""
33
def __init__(self, *args, **kwargs):
44
self._key_map = dict()
@@ -17,7 +17,7 @@ def __getitem__(self, key):
1717
if isinstance(key, str):
1818
key = self._key_map[key]
1919

20-
return super(PlayerDict, self).__getitem__(key)
20+
return super(ActorDict, self).__getitem__(key)
2121

2222
def __setitem__(self, key, value):
2323
if isinstance(key, str):
@@ -26,7 +26,7 @@ def __setitem__(self, key, value):
2626
elif isinstance(key, int):
2727
self._key_map[value.name] = key
2828

29-
super(PlayerDict, self).__setitem__(value.pid, value)
29+
super(ActorDict, self).__setitem__(value.pid, value)
3030

3131

3232
from cStringIO import StringIO

test_replays/test_all.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,15 @@ def test_standard_1v1():
3434
assert replay.is_private == False
3535

3636
assert len(replay.players) == 2
37-
assert replay.player[1].name == "Emperor"
38-
assert replay.player[2].name == "Boom"
39-
emperor = replay.player['Emperor']
37+
assert replay.actor[1].name == "Emperor"
38+
assert replay.actor[2].name == "Boom"
39+
emperor = replay.actor['Emperor']
4040
assert emperor.team == 1
4141
assert emperor.choosen_race == "Protoss"
4242
assert emperor.actual_race == "Protoss"
4343
assert emperor.recorder == False
4444

45-
boom = replay.player['Boom']
45+
boom = replay.actor['Boom']
4646
assert boom.team == 2
4747
assert boom.choosen_race == "Terran"
4848
assert boom.actual_race == "Terran"
@@ -126,27 +126,27 @@ def test_unknown_winner():
126126
def test_random_player():
127127
replay = Replay("test_replays/build17811/3.SC2Replay")
128128

129-
gogeta = replay.player['Gogeta']
129+
gogeta = replay.actor['Gogeta']
130130
assert gogeta.choosen_race == "Random"
131131
assert gogeta.actual_race == "Terran"
132132

133133
def test_random_player2():
134134
replay = Replay("test_replays/build17811/6.SC2Replay")
135-
permafrost = replay.player["Permafrost"]
135+
permafrost = replay.actor["Permafrost"]
136136
assert permafrost.choosen_race == "Random"
137137
assert permafrost.actual_race == "Protoss"
138138

139139
def test_us_realm():
140140
replay = Replay("test_replays/build17811/5.SC2Replay")
141-
assert replay.player['ShadesofGray'].url == "http://us.battle.net/sc2/en/profile/2358439/1/ShadesofGray/"
142-
assert replay.player['reddawn'].url == "http://us.battle.net/sc2/en/profile/2198663/1/reddawn/"
141+
assert replay.actor['ShadesofGray'].url == "http://us.battle.net/sc2/en/profile/2358439/1/ShadesofGray/"
142+
assert replay.actor['reddawn'].url == "http://us.battle.net/sc2/en/profile/2198663/1/reddawn/"
143143

144144
# TODO: Current problem.. both players are set as the recording players
145145
# Waiting for response https://github.com/arkx/mpyq/issues/closed#issue/7
146146
def test_kr_realm_and_tampered_messages():
147147
replay = Replay("test_replays/build17811/11.SC2Replay")
148-
assert replay.player['명지대학교'].url == "http://kr.battle.net/sc2/en/profile/258945/1/명지대학교/"
149-
assert replay.player['티에스엘사기수'].url == "http://kr.battle.net/sc2/en/profile/102472/1/티에스엘사기수/"
148+
assert replay.actor['명지대학교'].url == "http://kr.battle.net/sc2/en/profile/258945/1/명지대학교/"
149+
assert replay.actor['티에스엘사기수'].url == "http://kr.battle.net/sc2/en/profile/102472/1/티에스엘사기수/"
150150

151151
assert replay.messages[0].text == "sc2.replays.net"
152152
assert replay.messages[5].text == "sc2.replays.net"
@@ -159,7 +159,6 @@ def test_footmen():
159159
def test_encrypted():
160160
replay = Replay("test_replays/build17811/4.SC2Replay")
161161

162-
# TODO: Failing
163162
def test_observers():
164163
replay = Replay("test_replays/build17811/13.SC2Replay")
165164

0 commit comments

Comments
 (0)