Skip to content

Commit 7e8273e

Browse files
committed
merged graylin's master
2 parents 714bec4 + 080e426 commit 7e8273e

File tree

11 files changed

+118
-123
lines changed

11 files changed

+118
-123
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
*.pyc
33
dist
44
build
5-
sc2reader.egg-info
5+
sc2reader.egg-info
6+
PKG-INFO.txt

CONTRIBUTORS.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Author and Maintainer:
2+
Graylin Kim - graylinkim @ github
3+
4+
Contributors:
5+
Alexander Hanhikoski - alexhanh @ github

README.txt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,17 @@ Advanced Install
9090
$ python setup.py install
9191
$ sc2printer 'path/to/replay.sc2replay'
9292

93-
Contributors
93+
Testing
9494
~~~~~~~~~~~~~
9595

96+
sc2reader is tested using the `pytest`_ module. This module can be installed
97+
using ``pip`` or ``easy_install``.
98+
9699
::
97100

98101
$ easy_install pytest
99102

100-
In order to run the tests, you need to be in the root directory of sc2reader. Then:
103+
In order to run the tests, you need to be in the root directory of sc2reader.
101104

102105
::
103106

@@ -118,4 +121,4 @@ Until some further infrastructure is set up:
118121
.. _wiki: https://github.com/GraylinKim/sc2reader/wiki
119122
.. _phpsc2replay: http://code.google.com/p/phpsc2replay/
120123
.. _pytest: http://pytest.org/
121-
.. _issue tracker: https://github.com/GraylinKim/sc2reader/issues
124+
.. _issue tracker: https://github.com/GraylinKim/sc2reader/issues

sc2reader/objects.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,13 @@ def __repr__(self):
139139

140140
class Player(object):
141141

142-
def __init__(self, pid, data):
142+
def __init__(self, pid, data, realm="us"):
143143
self.pid = pid
144+
self.realm = realm
144145
self.name = data[0].decode("hex")
145146
self.uid = data[1][4]
146147
self.uidIndex = data[1][2]
147-
self.url = "http://us.battle.net/sc2/en/profile/%s/%s/%s/" % (self.uid, self.uidIndex, self.name)
148+
self.url = "http://%s.battle.net/sc2/en/profile/%s/%s/%s/" % (self.realm, self.uid, self.uidIndex, self.name)
148149
self.race = data[2].decode("hex")
149150
self.rgba = dict([
150151
['r', data[3][1]],
@@ -154,6 +155,8 @@ def __init__(self, pid, data):
154155
])
155156
self.recorder = True
156157
self.handicap = data[6]
158+
self.team = None # A number to be supplied later
159+
self.type = "" # Human or Computer
157160

158161
def __str__(self):
159162
return "Player %s - %s (%s)" % (self.pid, self.name, self.race)

sc2reader/parsers.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from time import ctime
22
from collections import defaultdict
33

4-
from objects import attribute, Message, Player, Event
5-
from EventParsers import *
4+
from objects import Attribute, Message, Player, Event
5+
from eventparsers import *
66
from utils import ByteStream
77
from exceptions import ParseError
88

@@ -14,7 +14,7 @@
1414
#################################################
1515
def get_detail_parser(build):
1616
#This file format appears to have never changed
17-
return detail_parser()
17+
return DetailParser()
1818

1919
def get_attribute_parser(build):
2020
if build >= 17326:
@@ -36,6 +36,29 @@ def get_message_parser(build):
3636
#format appears to have not changed
3737
return MessageParser()
3838

39+
def get_initdata_parser(build):
40+
return InitdataParser()
41+
42+
class InitdataParser(object):
43+
def load(self,replay,filecontents):
44+
bytes = ByteStream(filecontents)
45+
num_players = bytes.get_big_int(1)
46+
for p in range(0,num_players):
47+
name_length = bytes.get_big_int(1)
48+
name = bytes.get_string(name_length)
49+
bytes.skip(5)
50+
51+
bytes.skip(5) # Unknown
52+
bytes.get_string(4) # Always Dflt
53+
bytes.skip(15) #Unknown
54+
id_length = bytes.get_big_int(1)
55+
sc_account_id = bytes.get_string(id_length)
56+
bytes.skip(684) # Fixed Length data for unknown purpose
57+
while( bytes.get_string(4).lower() == 's2ma' ):
58+
bytes.skip(2)
59+
replay.realm = bytes.get_string(2).lower()
60+
unknown_map_hash = bytes.get_big(32)
61+
3962
#################################################
4063
# replay.attributes.events Parsing classes
4164
#################################################
@@ -54,7 +77,7 @@ def load_attribute(self, replay, bytes):
5477
]
5578

5679
#Complete the decoding in the attribute object
57-
return attribute(attr_data)
80+
return Attribute(attr_data)
5881

5982
def load(self, replay, filecontents):
6083
bytes = ByteStream(filecontents)
@@ -80,18 +103,17 @@ def load(self, replay, filecontents):
80103
#Set player attributes as available, requires already populated player list
81104
for pid, attributes in data.iteritems():
82105
if pid == 16: continue
83-
player = replay.players[pid]
106+
player = replay.player[pid]
84107
player.color = attributes['Color']
85108
player.team = attributes['Teams'+replay.type]
86109
player.race2 = attributes['Race']
87110
player.difficulty = attributes['Difficulty']
88-
89111
#Computer players can't record games
90112
player.type = attributes['Player Type']
91113
if player.type == "Computer":
92114
player.recorder = False
93115

94-
class AttributeParser_17326(attribute_parser):
116+
class AttributeParser_17326(AttributeParser):
95117
def load_header(self, replay, bytes):
96118
bytes.skip(5, byte_code=True) #Always start with 4 nulls
97119
self.count = bytes.get_little_int(4) #get total attribute count
@@ -103,13 +125,14 @@ class DetailParser(object):
103125
def load(self, replay, filecontents):
104126
data = ByteStream(filecontents).parse_serialized_data()
105127

106-
replay.players = [None] #Pad the front for proper IDs
107128
for pid, pdata in enumerate(data[0]):
108-
replay.players.append(Player(pid+1, pdata)) #shift the id to start @ 1
129+
replay.add_player(Player(pid+1, pdata, replay.realm)) #shift the id to start @ 1
109130

110131
replay.map = data[1].decode("hex")
111132
replay.file_time = data[5]
112133
replay.date = ctime( (data[5]-116444735995904000)/10000000 )
134+
135+
replay.details_data = data
113136

114137
##################################################
115138
# replay.message.events parsing classes
@@ -134,7 +157,7 @@ def load(self, replay, filecontents):
134157
elif flags & 0x0F == 0:
135158
bytes.skip(4)
136159
if player_id < len(replay.players):
137-
replay.players[player_id].recorder = False
160+
replay.player[player_id].recorder = False
138161
else:
139162
pass #This "player" is an observer or something
140163

sc2reader/replay.py

Lines changed: 54 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,19 @@
1010
class 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

176191
if __name__ == '__main__':

sc2reader/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def get_count(self, byte_code=False):
7272
return num/2, byte_string
7373
return num/2
7474

75-
def getTimestamp(self, byte_code=False):
75+
def get_timestamp(self, byte_code=False):
7676
#Get the first byte
7777
byte, byte_string = self.get_big_int(1, byte_code=True)
7878

scripts/sc2printer

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!c:\\Python27\python.exe
22

33
import os, sys
4+
sys.path.append(r'C:\Users\graylinkim\sc2reader')
45
from sc2reader import Replay
56
from sc2reader.exceptions import ParseError
67

shell.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from sc2reader import Replay
2+
from mpyq import MPQArchive

0 commit comments

Comments
 (0)