7 import pyramid.httpexceptions
8 from sqlalchemy import Sequence
9 from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
10 from xonstat.elo import EloProcessor
11 from xonstat.models import DBSession, Server, Map, Game, PlayerGameStat, PlayerWeaponStat
12 from xonstat.models import PlayerRank, PlayerCaptime
13 from xonstat.models import TeamGameStat, PlayerGameAnticheat, Player, Hashkey, PlayerNick
14 from xonstat.util import strip_colors, qfont_decode, verify_request, weapon_map
16 log = logging.getLogger(__name__)
19 class Submission(object):
20 """Parses an incoming POST request for stats submissions."""
22 def __init__(self, body, headers):
23 # a copy of the HTTP headers
24 self.headers = headers
26 # a copy of the HTTP POST body
38 # distinct weapons that we have seen fired
41 # number of real players in the match
44 # the parsing deque (we use this to allow peeking)
45 self.q = collections.deque(self.body.split("\n"))
48 """Returns the next key:value pair off the queue."""
50 items = self.q.popleft().strip().split(' ', 1)
52 # Some keys won't have values, like 'L' records where the server isn't actually
53 # participating in any ladders. These can be safely ignored.
60 def check_for_new_weapon_fired(self, sub_key):
61 """Checks if a given player key (subkey, actually) is a new weapon fired in the match."""
62 if sub_key.endswith("cnt-fired"):
63 weapon = sub_key.split("-")[1]
64 if weapon not in self.weapons:
65 self.weapons.add(weapon)
67 def parse_player(self, key, pid):
68 """Construct a player events listing from the submission."""
70 # all of the keys related to player records
71 player_keys = ['i', 'n', 't', 'e']
75 # Consume all following 'i' 'n' 't' 'e' records
76 while len(self.q) > 0:
77 (key, value) = self.next_item()
78 if key is None and value is None:
81 (sub_key, sub_value) = value.split(' ', 1)
82 player[sub_key] = sub_value
84 # keep track of the distinct weapons fired during the match
85 self.check_for_new_weapon_fired(sub_key)
87 player[key] = unicode(value, 'utf-8')
88 elif key in player_keys:
91 # something we didn't expect - put it back on the deque
92 self.q.appendleft("{} {}".format(key, value))
95 if is_real_player(player) and played_in_game(player):
96 self.real_players += 1
98 self.players.append(player)
100 def parse_team(self, key, tid):
101 """Construct a team events listing from the submission."""
104 # Consume all following 'e' records
105 while len(self.q) > 0 and self.q[0].startswith('e'):
106 (_, value) = self.next_item()
107 (sub_key, sub_value) = value.split(' ', 1)
108 team[sub_key] = sub_value
110 self.teams.append(team)
113 """Parses the request body into instance variables."""
114 while len(self.q) > 0:
115 (key, value) = self.next_item()
116 if key is None and value is None:
119 self.meta[key] = unicode(value, 'utf-8')
121 self.parse_player(key, value)
123 self.parse_team(key, value)
125 self.meta[key] = value
130 def parse_stats_submission(body):
132 Parses the POST request body for a stats submission
134 # storage vars for the request body
140 # we're not in either stanza to start
143 for line in body.split('\n'):
145 (key, value) = line.strip().split(' ', 1)
147 # Server (S) and Nick (n) fields can have international characters.
149 value = unicode(value, 'utf-8')
151 if key not in 'P' 'Q' 'n' 'e' 't' 'i':
152 game_meta[key] = value
154 if key == 'Q' or key == 'P':
155 #log.debug('Found a {0}'.format(key))
156 #log.debug('in_Q: {0}'.format(in_Q))
157 #log.debug('in_P: {0}'.format(in_P))
158 #log.debug('events: {0}'.format(events))
160 # check where we were before and append events accordingly
161 if in_Q and len(events) > 0:
162 #log.debug('creating a team (Q) entry')
165 elif in_P and len(events) > 0:
166 #log.debug('creating a player (P) entry')
167 players.append(events)
171 #log.debug('key == P')
175 #log.debug('key == Q')
182 (subkey, subvalue) = value.split(' ', 1)
183 events[subkey] = subvalue
189 # no key/value pair - move on to the next line
192 # add the last entity we were working on
193 if in_P and len(events) > 0:
194 players.append(events)
195 elif in_Q and len(events) > 0:
198 return (game_meta, players, teams)
201 def is_blank_game(gametype, players):
202 """Determine if this is a blank game or not. A blank game is either:
204 1) a match that ended in the warmup stage, where accuracy events are not
205 present (for non-CTS games)
207 2) a match in which no player made a positive or negative score AND was
210 ... or for CTS, which doesn't record accuracy events
212 1) a match in which no player made a fastest lap AND was
215 ... or for NB, in which not all maps have weapons
217 1) a match in which no player made a positive or negative score
219 r = re.compile(r'acc-.*-cnt-fired')
220 flg_nonzero_score = False
221 flg_acc_events = False
222 flg_fastest_lap = False
224 for events in players:
225 if is_real_player(events) and played_in_game(events):
226 for (key,value) in events.items():
227 if key == 'scoreboard-score' and value != 0:
228 flg_nonzero_score = True
230 flg_acc_events = True
231 if key == 'scoreboard-fastest':
232 flg_fastest_lap = True
234 if gametype == 'cts':
235 return not flg_fastest_lap
236 elif gametype == 'nb':
237 return not flg_nonzero_score
239 return not (flg_nonzero_score and flg_acc_events)
242 def get_remote_addr(request):
243 """Get the Xonotic server's IP address"""
244 if 'X-Forwarded-For' in request.headers:
245 return request.headers['X-Forwarded-For']
247 return request.remote_addr
250 def is_supported_gametype(gametype, version):
251 """Whether a gametype is supported or not"""
254 # if the type can be supported, but with version constraints, uncomment
255 # here and add the restriction for a specific version below
256 supported_game_types = (
275 if gametype in supported_game_types:
280 # some game types were buggy before revisions, thus this additional filter
281 if gametype == 'ca' and version <= 5:
287 def do_precondition_checks(request, game_meta, raw_players):
288 """Precondition checks for ALL gametypes.
289 These do not require a database connection."""
290 if not has_required_metadata(game_meta):
291 msg = "Missing required game metadata"
293 raise pyramid.httpexceptions.HTTPUnprocessableEntity(
295 content_type="text/plain"
299 version = int(game_meta['V'])
301 msg = "Invalid or incorrect game metadata provided"
303 raise pyramid.httpexceptions.HTTPUnprocessableEntity(
305 content_type="text/plain"
308 if not is_supported_gametype(game_meta['G'], version):
309 msg = "Unsupported game type ({})".format(game_meta['G'])
311 raise pyramid.httpexceptions.HTTPOk(
313 content_type="text/plain"
316 if not has_minimum_real_players(request.registry.settings, raw_players):
317 msg = "Not enough real players"
319 raise pyramid.httpexceptions.HTTPOk(
321 content_type="text/plain"
324 if is_blank_game(game_meta['G'], raw_players):
327 raise pyramid.httpexceptions.HTTPOk(
329 content_type="text/plain"
333 def is_real_player(events):
335 Determines if a given set of events correspond with a non-bot
337 if not events['P'].startswith('bot'):
343 def played_in_game(events):
345 Determines if a given set of player events correspond with a player who
346 played in the game (matches 1 and scoreboardvalid 1)
348 if 'matches' in events and 'scoreboardvalid' in events:
354 def num_real_players(player_events):
356 Returns the number of real players (those who played
357 and are on the scoreboard).
361 for events in player_events:
362 if is_real_player(events) and played_in_game(events):
368 def has_minimum_real_players(settings, player_events):
370 Determines if the collection of player events has enough "real" players
371 to store in the database. The minimum setting comes from the config file
372 under the setting xonstat.minimum_real_players.
374 flg_has_min_real_players = True
377 minimum_required_players = int(
378 settings['xonstat.minimum_required_players'])
380 minimum_required_players = 2
382 real_players = num_real_players(player_events)
384 if real_players < minimum_required_players:
385 flg_has_min_real_players = False
387 return flg_has_min_real_players
390 def has_required_metadata(metadata):
392 Determines if a give set of metadata has enough data to create a game,
393 server, and map with.
395 flg_has_req_metadata = True
397 if 'G' not in metadata or\
398 'M' not in metadata or\
399 'I' not in metadata or\
401 flg_has_req_metadata = False
403 return flg_has_req_metadata
406 def should_do_weapon_stats(game_type_cd):
407 """True of the game type should record weapon stats. False otherwise."""
408 if game_type_cd in 'cts':
414 def gametype_elo_eligible(game_type_cd):
415 """True of the game type should process Elos. False otherwise."""
416 elo_game_types = ('duel', 'dm', 'ca', 'ctf', 'tdm', 'ka', 'ft')
418 if game_type_cd in elo_game_types:
424 def register_new_nick(session, player, new_nick):
426 Change the player record's nick to the newly found nick. Store the old
427 nick in the player_nicks table for that player.
429 session - SQLAlchemy database session factory
430 player - player record whose nick is changing
431 new_nick - the new nickname
433 # see if that nick already exists
434 stripped_nick = strip_colors(qfont_decode(player.nick))
436 player_nick = session.query(PlayerNick).filter_by(
437 player_id=player.player_id, stripped_nick=stripped_nick).one()
438 except NoResultFound, e:
439 # player_id/stripped_nick not found, create one
440 # but we don't store "Anonymous Player #N"
441 if not re.search('^Anonymous Player #\d+$', player.nick):
442 player_nick = PlayerNick()
443 player_nick.player_id = player.player_id
444 player_nick.stripped_nick = stripped_nick
445 player_nick.nick = player.nick
446 session.add(player_nick)
448 # We change to the new nick regardless
449 player.nick = new_nick
450 player.stripped_nick = strip_colors(qfont_decode(new_nick))
454 def update_fastest_cap(session, player_id, game_id, map_id, captime, mod):
456 Check the fastest cap time for the player and map. If there isn't
457 one, insert one. If there is, check if the passed time is faster.
460 # we don't record fastest cap times for bots or anonymous players
464 # see if a cap entry exists already
465 # then check to see if the new captime is faster
467 cur_fastest_cap = session.query(PlayerCaptime).filter_by(
468 player_id=player_id, map_id=map_id, mod=mod).one()
470 # current captime is faster, so update
471 if captime < cur_fastest_cap.fastest_cap:
472 cur_fastest_cap.fastest_cap = captime
473 cur_fastest_cap.game_id = game_id
474 cur_fastest_cap.create_dt = datetime.datetime.utcnow()
475 session.add(cur_fastest_cap)
477 except NoResultFound, e:
478 # none exists, so insert
479 cur_fastest_cap = PlayerCaptime(player_id, game_id, map_id, captime,
481 session.add(cur_fastest_cap)
485 def update_server(server, name, hashkey, ip_addr, port, revision, impure_cvars):
487 Updates the server in the given DB session, if needed.
489 :param server: The found server instance.
490 :param name: The incoming server name.
491 :param hashkey: The incoming server hashkey.
492 :param ip_addr: The incoming server IP address.
493 :param port: The incoming server port.
494 :param revision: The incoming server revision.
495 :param impure_cvars: The incoming number of impure server cvars.
498 # ensure the two int attributes are actually ints
505 impure_cvars = int(impure_cvars)
510 if name and server.name != name:
513 if hashkey and server.hashkey != hashkey:
514 server.hashkey = hashkey
516 if ip_addr and server.ip_addr != ip_addr:
517 server.ip_addr = ip_addr
519 if port and server.port != port:
522 if revision and server.revision != revision:
523 server.revision = revision
525 if impure_cvars and server.impure_cvars != impure_cvars:
526 server.impure_cvars = impure_cvars
527 server.pure_ind = True if impure_cvars == 0 else False
533 def get_or_create_server(session, name, hashkey, ip_addr, revision, port, impure_cvars):
535 Find a server by name or create one if not found. Parameters:
537 session - SQLAlchemy database session factory
538 name - server name of the server to be found or created
539 hashkey - server hashkey
540 ip_addr - the IP address of the server
541 revision - the xonotic revision number
542 port - the port number of the server
543 impure_cvars - the number of impure cvar changes
545 servers_q = DBSession.query(Server).filter(Server.active_ind)
548 # if the hashkey is provided, we'll use that
549 servers_q = servers_q.filter((Server.name == name) or (Server.hashkey == hashkey))
551 # otherwise, it is just by name
552 servers_q = servers_q.filter(Server.name == name)
554 # order by the hashkey, which means any hashkey match will appear first if there are multiple
555 servers = servers_q.order_by(Server.hashkey, Server.create_dt).all()
557 if len(servers) == 0:
558 server = Server(name=name, hashkey=hashkey)
561 log.debug("Created server {} with hashkey {}.".format(server.server_id, server.hashkey))
564 if len(servers) == 1:
565 log.info("Found existing server {}.".format(server.server_id))
567 elif len(servers) > 1:
568 server_id_list = ", ".join(["{}".format(s.server_id) for s in servers])
569 log.warn("Multiple servers found ({})! Using the first one ({})."
570 .format(server_id_list, server.server_id))
572 if update_server(server, name, hashkey, ip_addr, port, revision, impure_cvars):
578 def get_or_create_map(session=None, name=None):
580 Find a map by name or create one if not found. Parameters:
582 session - SQLAlchemy database session factory
583 name - map name of the map to be found or created
586 # find one by the name, if it exists
587 gmap = session.query(Map).filter_by(name=name).one()
588 log.debug("Found map id {0}: {1}".format(gmap.map_id,
590 except NoResultFound, e:
591 gmap = Map(name=name)
594 log.debug("Created map id {0}: {1}".format(gmap.map_id,
596 except MultipleResultsFound, e:
597 # multiple found, so use the first one but warn
599 gmaps = session.query(Map).filter_by(name=name).order_by(
602 log.debug("Found map id {0}: {1} but found \
603 multiple".format(gmap.map_id, gmap.name))
608 def create_game(session, start_dt, game_type_cd, server_id, map_id,
609 match_id, duration, mod, winner=None):
611 Creates a game. Parameters:
613 session - SQLAlchemy database session factory
614 start_dt - when the game started (datetime object)
615 game_type_cd - the game type of the game being played
616 server_id - server identifier of the server hosting the game
617 map_id - map on which the game was played
618 winner - the team id of the team that won
619 duration - how long the game lasted
620 mod - mods in use during the game
622 seq = Sequence('games_game_id_seq')
623 game_id = session.execute(seq)
624 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
625 server_id=server_id, map_id=map_id, winner=winner)
626 game.match_id = match_id
629 # There is some drift between start_dt (provided by app) and create_dt
630 # (default in the database), so we'll make them the same until this is
632 game.create_dt = start_dt
635 game.duration = datetime.timedelta(seconds=int(round(float(duration))))
640 session.query(Game).filter(Game.server_id==server_id).\
641 filter(Game.match_id==match_id).one()
643 log.debug("Error: game with same server and match_id found! Ignoring.")
645 # if a game under the same server and match_id found,
646 # this is a duplicate game and can be ignored
647 raise pyramid.httpexceptions.HTTPOk('OK')
648 except NoResultFound, e:
649 # server_id/match_id combination not found. game is ok to insert
652 log.debug("Created game id {0} on server {1}, map {2} at \
653 {3}".format(game.game_id,
654 server_id, map_id, start_dt))
659 def get_or_create_player(session=None, hashkey=None, nick=None):
661 Finds a player by hashkey or creates a new one (along with a
662 corresponding hashkey entry. Parameters:
664 session - SQLAlchemy database session factory
665 hashkey - hashkey of the player to be found or created
666 nick - nick of the player (in case of a first time create)
669 if re.search('^bot#\d+', hashkey):
670 player = session.query(Player).filter_by(player_id=1).one()
671 # if we have an untracked player
672 elif re.search('^player#\d+$', hashkey):
673 player = session.query(Player).filter_by(player_id=2).one()
674 # else it is a tracked player
676 # see if the player is already in the database
677 # if not, create one and the hashkey along with it
679 hk = session.query(Hashkey).filter_by(
680 hashkey=hashkey).one()
681 player = session.query(Player).filter_by(
682 player_id=hk.player_id).one()
683 log.debug("Found existing player {0} with hashkey {1}".format(
684 player.player_id, hashkey))
690 # if nick is given to us, use it. If not, use "Anonymous Player"
691 # with a suffix added for uniqueness.
693 player.nick = nick[:128]
694 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
696 player.nick = "Anonymous Player #{0}".format(player.player_id)
697 player.stripped_nick = player.nick
699 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
701 log.debug("Created player {0} ({2}) with hashkey {1}".format(
702 player.player_id, hashkey, player.nick.encode('utf-8')))
707 def create_default_game_stat(session, game_type_cd):
708 """Creates a blanked-out pgstat record for the given game type"""
710 # this is what we have to do to get partitioned records in - grab the
711 # sequence value first, then insert using the explicit ID (vs autogenerate)
712 seq = Sequence('player_game_stats_player_game_stat_id_seq')
713 pgstat_id = session.execute(seq)
714 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
715 create_dt=datetime.datetime.utcnow())
717 if game_type_cd == 'as':
718 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
720 if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
721 pgstat.kills = pgstat.deaths = pgstat.suicides = 0
723 if game_type_cd == 'cq':
724 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
727 if game_type_cd == 'ctf':
728 pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
729 pgstat.returns = pgstat.carrier_frags = 0
731 if game_type_cd == 'cts':
734 if game_type_cd == 'dom':
735 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
738 if game_type_cd == 'ft':
739 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
741 if game_type_cd == 'ka':
742 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
743 pgstat.carrier_frags = 0
744 pgstat.time = datetime.timedelta(seconds=0)
746 if game_type_cd == 'kh':
747 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
748 pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
749 pgstat.carrier_frags = 0
751 if game_type_cd == 'lms':
752 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
754 if game_type_cd == 'nb':
755 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
758 if game_type_cd == 'rc':
759 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
764 def create_game_stat(session, game_meta, game, server, gmap, player, events):
765 """Game stats handler for all game types"""
767 game_type_cd = game.game_type_cd
769 pgstat = create_default_game_stat(session, game_type_cd)
771 # these fields should be on every pgstat record
772 pgstat.game_id = game.game_id
773 pgstat.player_id = player.player_id
774 pgstat.nick = events.get('n', 'Anonymous Player')[:128]
775 pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
776 pgstat.score = int(round(float(events.get('scoreboard-score', 0))))
777 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
778 pgstat.rank = int(events.get('rank', None))
779 pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
781 if pgstat.nick != player.nick \
782 and player.player_id > 2 \
783 and pgstat.nick != 'Anonymous Player':
784 register_new_nick(session, player, pgstat.nick)
788 # gametype-specific stuff is handled here. if passed to us, we store it
789 for (key,value) in events.items():
790 if key == 'wins': wins = True
791 if key == 't': pgstat.team = int(value)
793 if key == 'scoreboard-drops': pgstat.drops = int(value)
794 if key == 'scoreboard-returns': pgstat.returns = int(value)
795 if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
796 if key == 'scoreboard-pickups': pgstat.pickups = int(value)
797 if key == 'scoreboard-caps': pgstat.captures = int(value)
798 if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
799 if key == 'scoreboard-deaths': pgstat.deaths = int(value)
800 if key == 'scoreboard-kills': pgstat.kills = int(value)
801 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
802 if key == 'scoreboard-objectives': pgstat.collects = int(value)
803 if key == 'scoreboard-captured': pgstat.captures = int(value)
804 if key == 'scoreboard-released': pgstat.drops = int(value)
805 if key == 'scoreboard-fastest':
806 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
807 if key == 'scoreboard-takes': pgstat.pickups = int(value)
808 if key == 'scoreboard-ticks': pgstat.drops = int(value)
809 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
810 if key == 'scoreboard-bctime':
811 pgstat.time = datetime.timedelta(seconds=int(value))
812 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
813 if key == 'scoreboard-losses': pgstat.drops = int(value)
814 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
815 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
816 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
817 if key == 'scoreboard-lives': pgstat.lives = int(value)
818 if key == 'scoreboard-goals': pgstat.captures = int(value)
819 if key == 'scoreboard-faults': pgstat.drops = int(value)
820 if key == 'scoreboard-laps': pgstat.laps = int(value)
822 if key == 'avglatency': pgstat.avg_latency = float(value)
823 if key == 'scoreboard-captime':
824 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
825 if game.game_type_cd == 'ctf':
826 update_fastest_cap(session, player.player_id, game.game_id,
827 gmap.map_id, pgstat.fastest, game.mod)
829 # there is no "winning team" field, so we have to derive it
830 if wins and pgstat.team is not None and game.winner is None:
831 game.winner = pgstat.team
839 def create_anticheats(session, pgstat, game, player, events):
840 """Anticheats handler for all game types"""
844 # all anticheat events are prefixed by "anticheat"
845 for (key,value) in events.items():
846 if key.startswith("anticheat"):
848 ac = PlayerGameAnticheat(
854 anticheats.append(ac)
856 except Exception as e:
857 log.debug("Could not parse value for key %s. Ignoring." % key)
862 def create_default_team_stat(session, game_type_cd):
863 """Creates a blanked-out teamstat record for the given game type"""
865 # this is what we have to do to get partitioned records in - grab the
866 # sequence value first, then insert using the explicit ID (vs autogenerate)
867 seq = Sequence('team_game_stats_team_game_stat_id_seq')
868 teamstat_id = session.execute(seq)
869 teamstat = TeamGameStat(team_game_stat_id=teamstat_id,
870 create_dt=datetime.datetime.utcnow())
872 # all team game modes have a score, so we'll zero that out always
875 if game_type_cd in 'ca' 'ft' 'lms' 'ka':
878 if game_type_cd == 'ctf':
884 def create_team_stat(session, game, events):
885 """Team stats handler for all game types"""
888 teamstat = create_default_team_stat(session, game.game_type_cd)
889 teamstat.game_id = game.game_id
891 # we should have a team ID if we have a 'Q' event
892 if re.match(r'^team#\d+$', events.get('Q', '')):
893 team = int(events.get('Q').replace('team#', ''))
896 # gametype-specific stuff is handled here. if passed to us, we store it
897 for (key,value) in events.items():
898 if key == 'scoreboard-score': teamstat.score = int(round(float(value)))
899 if key == 'scoreboard-caps': teamstat.caps = int(value)
900 if key == 'scoreboard-goals': teamstat.caps = int(value)
901 if key == 'scoreboard-rounds': teamstat.rounds = int(value)
903 session.add(teamstat)
904 except Exception as e:
910 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
911 """Weapon stats handler for all game types"""
914 # Version 1 of stats submissions doubled the data sent.
915 # To counteract this we divide the data by 2 only for
916 # POSTs coming from version 1.
918 version = int(game_meta['V'])
921 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
927 for (key,value) in events.items():
928 matched = re.search("acc-(.*?)-cnt-fired", key)
930 weapon_cd = matched.group(1)
932 # Weapon names changed for 0.8. We'll convert the old
933 # ones to use the new scheme as well.
934 mapped_weapon_cd = weapon_map.get(weapon_cd, weapon_cd)
936 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
937 pwstat_id = session.execute(seq)
938 pwstat = PlayerWeaponStat()
939 pwstat.player_weapon_stats_id = pwstat_id
940 pwstat.player_id = player.player_id
941 pwstat.game_id = game.game_id
942 pwstat.player_game_stat_id = pgstat.player_game_stat_id
943 pwstat.weapon_cd = mapped_weapon_cd
946 pwstat.nick = events['n']
948 pwstat.nick = events['P']
950 if 'acc-' + weapon_cd + '-cnt-fired' in events:
951 pwstat.fired = int(round(float(
952 events['acc-' + weapon_cd + '-cnt-fired'])))
953 if 'acc-' + weapon_cd + '-fired' in events:
954 pwstat.max = int(round(float(
955 events['acc-' + weapon_cd + '-fired'])))
956 if 'acc-' + weapon_cd + '-cnt-hit' in events:
957 pwstat.hit = int(round(float(
958 events['acc-' + weapon_cd + '-cnt-hit'])))
959 if 'acc-' + weapon_cd + '-hit' in events:
960 pwstat.actual = int(round(float(
961 events['acc-' + weapon_cd + '-hit'])))
962 if 'acc-' + weapon_cd + '-frags' in events:
963 pwstat.frags = int(round(float(
964 events['acc-' + weapon_cd + '-frags'])))
967 pwstat.fired = pwstat.fired/2
968 pwstat.max = pwstat.max/2
969 pwstat.hit = pwstat.hit/2
970 pwstat.actual = pwstat.actual/2
971 pwstat.frags = pwstat.frags/2
974 pwstats.append(pwstat)
979 def get_ranks(session, player_ids, game_type_cd):
981 Gets the rank entries for all players in the given list, returning a dict
982 of player_id -> PlayerRank instance. The rank entry corresponds to the
983 game type of the parameter passed in as well.
986 for pr in session.query(PlayerRank).\
987 filter(PlayerRank.player_id.in_(player_ids)).\
988 filter(PlayerRank.game_type_cd == game_type_cd).\
990 ranks[pr.player_id] = pr
995 def submit_stats(request):
997 Entry handler for POST stats submissions.
1000 # placeholder for the actual session
1003 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
1004 "----- END REQUEST BODY -----\n\n")
1006 (idfp, status) = verify_request(request)
1007 (game_meta, raw_players, raw_teams) = parse_stats_submission(request.body)
1008 revision = game_meta.get('R', 'unknown')
1009 duration = game_meta.get('D', None)
1011 # only players present at the end of the match are eligible for stats
1012 raw_players = filter(played_in_game, raw_players)
1014 do_precondition_checks(request, game_meta, raw_players)
1016 # the "duel" gametype is fake
1017 if len(raw_players) == 2 \
1018 and num_real_players(raw_players) == 2 \
1019 and game_meta['G'] == 'dm':
1020 game_meta['G'] = 'duel'
1022 #----------------------------------------------------------------------
1023 # Actual setup (inserts/updates) below here
1024 #----------------------------------------------------------------------
1025 session = DBSession()
1027 game_type_cd = game_meta['G']
1029 # All game types create Game, Server, Map, and Player records
1031 server = get_or_create_server(
1034 name = game_meta['S'],
1035 revision = revision,
1036 ip_addr = get_remote_addr(request),
1037 port = game_meta.get('U', None),
1038 impure_cvars = game_meta.get('C', 0))
1040 gmap = get_or_create_map(
1042 name = game_meta['M'])
1046 start_dt = datetime.datetime.utcnow(),
1047 server_id = server.server_id,
1048 game_type_cd = game_type_cd,
1049 map_id = gmap.map_id,
1050 match_id = game_meta['I'],
1051 duration = duration,
1052 mod = game_meta.get('O', None))
1054 # keep track of the players we've seen
1058 for events in raw_players:
1059 player = get_or_create_player(
1061 hashkey = events['P'],
1062 nick = events.get('n', None))
1064 pgstat = create_game_stat(session, game_meta, game, server,
1065 gmap, player, events)
1066 pgstats.append(pgstat)
1068 if player.player_id > 1:
1069 anticheats = create_anticheats(session, pgstat, game, player, events)
1071 if player.player_id > 2:
1072 player_ids.append(player.player_id)
1073 hashkeys[player.player_id] = events['P']
1075 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
1076 pwstats = create_weapon_stats(session, game_meta, game, player,
1079 # store them on games for easy access
1080 game.players = player_ids
1082 for events in raw_teams:
1084 teamstat = create_team_stat(session, game, events)
1085 except Exception as e:
1088 if server.elo_ind and gametype_elo_eligible(game_type_cd):
1089 ep = EloProcessor(session, game, pgstats)
1093 log.debug('Success! Stats recorded.')
1095 # ranks are fetched after we've done the "real" processing
1096 ranks = get_ranks(session, player_ids, game_type_cd)
1098 # plain text response
1099 request.response.content_type = 'text/plain'
1102 "now" : calendar.timegm(datetime.datetime.utcnow().timetuple()),
1106 "player_ids" : player_ids,
1107 "hashkeys" : hashkeys,
1112 except Exception as e: