X-Git-Url: https://de.git.xonotic.org/?p=xonotic%2Fxonstat.git;a=blobdiff_plain;f=xonstat%2Fviews%2Fsubmission.py;h=b002454e963e418202cdd9a7a7ad12d646994260;hp=1f72df59b2e32ee429bef5c9cd1687c35800d87d;hb=4d088b1f73fe6caccbf41cabadd7f7c63b2bb49c;hpb=9f1516200c67ff01d51b4b929d4b9d7ff1ae3bde diff --git a/xonstat/views/submission.py b/xonstat/views/submission.py old mode 100755 new mode 100644 index 1f72df5..b002454 --- a/xonstat/views/submission.py +++ b/xonstat/views/submission.py @@ -4,37 +4,124 @@ import os import pyramid.httpexceptions import re import time +import sqlalchemy.sql.expression as expr from pyramid.response import Response from sqlalchemy import Sequence from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound from xonstat.d0_blind_id import d0_blind_id_verify +from xonstat.elo import process_elos from xonstat.models import * from xonstat.util import strip_colors, qfont_decode + log = logging.getLogger(__name__) -def is_blank_game(players): +def parse_stats_submission(body): + """ + Parses the POST request body for a stats submission + """ + # storage vars for the request body + game_meta = {} + events = {} + players = [] + teams = [] + + # we're not in either stanza to start + in_P = in_Q = False + + for line in body.split('\n'): + try: + (key, value) = line.strip().split(' ', 1) + + # Server (S) and Nick (n) fields can have international characters. + if key in 'S' 'n': + value = unicode(value, 'utf-8') + + if key not in 'P' 'Q' 'n' 'e' 't' 'i': + game_meta[key] = value + + if key == 'Q' or key == 'P': + #log.debug('Found a {0}'.format(key)) + #log.debug('in_Q: {0}'.format(in_Q)) + #log.debug('in_P: {0}'.format(in_P)) + #log.debug('events: {0}'.format(events)) + + # check where we were before and append events accordingly + if in_Q and len(events) > 0: + #log.debug('creating a team (Q) entry') + teams.append(events) + events = {} + elif in_P and len(events) > 0: + #log.debug('creating a player (P) entry') + players.append(events) + events = {} + + if key == 'P': + #log.debug('key == P') + in_P = True + in_Q = False + elif key == 'Q': + #log.debug('key == Q') + in_P = False + in_Q = True + + events[key] = value + + if key == 'e': + (subkey, subvalue) = value.split(' ', 1) + events[subkey] = subvalue + if key == 'n': + events[key] = value + if key == 't': + events[key] = value + except: + # no key/value pair - move on to the next line + pass + + # add the last entity we were working on + if in_P and len(events) > 0: + players.append(events) + elif in_Q and len(events) > 0: + teams.append(events) + + return (game_meta, players, teams) + + +def is_blank_game(gametype, players): """Determine if this is a blank game or not. A blank game is either: 1) a match that ended in the warmup stage, where accuracy events are not - present + present (for non-CTS games) 2) a match in which no player made a positive or negative score AND was on the scoreboard + + ... or for CTS, which doesn't record accuracy events + + 1) a match in which no player made a fastest lap AND was + on the scoreboard """ + r = re.compile(r'acc-.*-cnt-fired') flg_nonzero_score = False flg_acc_events = False + flg_fastest_lap = False for events in players: - if is_real_player(events): + if is_real_player(events) and played_in_game(events): for (key,value) in events.items(): - if key == 'scoreboard-score' and value != '0': + if key == 'scoreboard-score' and value != 0: flg_nonzero_score = True - if key.startswith('acc-'): + if r.search(key): flg_acc_events = True + if key == 'scoreboard-fastest': + flg_fastest_lap = True + + if gametype == 'cts': + return not flg_fastest_lap + else: + return not (flg_nonzero_score and flg_acc_events) - return flg_nonzero_score and flg_acc_events def get_remote_addr(request): """Get the Xonotic server's IP address""" @@ -44,17 +131,52 @@ def get_remote_addr(request): return request.remote_addr -def is_supported_gametype(gametype): +def is_supported_gametype(gametype, version): """Whether a gametype is supported or not""" - flg_supported = True + is_supported = False + + # if the type can be supported, but with version constraints, uncomment + # here and add the restriction for a specific version below + supported_game_types = ( + 'as', + 'ca', + # 'cq', + 'ctf', + 'cts', + 'dm', + 'dom', + 'ft', 'freezetag', + 'ka', 'keepaway', + 'kh', + # 'lms', + 'nb', 'nexball', + # 'rc', + 'rune', + 'tdm', + ) + + if gametype in supported_game_types: + is_supported = True + else: + is_supported = False - if gametype == 'cts' or gametype == 'ca' or gametype == 'lms': - flg_supported = False + # some game types were buggy before revisions, thus this additional filter + if gametype == 'ca' and version <= 5: + is_supported = False - return flg_supported + return is_supported def verify_request(request): + """Verify requests using the d0_blind_id library""" + + # first determine if we should be verifying or not + val_verify_requests = request.registry.settings.get('xonstat.verify_requests', 'true') + if val_verify_requests == "true": + flg_verify_requests = True + else: + flg_verify_requests = False + try: (idfp, status) = d0_blind_id_verify( sig=request.headers['X-D0-Blind-Id-Detached-Signature'], @@ -62,22 +184,73 @@ def verify_request(request): postdata=request.body) log.debug('\nidfp: {0}\nstatus: {1}'.format(idfp, status)) - except: + except: idfp = None status = None + if flg_verify_requests and not idfp: + log.debug("ERROR: Unverified request") + raise pyramid.httpexceptions.HTTPUnauthorized("Unverified request") + return (idfp, status) +def do_precondition_checks(request, game_meta, raw_players): + """Precondition checks for ALL gametypes. + These do not require a database connection.""" + if not has_required_metadata(game_meta): + log.debug("ERROR: Required game meta missing") + raise pyramid.httpexceptions.HTTPUnprocessableEntity("Missing game meta") + + try: + version = int(game_meta['V']) + except: + log.debug("ERROR: Required game meta invalid") + raise pyramid.httpexceptions.HTTPUnprocessableEntity("Invalid game meta") + + if not is_supported_gametype(game_meta['G'], version): + log.debug("ERROR: Unsupported gametype") + raise pyramid.httpexceptions.HTTPOk("OK") + + if not has_minimum_real_players(request.registry.settings, raw_players): + log.debug("ERROR: Not enough real players") + raise pyramid.httpexceptions.HTTPOk("OK") + + if is_blank_game(game_meta['G'], raw_players): + log.debug("ERROR: Blank game") + raise pyramid.httpexceptions.HTTPOk("OK") + + +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 + Returns the number of real players (those who played and are on the scoreboard). """ real_players = 0 for events in player_events: - if is_real_player(events): + if is_real_player(events) and played_in_game(events): real_players += 1 return real_players @@ -99,7 +272,6 @@ def has_minimum_real_players(settings, player_events): real_players = num_real_players(player_events) - #TODO: put this into a config setting in the ini file? if real_players < minimum_required_players: flg_has_min_real_players = False @@ -123,24 +295,22 @@ def has_required_metadata(metadata): return flg_has_req_metadata -def is_real_player(events): - """ - Determines if a given set of player events correspond with a player who - - 1) is not a bot (P event does not look like a bot) - 2) played in the game (matches 1) - 3) was present at the end of the game (scoreboardvalid 1) +def should_do_weapon_stats(game_type_cd): + """True of the game type should record weapon stats. False otherwise.""" + if game_type_cd in 'cts': + return False + else: + return True - Returns True if the player meets the above conditions, and false otherwise. - """ - flg_is_real = False - if not events['P'].startswith('bot'): - # removing 'joins' here due to bug, but it should be here - if 'matches' in events and 'scoreboardvalid' in events: - flg_is_real = True +def should_do_elos(game_type_cd): + """True of the game type should process Elos. False otherwise.""" + elo_game_types = ('duel', 'dm', 'ca', 'ctf', 'tdm', 'ka', 'ft') - return flg_is_real + if game_type_cd in elo_game_types: + return True + else: + return False def register_new_nick(session, player, new_nick): @@ -153,7 +323,7 @@ def register_new_nick(session, player, new_nick): new_nick - the new nickname """ # see if that nick already exists - stripped_nick = strip_colors(player.nick) + stripped_nick = strip_colors(qfont_decode(player.nick)) try: player_nick = session.query(PlayerNick).filter_by( player_id=player.player_id, stripped_nick=stripped_nick).one() @@ -163,18 +333,47 @@ def register_new_nick(session, player, new_nick): if not re.search('^Anonymous Player #\d+$', player.nick): player_nick = PlayerNick() player_nick.player_id = player.player_id - player_nick.stripped_nick = player.stripped_nick + player_nick.stripped_nick = stripped_nick player_nick.nick = player.nick session.add(player_nick) # We change to the new nick regardless player.nick = new_nick - player.stripped_nick = strip_colors(new_nick) + player.stripped_nick = strip_colors(qfont_decode(new_nick)) session.add(player) -def get_or_create_server(session=None, name=None, hashkey=None, ip_addr=None, - revision=None): +def update_fastest_cap(session, player_id, game_id, map_id, captime): + """ + Check the fastest cap time for the player and map. If there isn't + one, insert one. If there is, check if the passed time is faster. + If so, update! + """ + # we don't record fastest cap times for bots or anonymous players + if player_id <= 2: + return + + # see if a cap entry exists already + # then check to see if the new captime is faster + try: + cur_fastest_cap = session.query(PlayerCaptime).filter_by( + player_id=player_id, map_id=map_id).one() + + # current captime is faster, so update + if captime < cur_fastest_cap.fastest_cap: + cur_fastest_cap.fastest_cap = captime + cur_fastest_cap.game_id = game_id + cur_fastest_cap.create_dt = datetime.datetime.utcnow() + session.add(cur_fastest_cap) + + except NoResultFound, e: + # none exists, so insert + cur_fastest_cap = PlayerCaptime(player_id, game_id, map_id, captime) + session.add(cur_fastest_cap) + session.flush() + + +def get_or_create_server(session, name, hashkey, ip_addr, revision, port): """ Find a server by name or create one if not found. Parameters: @@ -182,41 +381,62 @@ def get_or_create_server(session=None, name=None, hashkey=None, ip_addr=None, name - server name of the server to be found or created hashkey - server hashkey """ - try: - # find one by that name, if it exists - server = session.query(Server).filter_by(name=name).one() + server = None - # store new hashkey - if server.hashkey != hashkey: - server.hashkey = hashkey - session.add(server) - - # store new IP address - if server.ip_addr != ip_addr: - server.ip_addr = ip_addr - session.add(server) - - # store new revision - if server.revision != revision: - server.revision = revision - session.add(server) - - log.debug("Found existing server {0}".format(server.server_id)) + try: + port = int(port) + except: + port = None + + # finding by hashkey is preferred, but if not we will fall + # back to using name only, which can result in dupes + if hashkey is not None: + servers = session.query(Server).\ + filter_by(hashkey=hashkey).\ + order_by(expr.desc(Server.create_dt)).limit(1).all() + + if len(servers) > 0: + server = servers[0] + log.debug("Found existing server {0} by hashkey ({1})".format( + server.server_id, server.hashkey)) + else: + servers = session.query(Server).\ + filter_by(name=name).\ + order_by(expr.desc(Server.create_dt)).limit(1).all() - except MultipleResultsFound, e: - # multiple found, so also filter by hashkey - server = session.query(Server).filter_by(name=name).\ - filter_by(hashkey=hashkey).one() - log.debug("Found existing server {0}".format(server.server_id)) + if len(servers) > 0: + server = servers[0] + log.debug("Found existing server {0} by name".format(server.server_id)) - except NoResultFound, e: - # not found, create one + # still haven't found a server by hashkey or name, so we need to create one + if server is None: server = Server(name=name, hashkey=hashkey) session.add(server) session.flush() log.debug("Created server {0} with hashkey {1}".format( server.server_id, server.hashkey)) + # detect changed fields + if server.name != name: + server.name = name + session.add(server) + + if server.hashkey != hashkey: + server.hashkey = hashkey + session.add(server) + + if server.ip_addr != ip_addr: + server.ip_addr = ip_addr + session.add(server) + + if server.port != port: + server.port = port + session.add(server) + + if server.revision != revision: + server.revision = revision + session.add(server) + return server @@ -230,7 +450,7 @@ def get_or_create_map(session=None, name=None): try: # find one by the name, if it exists gmap = session.query(Map).filter_by(name=name).one() - log.debug("Found map id {0}: {1}".format(gmap.map_id, + log.debug("Found map id {0}: {1}".format(gmap.map_id, gmap.name)) except NoResultFound, e: gmap = Map(name=name) @@ -250,8 +470,8 @@ def get_or_create_map(session=None, name=None): return gmap -def create_game(session=None, start_dt=None, game_type_cd=None, - server_id=None, map_id=None, winner=None, match_id=None): +def create_game(session, start_dt, game_type_cd, server_id, map_id, + match_id, duration, mod, winner=None): """ Creates a game. Parameters: @@ -261,24 +481,36 @@ def create_game(session=None, start_dt=None, game_type_cd=None, 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 + duration - how long the game lasted + mod - mods in use during the game """ 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.match_id = match_id + game.mod = mod[:64] + + try: + game.duration = datetime.timedelta(seconds=int(round(float(duration)))) + except: + pass try: session.query(Game).filter(Game.server_id==server_id).\ filter(Game.match_id==match_id).one() - # if a game under the same server and match_id found, + + 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 + raise pyramid.httpexceptions.HTTPOk('OK') except NoResultFound, e: # 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, + {3}".format(game.game_id, server_id, map_id, start_dt)) return game @@ -294,7 +526,7 @@ def get_or_create_player(session=None, hashkey=None, nick=None): nick - nick of the player (in case of a first time create) """ # if we have a bot - if re.search('^bot#\d+$', hashkey): + if re.search('^bot#\d+$', hashkey) or re.search('^bot#\d+#', hashkey): player = session.query(Player).filter_by(player_id=1).one() # if we have an untracked player elif re.search('^player#\d+$', hashkey): @@ -319,7 +551,7 @@ def get_or_create_player(session=None, hashkey=None, nick=None): # with a suffix added for uniqueness. if nick: player.nick = nick[:128] - player.stripped_nick = strip_colors(nick[:128]) + player.stripped_nick = strip_colors(qfont_decode(nick[:128])) else: player.nick = "Anonymous Player #{0}".format(player.player_id) player.stripped_nick = player.nick @@ -331,79 +563,131 @@ def get_or_create_player(session=None, hashkey=None, nick=None): return player -def create_player_game_stat(session=None, player=None, - game=None, player_events=None): - """ - Creates game statistics for a given player in a given game. Parameters: - session - SQLAlchemy session factory - player - Player record of the player who owns the stats - game - Game record for the game to which the stats pertain - player_events - dictionary for the actual stats that need to be transformed - """ +def create_default_game_stat(session, game_type_cd): + """Creates a blanked-out pgstat record for the given game type""" - # in here setup default values (e.g. if game type is CTF then - # set kills=0, score=0, captures=0, pickups=0, fckills=0, etc - # TODO: use game's create date here instead of now() + # this is what we have to do to get partitioned records in - grab the + # sequence value first, then insert using the explicit ID (vs autogenerate) seq = Sequence('player_game_stats_player_game_stat_id_seq') pgstat_id = session.execute(seq) - pgstat = PlayerGameStat(player_game_stat_id=pgstat_id, + pgstat = PlayerGameStat(player_game_stat_id=pgstat_id, create_dt=datetime.datetime.utcnow()) - # set player id from player record - pgstat.player_id = player.player_id + if game_type_cd == 'as': + pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0 - #set game id from game record - pgstat.game_id = game.game_id + if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm': + pgstat.kills = pgstat.deaths = pgstat.suicides = 0 - # all games have a score - pgstat.score = 0 + if game_type_cd == 'cq': + pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0 + pgstat.drops = 0 - if game.game_type_cd == 'dm' or game.game_type_cd == 'tdm' or game.game_type_cd == 'duel': - pgstat.kills = 0 + if game_type_cd == 'ctf': + pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0 + pgstat.returns = pgstat.carrier_frags = 0 + + if game_type_cd == 'cts': pgstat.deaths = 0 - pgstat.suicides = 0 - elif game.game_type_cd == 'ctf': - pgstat.kills = 0 - pgstat.captures = 0 - pgstat.pickups = 0 + + if game_type_cd == 'dom': + pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0 pgstat.drops = 0 - pgstat.returns = 0 + + if game_type_cd == 'ft': + pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0 + + if game_type_cd == 'ka': + pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0 + pgstat.carrier_frags = 0 + pgstat.time = datetime.timedelta(seconds=0) + + if game_type_cd == 'kh': + pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0 + pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0 pgstat.carrier_frags = 0 - for (key,value) in player_events.items(): - if key == 'n': pgstat.nick = value[:128] - if key == 't': pgstat.team = value - if key == 'rank': pgstat.rank = value - if key == 'alivetime': - pgstat.alivetime = datetime.timedelta(seconds=int(round(float(value)))) - if key == 'scoreboard-drops': pgstat.drops = value - if key == 'scoreboard-returns': pgstat.returns = value - if key == 'scoreboard-fckills': pgstat.carrier_frags = value - if key == 'scoreboard-pickups': pgstat.pickups = value - if key == 'scoreboard-caps': pgstat.captures = value - if key == 'scoreboard-score': pgstat.score = value - if key == 'scoreboard-deaths': pgstat.deaths = value - if key == 'scoreboard-kills': pgstat.kills = value - if key == 'scoreboard-suicides': pgstat.suicides = value - - # check to see if we had a name, and if - # not use the name from the player id - if pgstat.nick == None: - pgstat.nick = player.nick - - # whichever nick we ended up with, strip it and store as the stripped_nick - pgstat.stripped_nick = qfont_decode(strip_colors(pgstat.nick)) - - # if the nick we end up with is different from the one in the - # player record, change the nick to reflect the new value - if pgstat.nick != player.nick and player.player_id > 2: + 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 + + +def create_game_stat(session, game_meta, game, server, gmap, player, events): + """Game stats handler for all game types""" + + game_type_cd = game.game_type_cd + + pgstat = create_default_game_stat(session, game_type_cd) + + # these fields should be on every pgstat record + pgstat.game_id = game.game_id + pgstat.player_id = player.player_id + pgstat.nick = events.get('n', 'Anonymous Player')[:128] + pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick)) + pgstat.score = int(round(float(events.get('scoreboard-score', 0)))) + pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0))))) + 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) - # if the player is ranked #1 and it is a team game, set the game's winner - # to be the team of that player - # FIXME: this is a hack, should be using the 'W' field (not present) - if pgstat.rank == '1' and pgstat.team: + wins = False + + # gametype-specific stuff is handled here. if passed to us, we store it + for (key,value) in events.items(): + if key == 'wins': wins = True + if key == 't': pgstat.team = int(value) + + if key == 'scoreboard-drops': pgstat.drops = int(value) + if key == 'scoreboard-returns': pgstat.returns = int(value) + if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value) + if key == 'scoreboard-pickups': pgstat.pickups = int(value) + if key == 'scoreboard-caps': pgstat.captures = int(value) + if key == 'scoreboard-score': pgstat.score = int(round(float(value))) + if key == 'scoreboard-deaths': pgstat.deaths = int(value) + if key == 'scoreboard-kills': pgstat.kills = int(value) + if key == 'scoreboard-suicides': pgstat.suicides = int(value) + if key == 'scoreboard-objectives': pgstat.collects = int(value) + if key == 'scoreboard-captured': pgstat.captures = int(value) + if key == 'scoreboard-released': pgstat.drops = int(value) + if key == 'scoreboard-fastest': + pgstat.fastest = datetime.timedelta(seconds=float(value)/100) + if key == 'scoreboard-takes': pgstat.pickups = int(value) + if key == 'scoreboard-ticks': pgstat.drops = int(value) + if key == 'scoreboard-revivals': pgstat.revivals = int(value) + if key == 'scoreboard-bctime': + pgstat.time = datetime.timedelta(seconds=int(value)) + if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value) + if key == 'scoreboard-losses': pgstat.drops = int(value) + if key == 'scoreboard-pushes': pgstat.pushes = int(value) + if key == 'scoreboard-destroyed': pgstat.destroys = int(value) + if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value) + if key == 'scoreboard-lives': pgstat.lives = int(value) + if key == 'scoreboard-goals': pgstat.captures = int(value) + if key == 'scoreboard-faults': pgstat.drops = int(value) + if key == 'scoreboard-laps': pgstat.laps = int(value) + + if key == 'avglatency': pgstat.avg_latency = float(value) + if key == 'scoreboard-captime': + pgstat.fastest = datetime.timedelta(seconds=float(value)/100) + if game.game_type_cd == 'ctf': + update_fastest_cap(session, player.player_id, game.game_id, + gmap.map_id, pgstat.fastest) + + # there is no "winning team" field, so we have to derive it + if wins and pgstat.team is not None and game.winner is None: game.winner = pgstat.team session.add(game) @@ -412,22 +696,24 @@ def create_player_game_stat(session=None, player=None, return pgstat -def create_player_weapon_stats(session=None, player=None, - game=None, pgstat=None, player_events=None): - """ - Creates accuracy records for each weapon used by a given player in a - given game. Parameters: - - session - SQLAlchemy session factory object - player - Player record who owns the weapon stats - game - Game record in which the stats were created - pgstat - Corresponding PlayerGameStat record for these weapon stats - player_events - dictionary containing the raw weapon values that need to be - transformed - """ +def create_weapon_stats(session, game_meta, game, player, pgstat, events): + """Weapon stats handler for all game types""" pwstats = [] - for (key,value) in player_events.items(): + # Version 1 of stats submissions doubled the data sent. + # To counteract this we divide the data by 2 only for + # POSTs coming from version 1. + try: + version = int(game_meta['V']) + if version == 1: + is_doubled = True + log.debug('NOTICE: found a version 1 request, halving the weapon stats...') + else: + is_doubled = False + except: + is_doubled = False + + for (key,value) in events.items(): matched = re.search("acc-(.*?)-cnt-fired", key) if matched: weapon_cd = matched.group(1) @@ -440,26 +726,33 @@ def create_player_weapon_stats(session=None, player=None, pwstat.player_game_stat_id = pgstat.player_game_stat_id pwstat.weapon_cd = weapon_cd - if 'n' in player_events: - pwstat.nick = player_events['n'] + if 'n' in events: + pwstat.nick = events['n'] else: - pwstat.nick = player_events['P'] + pwstat.nick = events['P'] - if 'acc-' + weapon_cd + '-cnt-fired' in player_events: + if 'acc-' + weapon_cd + '-cnt-fired' in events: pwstat.fired = int(round(float( - player_events['acc-' + weapon_cd + '-cnt-fired']))) - if 'acc-' + weapon_cd + '-fired' in player_events: + events['acc-' + weapon_cd + '-cnt-fired']))) + if 'acc-' + weapon_cd + '-fired' in events: pwstat.max = int(round(float( - player_events['acc-' + weapon_cd + '-fired']))) - if 'acc-' + weapon_cd + '-cnt-hit' in player_events: + events['acc-' + weapon_cd + '-fired']))) + if 'acc-' + weapon_cd + '-cnt-hit' in events: pwstat.hit = int(round(float( - player_events['acc-' + weapon_cd + '-cnt-hit']))) - if 'acc-' + weapon_cd + '-hit' in player_events: + events['acc-' + weapon_cd + '-cnt-hit']))) + if 'acc-' + weapon_cd + '-hit' in events: pwstat.actual = int(round(float( - player_events['acc-' + weapon_cd + '-hit']))) - if 'acc-' + weapon_cd + '-frags' in player_events: + events['acc-' + weapon_cd + '-hit']))) + if 'acc-' + weapon_cd + '-frags' in events: pwstat.frags = int(round(float( - player_events['acc-' + weapon_cd + '-frags']))) + events['acc-' + weapon_cd + '-frags']))) + + if is_doubled: + pwstat.fired = pwstat.fired/2 + pwstat.max = pwstat.max/2 + pwstat.hit = pwstat.hit/2 + pwstat.actual = pwstat.actual/2 + pwstat.frags = pwstat.frags/2 session.add(pwstat) pwstats.append(pwstat) @@ -467,144 +760,92 @@ def create_player_weapon_stats(session=None, player=None, return pwstats -def parse_body(request): - """ - Parses the POST request body for a stats submission - """ - # storage vars for the request body - game_meta = {} - player_events = {} - current_team = None - players = [] - - for line in request.body.split('\n'): - try: - (key, value) = line.strip().split(' ', 1) - - # Server (S) and Nick (n) fields can have international characters. - # We convert to UTF-8. - if key in 'S' 'n': - value = unicode(value, 'utf-8') - - if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W' 'I': - game_meta[key] = value - - if key == 'P': - # if we were working on a player record already, append - # it and work on a new one (only set team info) - if len(player_events) != 0: - players.append(player_events) - player_events = {} - - player_events[key] = value - - if key == 'e': - (subkey, subvalue) = value.split(' ', 1) - player_events[subkey] = subvalue - if key == 'n': - player_events[key] = value - if key == 't': - player_events[key] = value - except: - # no key/value pair - move on to the next line - pass - - # add the last player we were working on - if len(player_events) > 0: - players.append(player_events) - - return (game_meta, players) - - -def create_player_stats(session=None, player=None, game=None, - player_events=None): - """ - Creates player game and weapon stats according to what type of player - """ - pgstat = create_player_game_stat(session=session, - player=player, game=game, player_events=player_events) - - #TODO: put this into a config setting in the ini file? - if not re.search('^bot#\d+$', player_events['P']): - create_player_weapon_stats(session=session, - player=player, game=game, pgstat=pgstat, - player_events=player_events) +def create_elos(session, game): + """Elo handler for all game types.""" + try: + process_elos(game, session) + except Exception as e: + log.debug('Error (non-fatal): elo processing failed.') -def stats_submit(request): +def submit_stats(request): """ Entry handler for POST stats submissions. """ try: - session = DBSession() + # placeholder for the actual session + session = None log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body + "----- END REQUEST BODY -----\n\n") (idfp, status) = verify_request(request) - if not idfp: - log.debug("ERROR: Unverified request") - raise pyramid.httpexceptions.HTTPUnauthorized("Unverified request") + (game_meta, raw_players, raw_teams) = parse_stats_submission(request.body) + revision = game_meta.get('R', 'unknown') + duration = game_meta.get('D', None) - (game_meta, players) = parse_body(request) + # only players present at the end of the match are eligible for stats + raw_players = filter(played_in_game, raw_players) - if not has_required_metadata(game_meta): - log.debug("ERROR: Required game meta missing") - raise pyramid.exceptions.HTTPUnprocessableEntity("Missing game meta") + do_precondition_checks(request, game_meta, raw_players) - if not is_supported_gametype(game_meta['G']): - log.debug("ERROR: Unsupported gametype") - raise pyramid.httpexceptions.HTTPOk("OK") - - if not has_minimum_real_players(request.registry.settings, players): - log.debug("ERROR: Not enough real players") - raise pyramid.httpexceptions.HTTPOk("OK") - - if is_blank_game(players): - log.debug("ERROR: Blank game") - raise pyramid.httpexceptions.HTTPOk("OK") - - # FIXME: if we have two players and game type is 'dm', - # change this into a 'duel' gametype. This should be - # removed when the stats actually send 'duel' instead of 'dm' - if num_real_players(players) == 2 and game_meta['G'] == 'dm': + # the "duel" gametype is fake + if len(raw_players) == 2 \ + and num_real_players(raw_players) == 2 \ + and game_meta['G'] == 'dm': game_meta['G'] = 'duel' - server = get_or_create_server(session=session, hashkey=idfp, - name=game_meta['S'], revision=game_meta['R'], - ip_addr=get_remote_addr(request)) - - gmap = get_or_create_map(session=session, name=game_meta['M']) - - # FIXME: use the gmtime instead of utcnow() when the timezone bug is - # fixed - game = create_game(session=session, - start_dt=datetime.datetime.utcnow(), - #start_dt=datetime.datetime( - #*time.gmtime(float(game_meta['T']))[:6]), - server_id=server.server_id, game_type_cd=game_meta['G'], - map_id=gmap.map_id, match_id=game_meta['I']) - - # find or create a record for each player - # and add stats for each if they were present at the end - # of the game - for player_events in players: - if 'n' in player_events: - nick = player_events['n'] - else: - nick = None + #---------------------------------------------------------------------- + # Actual setup (inserts/updates) below here + #---------------------------------------------------------------------- + session = DBSession() - if 'matches' in player_events and 'scoreboardvalid' \ - in player_events: - player = get_or_create_player(session=session, - hashkey=player_events['P'], nick=nick) - log.debug('Creating stats for %s' % player_events['P']) - create_player_stats(session=session, player=player, game=game, - player_events=player_events) + game_type_cd = game_meta['G'] + + # All game types create Game, Server, Map, and Player records + # the same way. + server = get_or_create_server( + session = session, + hashkey = idfp, + name = game_meta['S'], + revision = revision, + ip_addr = get_remote_addr(request), + port = game_meta.get('U', None)) + + gmap = get_or_create_map( + session = session, + name = game_meta['M']) + + game = create_game( + session = session, + start_dt = datetime.datetime.utcnow(), + server_id = server.server_id, + game_type_cd = game_type_cd, + map_id = gmap.map_id, + match_id = game_meta['I'], + duration = duration, + mod = game_meta.get('O', None)) + + for events in raw_players: + player = get_or_create_player( + session = session, + hashkey = events['P'], + nick = events.get('n', None)) + + pgstat = create_game_stat(session, game_meta, game, server, + gmap, player, events) + + if should_do_weapon_stats(game_type_cd) and player.player_id > 1: + pwstats = create_weapon_stats(session, game_meta, game, player, + pgstat, events) + + if should_do_elos(game_type_cd): + create_elos(session, game) session.commit() log.debug('Success! Stats recorded.') return Response('200 OK') except Exception as e: - session.rollback() + if session: + session.rollback() return e