]> de.git.xonotic.org Git - xonotic/xonstat.git/blobdiff - xonstat/views/submission.py
Remove more refs to sqlahelper.
[xonotic/xonstat.git] / xonstat / views / submission.py
index 9c2cd9ca9dd18a55870cd980a90426312195ed3c..567d8a4777ba67eb3e6b3c48be0d32a8230e4b37 100644 (file)
@@ -6,10 +6,10 @@ import re
 
 import pyramid.httpexceptions
 from sqlalchemy import Sequence
-from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
+from sqlalchemy.orm.exc import NoResultFound
 from xonstat.elo import EloProcessor
 from xonstat.models import DBSession, Server, Map, Game, PlayerGameStat, PlayerWeaponStat
-from xonstat.models import PlayerRank, PlayerCaptime
+from xonstat.models import PlayerRank, PlayerCaptime, PlayerGameFragMatrix
 from xonstat.models import TeamGameStat, PlayerGameAnticheat, Player, Hashkey, PlayerNick
 from xonstat.util import strip_colors, qfont_decode, verify_request, weapon_map
 
@@ -79,6 +79,9 @@ class Submission(object):
         # bots who played in the match
         self.bots = []
 
+        # player indexes for those who played
+        self.player_indexes = set()
+
         # distinct weapons that we have seen fired
         self.weapons = set()
 
@@ -91,6 +94,8 @@ class Submission(object):
         # does any human have a fastest cap?
         self.human_fastest = False
 
+        self.parse()
+
     def next_item(self):
         """Returns the next key:value pair off the queue."""
         try:
@@ -127,7 +132,7 @@ class Submission(object):
         """Construct a player events listing from the submission."""
 
         # all of the keys related to player records
-        player_keys = ['i', 'n', 't', 'e']
+        player_keys = ['i', 'n', 't', 'r', 'e']
 
         player = {key: pid}
 
@@ -147,7 +152,7 @@ class Submission(object):
                 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:
+                elif sub_key == 'scoreboard-score' and int(round(float(sub_value))) != 0:
                     player_nonzero_score = True
                 elif sub_key == 'scoreboard-fastest':
                     player_fastest = True
@@ -163,6 +168,9 @@ class Submission(object):
         played = self.played_in_game(player)
         human = self.is_human_player(player)
 
+        if played:
+            self.player_indexes.add(int(player["i"]))
+
         if played and human:
             self.humans.append(player)
 
@@ -338,7 +346,11 @@ def has_minimum_real_players(settings, submission):
     except:
         minimum_required_players = 2
 
-    return len(submission.human_players) >= minimum_required_players
+    # Make an exception for CTS since it can be done by individuals and there is no Elo anyway
+    if submission.game_type_cd == "cts":
+        minimum_required_players = 1
+
+    return len(submission.humans) >= minimum_required_players
 
 
 def do_precondition_checks(settings, submission):
@@ -397,6 +409,13 @@ def should_do_weapon_stats(game_type_cd):
     return game_type_cd not in {'cts'}
 
 
+def should_do_frag_matrix(game_type_cd):
+    """True if the game type should record frag matrix values. False otherwise."""
+    return game_type_cd in {
+        'as', 'ca', 'ctf', 'dm', 'dom', 'ft', 'freezetag', 'ka', 'kh', 'rune', 'tdm',
+    }
+
+
 def gametype_elo_eligible(game_type_cd):
     """True of the game type should process Elos. False otherwise."""
     return game_type_cd in {'duel', 'dm', 'ca', 'ctf', 'tdm', 'ka', 'ft'}
@@ -604,7 +623,7 @@ def create_game(session, game_type_cd, server_id, map_id, match_id, start_dt, du
     game.mod = mod[:64]
 
     # There is some drift between start_dt (provided by app) and create_dt
-    # (default in the database), so we'll make them the same until this is 
+    # (default in the database), so we'll make them the same until this is
     # resolved.
     game.create_dt = start_dt
 
@@ -695,10 +714,6 @@ def create_default_game_stat(session, game_type_cd):
     if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
         pgstat.kills = pgstat.deaths = pgstat.suicides = 0
 
-    if game_type_cd == 'cq':
-        pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
-        pgstat.drops = 0
-
     if game_type_cd == 'ctf':
         pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
         pgstat.returns = pgstat.carrier_frags = 0
@@ -723,16 +738,10 @@ def create_default_game_stat(session, game_type_cd):
         pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
         pgstat.carrier_frags = 0
 
-    if game_type_cd == 'lms':
-        pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
-
     if game_type_cd == 'nb':
         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
         pgstat.drops = 0
 
-    if game_type_cd == 'rc':
-        pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
-
     return pgstat
 
 
@@ -753,11 +762,6 @@ def create_game_stat(session, game, gmap, player, events):
     pgstat.rank          = int(events.get('rank', None))
     pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
 
-    if pgstat.nick != player.nick \
-            and player.player_id > 2 \
-            and pgstat.nick != 'Anonymous Player':
-        register_new_nick(session, player, pgstat.nick)
-
     wins = False
 
     # gametype-specific stuff is handled here. if passed to us, we store it
@@ -847,7 +851,7 @@ def create_default_team_stat(session, game_type_cd):
     # all team game modes have a score, so we'll zero that out always
     teamstat.score = 0
 
-    if game_type_cd in 'ca' 'ft' 'lms' 'ka':
+    if game_type_cd in 'ca' 'ft' 'ka':
         teamstat.rounds = 0
 
     if game_type_cd == 'ctf':
@@ -967,11 +971,43 @@ def get_ranks(session, player_ids, game_type_cd):
 
 
 def update_player(session, player, events):
-    pass
+    """
+    Updates a player record using the latest information.
+    :param session: SQLAlchemy session
+    :param player: Player model representing what is in the database right now (before updates)
+    :param events: Dict of player events from the submission
+    :return: player
+    """
+    nick = events.get('n', 'Anonymous Player')[:128]
+    if nick != player.nick and not nick.startswith("Anonymous Player"):
+        register_new_nick(session, player, nick)
+
+    return player
 
 
 def create_player(session, events):
-    pass
+    """
+    Creates a new player from the list of events.
+    :param session: SQLAlchemy session
+    :param events: Dict of player events from the submission
+    :return: Player
+    """
+    player = Player()
+    session.add(player)
+    session.flush()
+
+    nick = events.get('n', None)
+    if nick:
+        player.nick = nick[:128]
+        player.stripped_nick = strip_colors(qfont_decode(player.nick))
+    else:
+        player.nick = "Anonymous Player #{0}".format(player.player_id)
+        player.stripped_nick = player.nick
+
+    hk = Hashkey(player_id=player.player_id, hashkey=events.get('P', None))
+    session.add(hk)
+
+    return player
 
 
 def get_or_create_players(session, events_by_hashkey):
@@ -996,6 +1032,9 @@ def get_or_create_players(session, events_by_hashkey):
             .filter(Player.player_id == Hashkey.player_id)\
             .filter(Hashkey.hashkey.in_(hashkeys))\
             .all():
+                log.debug("Found existing player {} with hashkey {}"
+                          .format(p.player_id, hk.hashkey))
+
                 player = update_player(session, p, events_by_hashkey[hk.hashkey])
                 players_by_hashkey[hk.hashkey] = player
                 hashkeys.remove(hk.hashkey)
@@ -1003,30 +1042,70 @@ def get_or_create_players(session, events_by_hashkey):
     # The remainder are the players we haven't seen before, so we need to create them.
     for hashkey in hashkeys:
         player = create_player(session, events_by_hashkey[hashkey])
+
+        log.debug("Created player {0} ({2}) with hashkey {1}"
+                  .format(player.player_id, hashkey, player.nick.encode('utf-8')))
+
         players_by_hashkey[hashkey] = player
 
     return players_by_hashkey
 
 
+def create_frag_matrix(session, player_indexes, pgstat, events):
+    """
+    Construct a PlayerFragMatrix object from the events of a given player.
+
+    :param session: The DBSession we're adding objects to.
+    :param player_indexes: The set of player indexes of those that actually played in the game.
+    :param pgstat: The PlayerGameStat object of the player whose frag matrix we want to create.
+    :param events: The raw player events of the above player.
+    :return: PlayerFragMatrix
+    """
+    player_index = int(events.get("i", None))
+
+    # "kills-4" -> 4
+    victim_index = lambda x: int(x.split("-")[1])
+
+    matrix = {victim_index(k): int(v) for (k, v) in events.items()
+              if k.startswith("kills-") and victim_index(k) in player_indexes}
+
+    if len(matrix) > 0:
+        pfm = PlayerGameFragMatrix(pgstat.game_id, pgstat.player_game_stat_id, pgstat.player_id,
+                                   player_index, matrix)
+
+        session.add(pfm)
+        return pfm
+    else:
+        return None
+
+
 def submit_stats(request):
     """
     Entry handler for POST stats submissions.
     """
-    try:
-        # placeholder for the actual session
-        session = None
+    # placeholder for the actual session
+    session = None
 
+    try:
         log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
-                "----- END REQUEST BODY -----\n\n")
+                  "----- END REQUEST BODY -----\n\n")
 
         (idfp, status) = verify_request(request)
-        submission = Submission(request.body, request.headers)
+        try:
+            submission = Submission(request.body, request.headers)
+        except:
+            msg = "Invalid submission"
+            log.debug(msg)
+            raise pyramid.httpexceptions.HTTPUnprocessableEntity(
+                body=msg,
+                content_type="text/plain"
+            )
 
         do_precondition_checks(request.registry.settings, submission)
 
-        #----------------------------------------------------------------------
+        #######################################################################
         # Actual setup (inserts/updates) below here
-        #----------------------------------------------------------------------
+        #######################################################################
         session = DBSession()
 
         # All game types create Game, Server, Map, and Player records
@@ -1055,36 +1134,50 @@ def submit_stats(request):
         )
 
         events_by_hashkey = {elem["P"]: elem for elem in submission.humans + submission.bots}
-        get_or_create_players(session, game, gmap, events_by_hashkey)
+        players_by_hashkey = get_or_create_players(session, events_by_hashkey)
 
-        # keep track of the players we've seen
-        player_ids = []
         pgstats = []
-        hashkeys = {}
-        for events in submission.humans + submission.bots:
-            player = get_or_create_player(session, events['P'], events.get('n', None))
+        elo_pgstats = []
+        player_ids = []
+        hashkeys_by_player_id = {}
+        for hashkey, player in players_by_hashkey.items():
+            events = events_by_hashkey[hashkey]
+
             pgstat = create_game_stat(session, game, gmap, player, events)
             pgstats.append(pgstat)
 
+            if should_do_frag_matrix(submission.game_type_cd):
+                create_frag_matrix(session, submission.player_indexes, pgstat, events)
+
+            # player ranking opt-out
+            if 'r' in events and events['r'] == '0':
+                log.debug("Excluding player {} from ranking calculations (opt-out)"
+                          .format(pgstat.player_id))
+            else:
+                elo_pgstats.append(pgstat)
+
             if player.player_id > 1:
                 create_anticheats(session, pgstat, game, player, events)
 
             if player.player_id > 2:
                 player_ids.append(player.player_id)
-                hashkeys[player.player_id] = events['P']
+                hashkeys_by_player_id[player.player_id] = hashkey
 
             if should_do_weapon_stats(submission.game_type_cd) and player.player_id > 1:
                 create_weapon_stats(session, submission.version, game, player, pgstat, events)
 
-        # store them on games for easy access
+        # player_ids for human players get stored directly on games for fast indexing
         game.players = player_ids
 
         for events in submission.teams:
             create_team_stat(session, game, events)
 
         if server.elo_ind and gametype_elo_eligible(submission.game_type_cd):
-            ep = EloProcessor(session, game, pgstats)
+            ep = EloProcessor(session, game, elo_pgstats)
             ep.save(session)
+            elos = ep.wip
+        else:
+            elos = {}
 
         session.commit()
         log.debug('Success! Stats recorded.')
@@ -1101,8 +1194,8 @@ def submit_stats(request):
                 "game": game,
                 "gmap": gmap,
                 "player_ids": player_ids,
-                "hashkeys": hashkeys,
-                "elos": ep.wip,
+                "hashkeys": hashkeys_by_player_id,
+                "elos": elos,
                 "ranks": ranks,
         }