log = logging.getLogger(__name__)
+def is_real_player(events):
+ """
+ Determines if a given set of events correspond with a non-bot
+ """
+ return not events['P'].startswith('bot')
+
+
+def played_in_game(events):
+ """
+ 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 events and 'scoreboardvalid' in events
+
+
class Submission(object):
"""Parses an incoming POST request for stats submissions."""
# a copy of the HTTP POST body
self.body = body
- # game metadata
- self.meta = {}
+ # the submission code version (from the server)
+ self.version = None
+
+ # the revision string of the server
+ self.revision = None
+
+ # the game type played
+ self.game_type_cd = None
+
+ # the active game mod
+ self.mod = None
+
+ # the name of the map played
+ self.map_name = None
- # raw player events
+ # unique identifier (string) for a match on a given server
+ self.match_id = None
+
+ # the name of the server
+ self.server_name = None
+
+ # the number of cvars that were changed to be different than default
+ self.impure_cvar_changes = None
+
+ # the port number the game server is listening on
+ self.port_number = None
+
+ # how long the game lasted
+ self.duration = None
+
+ # which ladder is being used, if any
+ self.ladder = None
+
+ # players involved in the match (humans, bots, and spectators)
self.players = []
# raw team events
self.teams = []
+ # the parsing deque (we use this to allow peeking)
+ self.q = collections.deque(self.body.split("\n"))
+
+ ############################################################################################
+ # Below this point are fields useful in determining if the submission is valid or
+ # performance optimizations that save us from looping over the events over and over again.
+ ############################################################################################
+
+ # humans who played in the match
+ self.humans = []
+
+ # bots who played in the match
+ self.bots = []
+
# distinct weapons that we have seen fired
self.weapons = set()
- # number of real players in the match
- self.real_players = 0
+ # has a human player fired a shot?
+ self.human_fired_weapon = False
- # the parsing deque (we use this to allow peeking)
- self.q = collections.deque(self.body.split("\n"))
+ # does any human have a non-zero score?
+ self.human_nonzero_score = False
+
+ # does any human have a fastest cap?
+ self.human_fastest = False
def next_item(self):
"""Returns the next key:value pair off the queue."""
return None, None
def check_for_new_weapon_fired(self, sub_key):
- """Checks if a given player key (subkey, actually) is a new weapon fired in the match."""
- if sub_key.endswith("cnt-fired"):
- weapon = sub_key.split("-")[1]
- if weapon not in self.weapons:
- self.weapons.add(weapon)
+ """Checks if a given weapon fired event is a new one for the match."""
+ weapon = sub_key.split("-")[1]
+ if weapon not in self.weapons:
+ self.weapons.add(weapon)
def parse_player(self, key, pid):
"""Construct a player events listing from the submission."""
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()
(sub_key, sub_value) = value.split(' ', 1)
player[sub_key] = sub_value
- # keep track of the distinct weapons fired during the match
- self.check_for_new_weapon_fired(sub_key)
+ if sub_key.endswith("cnt-fired"):
+ player_fired_weapon = True
+ self.check_for_new_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:
self.q.appendleft("{} {}".format(key, value))
break
- if is_real_player(player) and played_in_game(player):
- self.real_players += 1
+ played = played_in_game(player)
+ human = is_real_player(player)
- self.players.append(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)
+ else:
+ self.players.append(player)
def parse_team(self, key, tid):
"""Construct a team events listing from the submission."""
(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.meta[key] = unicode(value, 'utf-8')
- elif key == 'P':
- self.parse_player(key, value)
+ 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:
- self.meta[key] = value
+ raise Exception("Invalid submission")
return self
+def elo_submission_category(submission):
+ """Determines the Elo category purely by what is in the submission data."""
+ mod = submission.meta.get("O", "None")
+
+ 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"
+
+ return "general"
+
+
def parse_stats_submission(body):
"""
Parses the POST request body for a stats submission
)
-def is_real_player(events):
- """
- Determines if a given set of events correspond with a non-bot
- """
- if not events['P'].startswith('bot'):
- return True
- else:
- return False
-
-
-def played_in_game(events):
- """
- Determines if a given set of player events correspond with a player who
- played in the game (matches 1 and scoreboardvalid 1)
- """
- if 'matches' in events and 'scoreboardvalid' in events:
- return True
- else:
- return False
-
-
def num_real_players(player_events):
"""
Returns the number of real players (those who played