+ def next_item(self):
+ """Returns the next key:value pair off the queue."""
+ try:
+ items = self.q.popleft().strip().split(' ', 1)
+ if len(items) == 1:
+ # Some keys won't have values, like 'L' records where the server isn't actually
+ # participating in any ladders. These can be safely ignored.
+ return None, None
+ else:
+ return items
+ except:
+ return None, None
+
+ def add_weapon_fired(self, sub_key):
+ """Adds a weapon to the set of weapons fired during the match (a set)."""
+ self.weapons.add(sub_key.split("-")[1])
+
+ @staticmethod
+ def is_human_player(player):
+ """
+ Determines if a given set of events correspond with a non-bot
+ """
+ return not player['P'].startswith('bot')
+
+ @staticmethod
+ def played_in_game(player):
+ """
+ Determines if a given set of player events correspond with a player who
+ played in the game (matches 1 and scoreboardvalid 1)
+ """
+ return 'matches' in player and 'scoreboardvalid' in player
+
+ def parse_player(self, key, pid):
+ """Construct a player events listing from the submission."""
+
+ # all of the keys related to player records
+ player_keys = ['i', 'n', 't', 'e']
+
+ player = {key: pid}
+
+ player_fired_weapon = False
+ player_nonzero_score = False
+ player_fastest = False
+
+ # Consume all following 'i' 'n' 't' 'e' records
+ while len(self.q) > 0:
+ (key, value) = self.next_item()
+ if key is None and value is None:
+ continue
+ elif key == 'e':
+ (sub_key, sub_value) = value.split(' ', 1)
+ player[sub_key] = sub_value
+
+ if sub_key.endswith("cnt-fired"):
+ player_fired_weapon = True
+ self.add_weapon_fired(sub_key)
+ elif sub_key == 'scoreboard-score' and int(sub_value) != 0:
+ player_nonzero_score = True
+ elif sub_key == 'scoreboard-fastest':
+ player_fastest = True
+ elif key == 'n':
+ player[key] = unicode(value, 'utf-8')
+ elif key in player_keys:
+ player[key] = value
+ else:
+ # something we didn't expect - put it back on the deque
+ self.q.appendleft("{} {}".format(key, value))
+ break
+
+ played = self.played_in_game(player)
+ human = self.is_human_player(player)
+
+ if played and human:
+ self.humans.append(player)
+
+ if player_fired_weapon:
+ self.human_fired_weapon = True
+
+ if player_nonzero_score:
+ self.human_nonzero_score = True
+
+ if player_fastest:
+ self.human_fastest = True
+
+ elif played and not human:
+ self.bots.append(player)
+
+ self.players.append(player)
+
+ def parse_team(self, key, tid):
+ """Construct a team events listing from the submission."""
+ team = {key: tid}
+
+ # Consume all following 'e' records
+ while len(self.q) > 0 and self.q[0].startswith('e'):
+ (_, value) = self.next_item()
+ (sub_key, sub_value) = value.split(' ', 1)
+ team[sub_key] = sub_value
+
+ self.teams.append(team)
+
+ def parse(self):
+ """Parses the request body into instance variables."""
+ while len(self.q) > 0:
+ (key, value) = self.next_item()
+ if key is None and value is None:
+ continue
+ elif key == 'V':
+ self.version = value
+ elif key == 'R':
+ self.revision = value
+ elif key == 'G':
+ self.game_type_cd = value
+ elif key == 'O':
+ self.mod = value
+ elif key == 'M':
+ self.map_name = value
+ elif key == 'I':
+ self.match_id = value
+ elif key == 'S':
+ self.server_name = unicode(value, 'utf-8')
+ elif key == 'C':
+ self.impure_cvar_changes = int(value)
+ elif key == 'U':
+ self.port_number = int(value)
+ elif key == 'D':
+ self.duration = datetime.timedelta(seconds=int(round(float(value))))
+ elif key == 'L':
+ self.ladder = value
+ elif key == 'Q':
+ self.parse_team(key, value)
+ elif key == 'P':
+ self.parse_player(key, value)
+ else:
+ raise Exception("Invalid submission")
+
+ return self
+
+ def __repr__(self):
+ """Debugging representation of a submission."""
+ return "game_type_cd: {}, mod: {}, players: {}, humans: {}, bots: {}, weapons: {}".format(
+ self.game_type_cd, self.mod, len(self.players), len(self.humans), len(self.bots),
+ self.weapons)
+
+
+def elo_submission_category(submission):
+ """Determines the Elo category purely by what is in the submission data."""
+ mod = submission.mod
+
+ vanilla_allowed_weapons = {"shotgun", "devastator", "blaster", "mortar", "vortex", "electro",
+ "arc", "hagar", "crylink", "machinegun"}
+ insta_allowed_weapons = {"vaporizer", "blaster"}
+ overkill_allowed_weapons = {"hmg", "vortex", "shotgun", "blaster", "machinegun", "rpc"}
+
+ if mod == "Xonotic":
+ if len(submission.weapons - vanilla_allowed_weapons) == 0:
+ return "vanilla"
+ elif mod == "InstaGib":
+ if len(submission.weapons - insta_allowed_weapons) == 0:
+ return "insta"
+ elif mod == "Overkill":
+ if len(submission.weapons - overkill_allowed_weapons) == 0:
+ return "overkill"
+ else:
+ return "general"