import datetime import logging import re import time from pyramid.response import Response from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound from sqlalchemy import desc from webhelpers.paginate import Page, PageURL from xonstat.models import * from xonstat.util import page_url log = logging.getLogger(__name__) ########################################################################## def main_index(request): """ This is the main entry point to the entire site. """ log.debug("testing logging; entered MainHandler.index()") return {'project':'xonstat'} ########################################################################## def player_index(request): """ Provides a list of all the current players. """ players = DBSession.query(Player) log.debug("testing logging; entered PlayerHandler.index()") return {'players':players} def player_info(request): """ Provides detailed information on a specific player """ player_id = request.matchdict['id'] try: player = DBSession.query(Player).filter_by(player_id=player_id).one() weapon_stats = DBSession.query("descr", "actual_total", "max_total", "hit_total", "fired_total", "frags_total").\ from_statement( "select cw.descr, sum(actual) actual_total, " "sum(max) max_total, sum(hit) hit_total, " "sum(fired) fired_total, sum(frags) frags_total " "from xonstat.player_weapon_stats ws, xonstat.cd_weapon cw " "where ws.weapon_cd = cw.weapon_cd " "and player_id = :player_id " "group by descr " "order by descr" ).params(player_id=player_id).all() log.debug(weapon_stats) recent_games = DBSession.query(PlayerGameStat, Game, Server, Map).\ filter(PlayerGameStat.player_id == player_id).\ filter(PlayerGameStat.game_id == Game.game_id).\ filter(Game.server_id == Server.server_id).\ filter(Game.map_id == Map.map_id).\ order_by(Game.game_id.desc())[0:10] except Exception as e: player = None weapon_stats = None recent_games = None return {'player':player, 'recent_games':recent_games, 'weapon_stats':weapon_stats} def player_game_index(request): """ Provides an index of the games in which a particular player was involved. This is ordered by game_id, with the most recent game_ids first. Paginated. """ player_id = request.matchdict['player_id'] if 'page' in request.matchdict: current_page = request.matchdict['page'] else: current_page = 1 try: player = DBSession.query(Player).filter_by(player_id=player_id).one() games_q = DBSession.query(PlayerGameStat, Game, Server, Map).\ filter(PlayerGameStat.player_id == player_id).\ filter(PlayerGameStat.game_id == Game.game_id).\ filter(Game.server_id == Server.server_id).\ filter(Game.map_id == Map.map_id).\ order_by(Game.game_id.desc()) games = Page(games_q, current_page, url=page_url) except Exception as e: player = None games = None raise e return {'player':player, 'games':games} def player_weapon_stats(request): """ List the accuracy statistics for the given player_id in a particular game. """ game_id = request.matchdict['game_id'] pgstat_id = request.matchdict['pgstat_id'] try: pwstats = DBSession.query(PlayerWeaponStat, Weapon).\ filter(PlayerWeaponStat.weapon_cd==Weapon.weapon_cd).\ filter_by(game_id=game_id).\ filter_by(player_game_stat_id=pgstat_id).\ order_by(Weapon.descr).\ all() pgstat = DBSession.query(PlayerGameStat).\ filter_by(player_game_stat_id=pgstat_id).one() game = DBSession.query(Game).filter_by(game_id=game_id).one() log.debug(pwstats) log.debug(pgstat) log.debug(game) except Exception as e: pwstats = None pgstat = None game = None raise e return {'pwstats':pwstats, 'pgstat':pgstat, 'game':game} ########################################################################## def game_index(request): """ Provides a list of current games, with the associated game stats. These games are ordered by game_id, with the most current ones first. Paginated. """ if 'page' in request.matchdict: current_page = request.matchdict['page'] else: current_page = 1 games_q = DBSession.query(Game, Server, Map).\ filter(Game.server_id == Server.server_id).\ filter(Game.map_id == Map.map_id).\ order_by(Game.game_id.desc()) games = Page(games_q, current_page, url=page_url) pgstats = {} for (game, server, map) in games: pgstats[game.game_id] = DBSession.query(PlayerGameStat).\ filter(PlayerGameStat.game_id == game.game_id).\ order_by(PlayerGameStat.rank).\ order_by(PlayerGameStat.score).all() return {'games':games, 'pgstats':pgstats} def game_info(request): """ List the game stats (scoreboard) for a particular game. Paginated. """ game_id = request.matchdict['id'] try: notfound = False (start_dt, game_type_cd, server_id, server_name, map_id, map_name) = \ DBSession.query("start_dt", "game_type_cd", "server_id", "server_name", "map_id", "map_name").\ from_statement("select g.start_dt, g.game_type_cd, " "g.server_id, s.name as server_name, g.map_id, " "m.name as map_name " "from games g, servers s, maps m " "where g.game_id = :game_id " "and g.server_id = s.server_id " "and g.map_id = m.map_id").\ params(game_id=game_id).one() player_game_stats = DBSession.query(PlayerGameStat).\ from_statement("select * from player_game_stats " "where game_id = :game_id " "order by score desc").\ params(game_id=game_id).all() except Exception as inst: notfound = True start_dt = None game_type_cd = None server_id = None server_name = None map_id = None map_name = None player_game_stats = None return {'notfound':notfound, 'start_dt':start_dt, 'game_type_cd':game_type_cd, 'server_id':server_id, 'server_name':server_name, 'map_id':map_id, 'map_name':map_name, 'player_game_stats':player_game_stats} ########################################################################## def server_info(request): """ List the stored information about a given server. """ server_id = request.matchdict['id'] try: server = DBSession.query(Server).filter_by(server_id=server_id).one() recent_games = DBSession.query(Game, Server, Map).\ filter(Game.server_id == server_id).\ filter(Game.server_id == Server.server_id).\ filter(Game.map_id == Map.map_id).\ order_by(Game.game_id.desc())[0:10] except Exception as e: server = None recent_games = None return {'server':server, 'recent_games':recent_games} def server_game_index(request): """ List the games played on a given server. Paginated. """ server_id = request.matchdict['server_id'] current_page = request.matchdict['page'] try: server = DBSession.query(Server).filter_by(server_id=server_id).one() games_q = DBSession.query(Game, Server, Map).\ filter(Game.server_id == server_id).\ filter(Game.server_id == Server.server_id).\ filter(Game.map_id == Map.map_id).\ order_by(Game.game_id.desc()) games = Page(games_q, current_page, url=page_url) except Exception as e: server = None games = None raise e return {'games':games, 'server':server} ########################################################################## def map_info(request): """ List the information stored about a given map. """ map_id = request.matchdict['id'] try: gmap = DBSession.query(Map).filter_by(map_id=map_id).one() except: gmap = None return {'gmap':gmap} ########################################################################## def get_or_create_server(session=None, name=None): """ Find a server by name or create one if not found. Parameters: session - SQLAlchemy database session factory name - server name of the server to be found or created """ try: # find one by that name, if it exists server = session.query(Server).filter_by(name=name).one() log.debug("Found server id {0} with name {1}.".format( server.server_id, server.name)) except NoResultFound, e: server = Server(name=name) session.add(server) session.flush() log.debug("Created server id {0} with name {1}".format( server.server_id, server.name)) except MultipleResultsFound, e: # multiple found, so use the first one but warn log.debug(e) servers = session.query(Server).filter_by(name=name).order_by( Server.server_id).all() server = servers[0] log.debug("Created server id {0} with name {1} but found \ multiple".format( server.server_id, server.name)) return server def get_or_create_map(session=None, name=None): """ Find a map by name or create one if not found. Parameters: session - SQLAlchemy database session factory name - map name of the map to be found or created """ try: # find one by the name, if it exists gmap = session.query(Map).filter_by(name=name).one() log.debug("Found map id {0} with name {1}.".format(gmap.map_id, gmap.name)) except NoResultFound, e: gmap = Map(name=name) session.add(gmap) session.flush() log.debug("Created map id {0} with name {1}.".format(gmap.map_id, gmap.name)) except MultipleResultsFound, e: # multiple found, so use the first one but warn log.debug(e) gmaps = session.query(Map).filter_by(name=name).order_by( Map.map_id).all() gmap = gmaps[0] log.debug("Found map id {0} with name {1} but found \ multiple.".format(gmap.map_id, gmap.name)) return gmap def create_game(session=None, start_dt=None, game_type_cd=None, server_id=None, map_id=None, 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 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 """ game = Game(start_dt=start_dt, game_type_cd=game_type_cd, server_id=server_id, map_id=map_id, winner=winner) session.add(game) session.flush() log.debug("Created game id {0} on server {1}, map {2} at time \ {3} and on map {4}".format(game.game_id, server_id, map_id, start_dt, map_id)) return game def get_or_create_player(session=None, hashkey=None, nick=None): """ Finds a player by hashkey or creates a new one (along with a corresponding hashkey entry. Parameters: session - SQLAlchemy database session factory hashkey - hashkey of the player to be found or created nick - nick of the player (in case of a first time create) """ # if we have a bot if 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): player = session.query(Player).filter_by(player_id=2).one() # else it is a tracked player else: # see if the player is already in the database # if not, create one and the hashkey along with it try: hashkey = session.query(Hashkey).filter_by( hashkey=hashkey).one() player = session.query(Player).filter_by( player_id=hashkey.player_id).one() log.debug("Found existing player {0} with hashkey {1}.".format( player.player_id, hashkey.hashkey)) except: player = Player() if nick: player.nick = nick session.add(player) session.flush() hashkey = Hashkey(player_id=player.player_id, hashkey=hashkey) session.add(hashkey) log.debug("Created player {0} with hashkey {1}.".format( player.player_id, hashkey.hashkey)) 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 """ # 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() pgstat = PlayerGameStat(create_dt=datetime.datetime.now()) # set player id from player record pgstat.player_id = player.player_id #set game id from game record pgstat.game_id = game.game_id # all games have a score pgstat.score = 0 if game.game_type_cd == 'dm': pgstat.kills = 0 pgstat.deaths = 0 pgstat.suicides = 0 elif game.game_type_cd == 'ctf': pgstat.kills = 0 pgstat.captures = 0 pgstat.pickups = 0 pgstat.drops = 0 pgstat.returns = 0 pgstat.carrier_frags = 0 for (key,value) in player_events.items(): if key == 'n': pgstat.nick = value 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 session.add(pgstat) session.flush() 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 """ pwstats = [] for (key,value) in player_events.items(): matched = re.search("acc-(.*?)-cnt-fired", key) if matched: weapon_cd = matched.group(1) pwstat = PlayerWeaponStat() pwstat.player_id = player.player_id pwstat.game_id = game.game_id 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'] else: pwstat.nick = player_events['P'] if 'acc-' + weapon_cd + '-cnt-fired' in player_events: pwstat.fired = int(round(float( player_events['acc-' + weapon_cd + '-cnt-fired']))) if 'acc-' + weapon_cd + '-fired' in player_events: pwstat.max = int(round(float( player_events['acc-' + weapon_cd + '-fired']))) if 'acc-' + weapon_cd + '-cnt-hit' in player_events: pwstat.hit = int(round(float( player_events['acc-' + weapon_cd + '-cnt-hit']))) if 'acc-' + weapon_cd + '-hit' in player_events: pwstat.actual = int(round(float( player_events['acc-' + weapon_cd + '-hit']))) if 'acc-' + weapon_cd + '-frags' in player_events: pwstat.frags = int(round(float( player_events['acc-' + weapon_cd + '-frags']))) session.add(pwstat) pwstats.append(pwstat) 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 = [] log.debug(request.body) for line in request.body.split('\n'): try: (key, value) = line.strip().split(' ', 1) if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W': 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 """ if 'joins' in player_events and 'matches' in player_events\ and 'scoreboardvalid' in player_events: pgstat = create_player_game_stat(session=session, player=player, game=game, player_events=player_events) 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 stats_submit(request): """ Entry handler for POST stats submissions. """ try: session = DBSession() (game_meta, players) = parse_body(request) # verify required metadata is present if 'T' not in game_meta or\ 'G' not in game_meta or\ 'M' not in game_meta or\ 'S' not in game_meta: log.debug("Required game meta fields (T, G, M, or S) missing. "\ "Can't continue.") raise Exception("Required game meta fields (T, G, M, or S) missing.") has_real_players = False for player_events in players: if not player_events['P'].startswith('bot'): if 'joins' in player_events and 'matches' in player_events\ and 'scoreboardvalid' in player_events: has_real_players = True if not has_real_players: raise Exception("No real players found. Stats ignored.") server = get_or_create_server(session=session, name=game_meta['S']) gmap = get_or_create_map(session=session, name=game_meta['M']) if 'W' in game_meta: winner = game_meta['W'] else: winner = None game = create_game(session=session, 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, winner=winner) # 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 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) session.commit() log.debug('Success! Stats recorded.') return Response('200 OK') except Exception as e: session.rollback() raise e