@@ -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