Skip to content

Commit a312310

Browse files
committed
Reorganize the Game Summary resource.
New organization has the main contruction logic in the __init__ function and moves all complex load processes into separate methods. This organization will allow for partial loading in the future and manual construction by pieces when necessary.
1 parent 88e1dcf commit a312310

File tree

2 files changed

+214
-205
lines changed

2 files changed

+214
-205
lines changed

sc2reader/resources.py

Lines changed: 214 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -512,15 +512,15 @@ class GameSummary(Resource):
512512
game_speed = str()
513513

514514
#: Game length (real-time)
515-
game_length = int()
515+
real_length = int()
516516

517517
#: Game length (in-game)
518-
game_length_ingame = int()
518+
game_length = int()
519519

520-
#: Lobby properties
520+
#: A dictionary of Lobby properties
521521
lobby_properties = dict()
522522

523-
#: Lobby player properties
523+
#: A dictionary of Lobby player properties
524524
lobby_player_properties = dict()
525525

526526
#: Game completion time
@@ -558,12 +558,14 @@ def __init__(self, summary_file, filename=None, **options):
558558
self.lobby_properties = dict()
559559
self.lobby_player_properties = dict()
560560

561-
self.data = zlib.decompress(summary_file.read()[16:])
561+
# The first 16 bytes appear to be some sort of compression header
562+
buffer = utils.ReplayBuffer(zlib.decompress(summary_file.read()[16:]))
563+
564+
# TODO: Is there a fixed number of entries?
565+
# TODO: Maybe the # of parts is recorded somewhere?
562566
self.parts = list()
563-
buffer = utils.ReplayBuffer(self.data)
564567
while buffer.left:
565-
part = buffer.read_data_struct()
566-
self.parts.append(part)
568+
self.parts.append(buffer.read_data_struct())
567569

568570
# Parse basic info
569571
self.game_speed = GAME_SPEED_CODES[''.join(reversed(self.parts[0][0][1]))]
@@ -573,21 +575,92 @@ def __init__(self, summary_file, filename=None, **options):
573575
# 0, 1 might be an adjustment of some sort
574576
self.unknown_time = self.parts[0][2][2]
575577

576-
# this one is alone
578+
# this one is alone as a unix timestamp
577579
self.time = self.parts[0][8]
578580

579-
self.game_length_ingame = self.parts[0][7]
580-
self.game_length = self.game_length_ingame / GAME_SPEED_FACTOR[self.game_speed]
581+
# in seconds
582+
self.game_length = utils.Length(seconds=self.parts[0][7])
583+
self.real_length = utils.Length(seconds=self.parts[0][7]/GAME_SPEED_FACTOR[self.game_speed])
581584

582-
# parse lobby properties
583-
(self.lobby_properties, self.lobby_player_properties) = utils.get_lobby_properties(self.parts)
585+
self.load_lobby_properties()
586+
self.load_player_info()
587+
self.load_player_graphs()
588+
self.load_map_data()
589+
self.load_player_builds()
590+
591+
def load_player_builds(self):
592+
# Parse build orders
593+
bo_structs = [x[0] for x in self.parts[5:]]
594+
bo_structs.append(self.parts[4][0][3:])
595+
596+
# This might not be the most effective way, but it works
597+
for pid, p in self.player.items():
598+
bo = list()
599+
for bo_struct in bo_structs:
600+
for order in bo_struct:
601+
602+
if order[0][1] >> 24 == 0x01:
603+
# unit
604+
parsed_order = utils.get_unit(order[0][1])
605+
elif order[0][1] >> 24 == 0x02:
606+
# research
607+
parsed_order = utils.get_research(order[0][1])
608+
609+
for entry in order[1][p.pid]:
610+
bo.append({
611+
'supply' : entry[0],
612+
'total_supply' : entry[1]&0xff,
613+
'time' : (entry[2] >> 8) / 16,
614+
'order' : parsed_order,
615+
'build_index' : entry[1] >> 16,
616+
})
617+
bo.sort(key=lambda x: x['build_index'])
618+
self.build_orders[p.pid] = bo
619+
620+
def load_map_data(self):
621+
# Parse map localization data
622+
for l in self.parts[0][6][8]:
623+
lang = l[0]
624+
urls = list()
625+
for hash in l[1]:
626+
parsed_hash = utils.parse_hash(hash)
627+
if not parsed_hash['server']:
628+
continue
629+
urls.append(self.base_url_template.format(parsed_hash['server'], parsed_hash['hash'], parsed_hash['type']))
630+
631+
self.localization_urls[lang] = urls
632+
633+
# Parse map images
634+
for hash in self.parts[0][6][7]:
635+
parsed_hash = utils.parse_hash(hash)
636+
self.image_urls.append(self.base_url_template.format(parsed_hash['server'], parsed_hash['hash'], parsed_hash['type']))
637+
638+
def load_player_graphs(self):
639+
# Parse graph and stats stucts, for each player
640+
for pid, p in self.player.items():
641+
print type(pid), type(p)
642+
# Graph stuff
643+
xy = [(o[2], o[0]) for o in self.parts[4][0][2][1][p.pid]]
644+
p.army_graph = Graph([], [], xy_list=xy)
645+
646+
xy = [(o[2], o[0]) for o in self.parts[4][0][1][1][p.pid]]
647+
p.income_graph = Graph([], [], xy_list=xy)
648+
649+
# Stats stuff
650+
stats_struct = self.parts[3][0]
651+
# The first group of stats is located in parts[3][0]
652+
for i in range(len(stats_struct)):
653+
p.stats[self.stats_keys[i]] = stats_struct[i][1][p.pid][0][0]
654+
# The last piece of stats is in parts[4][0][0][1]
655+
p.stats[self.stats_keys[len(stats_struct)]] = self.parts[4][0][0][1][p.pid][0][0]
584656

657+
def load_player_info(self):
585658
# Parse player structs, 16 is the maximum amount of players
586659
for i in range(16):
587-
player = None
588660
# Check if player, skip if not
589661
if self.parts[0][3][i][2] == '\x00\x00\x00\x00':
590662
continue
663+
591664
player_struct = self.parts[0][3][i]
592665

593666
player = PlayerSummary(player_struct[0][0])
@@ -624,76 +697,138 @@ def __init__(self, summary_file, filename=None, **options):
624697
self.team[player.teamid].append(player.pid)
625698
self.teams = [self.team[tid] for tid in sorted(self.team.keys())]
626699

627-
628-
# Parse graph and stats stucts, for each player
629-
for pid, p in self.player.items():
630-
print type(pid), type(p)
631-
# Graph stuff
632-
xy = [(o[2], o[0]) for o in self.parts[4][0][2][1][p.pid]]
633-
p.army_graph = Graph([], [], xy_list=xy)
634-
635-
xy = [(o[2], o[0]) for o in self.parts[4][0][1][1][p.pid]]
636-
p.income_graph = Graph([], [], xy_list=xy)
637-
638-
# Stats stuff
639-
stats_struct = self.parts[3][0]
640-
# The first group of stats is located in parts[3][0]
641-
for i in range(len(stats_struct)):
642-
p.stats[self.stats_keys[i]] = stats_struct[i][1][p.pid][0][0]
643-
# The last piece of stats is in parts[4][0][0][1]
644-
p.stats[self.stats_keys[len(stats_struct)]] = self.parts[4][0][0][1][p.pid][0][0]
645-
646-
# Parse map localization data
647-
for l in self.parts[0][6][8]:
648-
lang = l[0]
649-
urls = list()
650-
for hash in l[1]:
651-
parsed_hash = utils.parse_hash(hash)
652-
if parsed_hash['server'] == '\x00\x00':
700+
def load_lobby_properties(self):
701+
#Monster function used to parse lobby properties in GameSummary
702+
#
703+
# The definition of each lobby property is in data[0][5] with the structure
704+
#
705+
# id = def[0][1] # The unique property id
706+
# vals = def[1] # A list with the values the property can be
707+
# reqs = def[3] # A list of requirements the property has
708+
# dflt = def[8] # The default value(s) of the property
709+
# this is a single entry for a global property
710+
# and a list() of entries for a player property
711+
712+
# The def-values is structured like this
713+
#
714+
# id = `the index in the vals list`
715+
# name = v[0] # The name of the value
716+
717+
# The requirement structure looks like this
718+
#
719+
# id = r[0][1][1] # The property id of this requirement
720+
# vals = r[1] # A list of names of valid values for this requirement
721+
722+
###
723+
# The values of each property is in data[0][6][6] with the structure
724+
#
725+
# id = v[0][1] # The property id of this value
726+
# vals = v[1] # The value(s) of this property
727+
# this is a single entry for a global property
728+
# and a list() of entries for a player property
729+
730+
###
731+
# A value-entry looks like this
732+
#
733+
# index = v[0] # The index in the def.vals array representing the value
734+
# unknown = v[1]
735+
736+
# TODO: this indirection is confusing, fix at some point..
737+
data = self.parts
738+
739+
# First get the definitions in data[0][5]
740+
defs = dict()
741+
for d in data[0][5]:
742+
k = d[0][1]
743+
defs[k] = {
744+
'id':k,
745+
'vals':d[1],
746+
'reqs':d[3],
747+
'dflt':d[8],
748+
'lobby_prop':type(d[8]) == type(dict())
749+
}
750+
vals = dict()
751+
752+
# Get the values in data[0][6][6]
753+
for v in data[0][6][6]:
754+
k = v[0][1]
755+
vals[k] = {
756+
'id':k,
757+
'vals':v[1]
758+
}
759+
760+
lobby_ids = [k for k in defs if defs[k]['lobby_prop']]
761+
lobby_ids.sort()
762+
player_ids = [k for k in defs if not defs[k]['lobby_prop']]
763+
player_ids.sort()
764+
765+
left_lobby = deque([k for k in defs if defs[k]['lobby_prop']])
766+
767+
lobby_props = dict()
768+
# We cycle through all property values 'til we're done
769+
while len(left_lobby) > 0:
770+
propid = left_lobby.popleft()
771+
can_be_parsed = True
772+
active = True
773+
# Check the requirements
774+
for req in defs[propid]['reqs']:
775+
can_be_parsed = can_be_parsed and (req[0][1][1] in lobby_props)
776+
# Have we parsed all req-fields?
777+
if not can_be_parsed:
778+
break
779+
# Is this requirement fullfilled?
780+
active = active and (lobby_props[req[0][1][1]] in req[1])
781+
782+
if not can_be_parsed:
783+
# Try parse this later
784+
left_lobby.append(propid)
785+
continue
786+
if not active:
787+
# Ok, so the reqs weren't fullfilled, don't use this property
788+
continue
789+
# Nice! We've parsed a property
790+
lobby_props[propid] = defs[propid]['vals'][vals[propid]['vals'][0]][0]
791+
792+
player_props = [dict() for pid in range(16)]
793+
# Parse each player separately (this is required :( )
794+
for pid in range(16):
795+
left_players = deque([a for a in player_ids])
796+
player = dict()
797+
798+
# Use this to avoid an infinite loop
799+
last_success = 0
800+
max = len(left_players)
801+
while len(left_players) > 0 and not (last_success > max+1):
802+
last_success += 1
803+
propid = left_players.popleft()
804+
can_be_parsed = True
805+
active = True
806+
for req in defs[propid]['reqs']:
807+
#req is a lobby prop
808+
if req[0][1][1] in lobby_ids:
809+
active = active and (req[0][1][1] in lobby_props) and (lobby_props[req[0][1][1]] in req[1])
810+
#req is a player prop
811+
else:
812+
can_be_parsed = can_be_parsed and (req[0][1][1] in player)
813+
if not can_be_parsed:
814+
break
815+
active = active and (player[req[0][1][1]] in req[1])
816+
817+
if not can_be_parsed:
818+
left_players.append(propid)
653819
continue
654-
urls.append(self.base_url_template.format(parsed_hash['server'], parsed_hash['hash'], parsed_hash['type']))
655-
656-
self.localization_urls[lang] = urls
657-
658-
# Parse map images
659-
for hash in self.parts[0][6][7]:
660-
parsed_hash = utils.parse_hash(hash)
661-
self.image_urls.append(self.base_url_template.format(parsed_hash['server'], parsed_hash['hash'], parsed_hash['type']))
662-
663-
# Parse build orders
664-
bo_structs = [x[0] for x in self.parts[5:]]
665-
bo_structs.append(self.parts[4][0][3:])
666-
667-
# This might not be the most effective way, but it works
668-
for pid, p in self.player.items():
669-
bo = list()
670-
for bo_struct in bo_structs:
671-
for order in bo_struct:
672-
673-
if order[0][1] >> 24 == 0x01:
674-
# unit
675-
parsed_order = utils.get_unit(order[0][1])
676-
elif order[0][1] >> 24 == 0x02:
677-
# research
678-
parsed_order = utils.get_research(order[0][1])
820+
last_success = 0
821+
if not active:
822+
continue
823+
player[propid] = defs[propid]['vals'][vals[propid]['vals'][pid][0]][0]
679824

680-
for entry in order[1][p.pid]:
681-
bo.append({
682-
'supply' : entry[0],
683-
'total_supply' : entry[1]&0xff,
684-
'time' : (entry[2] >> 8) / 16,
685-
'order' : parsed_order,
686-
'build_index' : entry[1] >> 16,
687-
})
688-
bo.sort(key=lambda x: x['build_index'])
689-
self.build_orders[p.pid] = bo
825+
player_props[pid] = player
690826

827+
self.lobby_props = lobby_props
828+
self.player_props = player_props
691829

692830
def __str__(self):
693-
return "{} - {:0>2}:{:0>2}:{:0>2} {}".format(time.ctime(self.time),
694-
int(self.game_length)/3600,
695-
(int(self.game_length)%3600)/60,
696-
(int(self.game_length)%3600)%60,
831+
return "{} - {} {}".format(time.ctime(self.time),self.game_length,
697832
'v'.join(''.join(self.players[p].race[0] for p in self.teams[tid]) for tid in self.teams))
698833

699834

0 commit comments

Comments
 (0)