]> de.git.xonotic.org Git - xonotic/xonstat.git/blobdiff - xonstat/views/submission.py
Use the new data structures in the submission handler.
[xonotic/xonstat.git] / xonstat / views / submission.py
index 0cd61be18f98f4d5699640d562f6e6e7bd6f4328..b527167c4adc25ad95bb841e566dd837efdb6cc5 100644 (file)
@@ -338,7 +338,7 @@ def has_minimum_real_players(settings, submission):
     except:
         minimum_required_players = 2
 
-    return len(submission.human_players) >= minimum_required_players
+    return len(submission.humans) >= minimum_required_players
 
 
 def do_precondition_checks(settings, submission):
@@ -581,24 +581,25 @@ def get_or_create_map(session, name):
     return gmap
 
 
-def create_game(session, start_dt, game_type_cd, server_id, map_id,
-        match_id, duration, mod, winner=None):
+def create_game(session, game_type_cd, server_id, map_id, match_id, start_dt, duration, mod,
+                winner=None):
     """
     Creates a game. Parameters:
 
     session - SQLAlchemy database session factory
-    start_dt - when the game started (datetime object)
     game_type_cd - the game type of the game being played
+    mod - mods in use during the game
     server_id - server identifier of the server hosting the game
     map_id - map on which the game was played
-    winner - the team id of the team that won
+    match_id - a unique match ID given by the server
+    start_dt - when the game started (datetime object)
     duration - how long the game lasted
-    mod - mods in use during the game
+    winner - the team id of the team that won
     """
     seq = Sequence('games_game_id_seq')
     game_id = session.execute(seq)
-    game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
-                server_id=server_id, map_id=map_id, winner=winner)
+    game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd, server_id=server_id,
+                map_id=map_id, winner=winner)
     game.match_id = match_id
     game.mod = mod[:64]
 
@@ -607,27 +608,25 @@ def create_game(session, start_dt, game_type_cd, server_id, map_id,
     # resolved.
     game.create_dt = start_dt
 
-    try:
-        game.duration = datetime.timedelta(seconds=int(round(float(duration))))
-    except:
-        pass
+    game.duration = duration
 
     try:
-        session.query(Game).filter(Game.server_id==server_id).\
-                filter(Game.match_id==match_id).one()
+        session.query(Game).filter(Game.server_id == server_id)\
+            .filter(Game.match_id == match_id).one()
 
         log.debug("Error: game with same server and match_id found! Ignoring.")
 
-        # if a game under the same server and match_id found,
-        # this is a duplicate game and can be ignored
-        raise pyramid.httpexceptions.HTTPOk('OK')
-    except NoResultFound, e:
+        # if a game under the same server_id and match_id exists, this is a duplicate
+        msg = "Duplicate game (pre-existing match_id)"
+        log.debug(msg)
+        raise pyramid.httpexceptions.HTTPOk(body=msg, content_type="text/plain")
+
+    except NoResultFound:
         # server_id/match_id combination not found. game is ok to insert
         session.add(game)
         session.flush()
-        log.debug("Created game id {0} on server {1}, map {2} at \
-                {3}".format(game.game_id,
-                    server_id, map_id, start_dt))
+        log.debug("Created game id {} on server {}, map {} at {}"
+                  .format(game.game_id, server_id, map_id, start_dt))
 
     return game
 
@@ -754,11 +753,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
@@ -967,6 +961,87 @@ def get_ranks(session, player_ids, game_type_cd):
     return ranks
 
 
+def update_player(session, player, events):
+    """
+    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):
+    """
+    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):
+    hashkeys = set(events_by_hashkey.keys())
+    players_by_hashkey = {}
+
+    bot = session.query(Player).filter(Player.player_id == 1).one()
+    anon = session.query(Player).filter(Player.player_id == 2).one()
+
+    # fill in the bots and anonymous players
+    for hashkey in events_by_hashkey.keys():
+        if hashkey.startswith("bot#"):
+            players_by_hashkey[hashkey] = bot
+            hashkeys.remove(hashkey)
+        elif hashkey.startswith("player#"):
+            players_by_hashkey[hashkey] = anon
+            hashkeys.remove(hashkey)
+
+    # We are left with the "real" players and can now fetch them by their collective hashkeys.
+    # Those that are returned here are pre-existing players who need to be updated.
+    for p, hk in session.query(Player, 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)
+
+    # 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 submit_stats(request):
     """
     Entry handler for POST stats submissions.
@@ -1004,21 +1079,23 @@ def submit_stats(request):
 
         game = create_game(
             session=session,
-            start_dt=datetime.datetime.utcnow(),
-            server_id=server.server_id,
             game_type_cd=submission.game_type_cd,
+            mod=submission.mod,
+            server_id=server.server_id,
             map_id=gmap.map_id,
             match_id=submission.match_id,
-            duration=submission.duration,
-            mod=submission.mod
+            start_dt=datetime.datetime.utcnow(),
+            duration=submission.duration
         )
 
-        # keep track of the players we've seen
-        player_ids = []
+        events_by_hashkey = {elem["P"]: elem for elem in submission.humans + submission.bots}
+        players_by_hashkey = get_or_create_players(session, events_by_hashkey)
+
         pgstats = []
-        hashkeys = {}
-        for events in submission.humans + submission.bots:
-            player = get_or_create_player(session, events['P'], events.get('n', None))
+        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)
 
@@ -1027,12 +1104,12 @@ def submit_stats(request):
 
             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:
@@ -1057,7 +1134,7 @@ def submit_stats(request):
                 "game": game,
                 "gmap": gmap,
                 "player_ids": player_ids,
-                "hashkeys": hashkeys,
+                "hashkeys": hashkeys_by_player_id,
                 "elos": ep.wip,
                 "ranks": ranks,
         }