6 import pyramid.httpexceptions
7 from sqlalchemy import Sequence
8 from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
9 from xonstat.elo import EloProcessor
10 from xonstat.models import DBSession, Server, Map, Game, PlayerGameStat, PlayerWeaponStat
11 from xonstat.models import PlayerRank, PlayerCaptime
12 from xonstat.models import TeamGameStat, PlayerGameAnticheat, Player, Hashkey, PlayerNick
13 from xonstat.util import strip_colors, qfont_decode, verify_request, weapon_map
15 log = logging.getLogger(__name__)
18 def parse_stats_submission(body):
20 Parses the POST request body for a stats submission
22 # storage vars for the request body
28 # we're not in either stanza to start
31 for line in body.split('\n'):
33 (key, value) = line.strip().split(' ', 1)
35 # Server (S) and Nick (n) fields can have international characters.
37 value = unicode(value, 'utf-8')
39 if key not in 'P' 'Q' 'n' 'e' 't' 'i':
40 game_meta[key] = value
42 if key == 'Q' or key == 'P':
43 #log.debug('Found a {0}'.format(key))
44 #log.debug('in_Q: {0}'.format(in_Q))
45 #log.debug('in_P: {0}'.format(in_P))
46 #log.debug('events: {0}'.format(events))
48 # check where we were before and append events accordingly
49 if in_Q and len(events) > 0:
50 #log.debug('creating a team (Q) entry')
53 elif in_P and len(events) > 0:
54 #log.debug('creating a player (P) entry')
55 players.append(events)
59 #log.debug('key == P')
63 #log.debug('key == Q')
70 (subkey, subvalue) = value.split(' ', 1)
71 events[subkey] = subvalue
77 # no key/value pair - move on to the next line
80 # add the last entity we were working on
81 if in_P and len(events) > 0:
82 players.append(events)
83 elif in_Q and len(events) > 0:
86 return (game_meta, players, teams)
89 def is_blank_game(gametype, players):
90 """Determine if this is a blank game or not. A blank game is either:
92 1) a match that ended in the warmup stage, where accuracy events are not
93 present (for non-CTS games)
95 2) a match in which no player made a positive or negative score AND was
98 ... or for CTS, which doesn't record accuracy events
100 1) a match in which no player made a fastest lap AND was
103 ... or for NB, in which not all maps have weapons
105 1) a match in which no player made a positive or negative score
107 r = re.compile(r'acc-.*-cnt-fired')
108 flg_nonzero_score = False
109 flg_acc_events = False
110 flg_fastest_lap = False
112 for events in players:
113 if is_real_player(events) and played_in_game(events):
114 for (key,value) in events.items():
115 if key == 'scoreboard-score' and value != 0:
116 flg_nonzero_score = True
118 flg_acc_events = True
119 if key == 'scoreboard-fastest':
120 flg_fastest_lap = True
122 if gametype == 'cts':
123 return not flg_fastest_lap
124 elif gametype == 'nb':
125 return not flg_nonzero_score
127 return not (flg_nonzero_score and flg_acc_events)
130 def get_remote_addr(request):
131 """Get the Xonotic server's IP address"""
132 if 'X-Forwarded-For' in request.headers:
133 return request.headers['X-Forwarded-For']
135 return request.remote_addr
138 def is_supported_gametype(gametype, version):
139 """Whether a gametype is supported or not"""
142 # if the type can be supported, but with version constraints, uncomment
143 # here and add the restriction for a specific version below
144 supported_game_types = (
162 if gametype in supported_game_types:
167 # some game types were buggy before revisions, thus this additional filter
168 if gametype == 'ca' and version <= 5:
174 def do_precondition_checks(request, game_meta, raw_players):
175 """Precondition checks for ALL gametypes.
176 These do not require a database connection."""
177 if not has_required_metadata(game_meta):
178 log.debug("ERROR: Required game meta missing")
179 raise pyramid.httpexceptions.HTTPUnprocessableEntity("Missing game meta")
182 version = int(game_meta['V'])
184 log.debug("ERROR: Required game meta invalid")
185 raise pyramid.httpexceptions.HTTPUnprocessableEntity("Invalid game meta")
187 if not is_supported_gametype(game_meta['G'], version):
188 log.debug("ERROR: Unsupported gametype")
189 raise pyramid.httpexceptions.HTTPOk("OK")
191 if not has_minimum_real_players(request.registry.settings, raw_players):
192 log.debug("ERROR: Not enough real players")
193 raise pyramid.httpexceptions.HTTPOk("OK")
195 if is_blank_game(game_meta['G'], raw_players):
196 log.debug("ERROR: Blank game")
197 raise pyramid.httpexceptions.HTTPOk("OK")
200 def is_real_player(events):
202 Determines if a given set of events correspond with a non-bot
204 if not events['P'].startswith('bot'):
210 def played_in_game(events):
212 Determines if a given set of player events correspond with a player who
213 played in the game (matches 1 and scoreboardvalid 1)
215 if 'matches' in events and 'scoreboardvalid' in events:
221 def num_real_players(player_events):
223 Returns the number of real players (those who played
224 and are on the scoreboard).
228 for events in player_events:
229 if is_real_player(events) and played_in_game(events):
235 def has_minimum_real_players(settings, player_events):
237 Determines if the collection of player events has enough "real" players
238 to store in the database. The minimum setting comes from the config file
239 under the setting xonstat.minimum_real_players.
241 flg_has_min_real_players = True
244 minimum_required_players = int(
245 settings['xonstat.minimum_required_players'])
247 minimum_required_players = 2
249 real_players = num_real_players(player_events)
251 if real_players < minimum_required_players:
252 flg_has_min_real_players = False
254 return flg_has_min_real_players
257 def has_required_metadata(metadata):
259 Determines if a give set of metadata has enough data to create a game,
260 server, and map with.
262 flg_has_req_metadata = True
264 if 'G' not in metadata or\
265 'M' not in metadata or\
266 'I' not in metadata or\
268 flg_has_req_metadata = False
270 return flg_has_req_metadata
273 def should_do_weapon_stats(game_type_cd):
274 """True of the game type should record weapon stats. False otherwise."""
275 if game_type_cd in 'cts':
281 def gametype_elo_eligible(game_type_cd):
282 """True of the game type should process Elos. False otherwise."""
283 elo_game_types = ('duel', 'dm', 'ca', 'ctf', 'tdm', 'ka', 'ft')
285 if game_type_cd in elo_game_types:
291 def register_new_nick(session, player, new_nick):
293 Change the player record's nick to the newly found nick. Store the old
294 nick in the player_nicks table for that player.
296 session - SQLAlchemy database session factory
297 player - player record whose nick is changing
298 new_nick - the new nickname
300 # see if that nick already exists
301 stripped_nick = strip_colors(qfont_decode(player.nick))
303 player_nick = session.query(PlayerNick).filter_by(
304 player_id=player.player_id, stripped_nick=stripped_nick).one()
305 except NoResultFound, e:
306 # player_id/stripped_nick not found, create one
307 # but we don't store "Anonymous Player #N"
308 if not re.search('^Anonymous Player #\d+$', player.nick):
309 player_nick = PlayerNick()
310 player_nick.player_id = player.player_id
311 player_nick.stripped_nick = stripped_nick
312 player_nick.nick = player.nick
313 session.add(player_nick)
315 # We change to the new nick regardless
316 player.nick = new_nick
317 player.stripped_nick = strip_colors(qfont_decode(new_nick))
321 def update_fastest_cap(session, player_id, game_id, map_id, captime, mod):
323 Check the fastest cap time for the player and map. If there isn't
324 one, insert one. If there is, check if the passed time is faster.
327 # we don't record fastest cap times for bots or anonymous players
331 # see if a cap entry exists already
332 # then check to see if the new captime is faster
334 cur_fastest_cap = session.query(PlayerCaptime).filter_by(
335 player_id=player_id, map_id=map_id, mod=mod).one()
337 # current captime is faster, so update
338 if captime < cur_fastest_cap.fastest_cap:
339 cur_fastest_cap.fastest_cap = captime
340 cur_fastest_cap.game_id = game_id
341 cur_fastest_cap.create_dt = datetime.datetime.utcnow()
342 session.add(cur_fastest_cap)
344 except NoResultFound, e:
345 # none exists, so insert
346 cur_fastest_cap = PlayerCaptime(player_id, game_id, map_id, captime,
348 session.add(cur_fastest_cap)
352 def update_server(server, name, hashkey, ip_addr, port, revision, impure_cvars):
354 Updates the server in the given DB session, if needed.
356 :param server: The found server instance.
357 :param name: The incoming server name.
358 :param hashkey: The incoming server hashkey.
359 :param ip_addr: The incoming server IP address.
360 :param port: The incoming server port.
361 :param revision: The incoming server revision.
362 :param impure_cvars: The incoming number of impure server cvars.
365 # ensure the two int attributes are actually ints
372 impure_cvars = int(impure_cvars)
377 if name and server.name != name:
380 if hashkey and server.hashkey != hashkey:
381 server.hashkey = hashkey
383 if ip_addr and server.ip_addr != ip_addr:
384 server.ip_addr = ip_addr
386 if port and server.port != port:
389 if revision and server.revision != revision:
390 server.revision = revision
392 if impure_cvars and server.impure_cvars != impure_cvars:
393 server.impure_cvars = impure_cvars
394 server.pure_ind = True if impure_cvars == 0 else False
400 def get_or_create_server(session, name, hashkey, ip_addr, revision, port, impure_cvars):
402 Find a server by name or create one if not found. Parameters:
404 session - SQLAlchemy database session factory
405 name - server name of the server to be found or created
406 hashkey - server hashkey
407 ip_addr - the IP address of the server
408 revision - the xonotic revision number
409 port - the port number of the server
410 impure_cvars - the number of impure cvar changes
412 servers_q = DBSession.query(Server).filter(Server.active_ind)
415 # if the hashkey is provided, we'll use that
416 servers_q = servers_q.filter((Server.name == name) or (Server.hashkey == hashkey))
418 # otherwise, it is just by name
419 servers_q = servers_q.filter(Server.name == name)
421 # order by the hashkey, which means any hashkey match will appear first if there are multiple
422 servers = servers_q.order_by(Server.hashkey, Server.create_dt).all()
424 if len(servers) == 0:
425 server = Server(name=name, hashkey=hashkey)
428 log.debug("Created server {} with hashkey {}.".format(server.server_id, server.hashkey))
431 if len(servers) == 1:
432 log.info("Found existing server {}.".format(server.server_id))
434 elif len(servers) > 1:
435 server_id_list = ", ".join(["{}".format(s.server_id) for s in servers])
436 log.warn("Multiple servers found ({})! Using the first one ({})."
437 .format(server_id_list, server.server_id))
439 if update_server(server, name, hashkey, ip_addr, port, revision, impure_cvars):
445 def get_or_create_map(session=None, name=None):
447 Find a map by name or create one if not found. Parameters:
449 session - SQLAlchemy database session factory
450 name - map name of the map to be found or created
453 # find one by the name, if it exists
454 gmap = session.query(Map).filter_by(name=name).one()
455 log.debug("Found map id {0}: {1}".format(gmap.map_id,
457 except NoResultFound, e:
458 gmap = Map(name=name)
461 log.debug("Created map id {0}: {1}".format(gmap.map_id,
463 except MultipleResultsFound, e:
464 # multiple found, so use the first one but warn
466 gmaps = session.query(Map).filter_by(name=name).order_by(
469 log.debug("Found map id {0}: {1} but found \
470 multiple".format(gmap.map_id, gmap.name))
475 def create_game(session, start_dt, game_type_cd, server_id, map_id,
476 match_id, duration, mod, winner=None):
478 Creates a game. Parameters:
480 session - SQLAlchemy database session factory
481 start_dt - when the game started (datetime object)
482 game_type_cd - the game type of the game being played
483 server_id - server identifier of the server hosting the game
484 map_id - map on which the game was played
485 winner - the team id of the team that won
486 duration - how long the game lasted
487 mod - mods in use during the game
489 seq = Sequence('games_game_id_seq')
490 game_id = session.execute(seq)
491 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
492 server_id=server_id, map_id=map_id, winner=winner)
493 game.match_id = match_id
496 # There is some drift between start_dt (provided by app) and create_dt
497 # (default in the database), so we'll make them the same until this is
499 game.create_dt = start_dt
502 game.duration = datetime.timedelta(seconds=int(round(float(duration))))
507 session.query(Game).filter(Game.server_id==server_id).\
508 filter(Game.match_id==match_id).one()
510 log.debug("Error: game with same server and match_id found! Ignoring.")
512 # if a game under the same server and match_id found,
513 # this is a duplicate game and can be ignored
514 raise pyramid.httpexceptions.HTTPOk('OK')
515 except NoResultFound, e:
516 # server_id/match_id combination not found. game is ok to insert
519 log.debug("Created game id {0} on server {1}, map {2} at \
520 {3}".format(game.game_id,
521 server_id, map_id, start_dt))
526 def get_or_create_player(session=None, hashkey=None, nick=None):
528 Finds a player by hashkey or creates a new one (along with a
529 corresponding hashkey entry. Parameters:
531 session - SQLAlchemy database session factory
532 hashkey - hashkey of the player to be found or created
533 nick - nick of the player (in case of a first time create)
536 if re.search('^bot#\d+', hashkey):
537 player = session.query(Player).filter_by(player_id=1).one()
538 # if we have an untracked player
539 elif re.search('^player#\d+$', hashkey):
540 player = session.query(Player).filter_by(player_id=2).one()
541 # else it is a tracked player
543 # see if the player is already in the database
544 # if not, create one and the hashkey along with it
546 hk = session.query(Hashkey).filter_by(
547 hashkey=hashkey).one()
548 player = session.query(Player).filter_by(
549 player_id=hk.player_id).one()
550 log.debug("Found existing player {0} with hashkey {1}".format(
551 player.player_id, hashkey))
557 # if nick is given to us, use it. If not, use "Anonymous Player"
558 # with a suffix added for uniqueness.
560 player.nick = nick[:128]
561 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
563 player.nick = "Anonymous Player #{0}".format(player.player_id)
564 player.stripped_nick = player.nick
566 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
568 log.debug("Created player {0} ({2}) with hashkey {1}".format(
569 player.player_id, hashkey, player.nick.encode('utf-8')))
574 def create_default_game_stat(session, game_type_cd):
575 """Creates a blanked-out pgstat record for the given game type"""
577 # this is what we have to do to get partitioned records in - grab the
578 # sequence value first, then insert using the explicit ID (vs autogenerate)
579 seq = Sequence('player_game_stats_player_game_stat_id_seq')
580 pgstat_id = session.execute(seq)
581 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
582 create_dt=datetime.datetime.utcnow())
584 if game_type_cd == 'as':
585 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
587 if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
588 pgstat.kills = pgstat.deaths = pgstat.suicides = 0
590 if game_type_cd == 'cq':
591 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
594 if game_type_cd == 'ctf':
595 pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
596 pgstat.returns = pgstat.carrier_frags = 0
598 if game_type_cd == 'cts':
601 if game_type_cd == 'dom':
602 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
605 if game_type_cd == 'ft':
606 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
608 if game_type_cd == 'ka':
609 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
610 pgstat.carrier_frags = 0
611 pgstat.time = datetime.timedelta(seconds=0)
613 if game_type_cd == 'kh':
614 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
615 pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
616 pgstat.carrier_frags = 0
618 if game_type_cd == 'lms':
619 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
621 if game_type_cd == 'nb':
622 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
625 if game_type_cd == 'rc':
626 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
631 def create_game_stat(session, game_meta, game, server, gmap, player, events):
632 """Game stats handler for all game types"""
634 game_type_cd = game.game_type_cd
636 pgstat = create_default_game_stat(session, game_type_cd)
638 # these fields should be on every pgstat record
639 pgstat.game_id = game.game_id
640 pgstat.player_id = player.player_id
641 pgstat.nick = events.get('n', 'Anonymous Player')[:128]
642 pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
643 pgstat.score = int(round(float(events.get('scoreboard-score', 0))))
644 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
645 pgstat.rank = int(events.get('rank', None))
646 pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
648 if pgstat.nick != player.nick \
649 and player.player_id > 2 \
650 and pgstat.nick != 'Anonymous Player':
651 register_new_nick(session, player, pgstat.nick)
655 # gametype-specific stuff is handled here. if passed to us, we store it
656 for (key,value) in events.items():
657 if key == 'wins': wins = True
658 if key == 't': pgstat.team = int(value)
660 if key == 'scoreboard-drops': pgstat.drops = int(value)
661 if key == 'scoreboard-returns': pgstat.returns = int(value)
662 if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
663 if key == 'scoreboard-pickups': pgstat.pickups = int(value)
664 if key == 'scoreboard-caps': pgstat.captures = int(value)
665 if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
666 if key == 'scoreboard-deaths': pgstat.deaths = int(value)
667 if key == 'scoreboard-kills': pgstat.kills = int(value)
668 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
669 if key == 'scoreboard-objectives': pgstat.collects = int(value)
670 if key == 'scoreboard-captured': pgstat.captures = int(value)
671 if key == 'scoreboard-released': pgstat.drops = int(value)
672 if key == 'scoreboard-fastest':
673 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
674 if key == 'scoreboard-takes': pgstat.pickups = int(value)
675 if key == 'scoreboard-ticks': pgstat.drops = int(value)
676 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
677 if key == 'scoreboard-bctime':
678 pgstat.time = datetime.timedelta(seconds=int(value))
679 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
680 if key == 'scoreboard-losses': pgstat.drops = int(value)
681 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
682 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
683 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
684 if key == 'scoreboard-lives': pgstat.lives = int(value)
685 if key == 'scoreboard-goals': pgstat.captures = int(value)
686 if key == 'scoreboard-faults': pgstat.drops = int(value)
687 if key == 'scoreboard-laps': pgstat.laps = int(value)
689 if key == 'avglatency': pgstat.avg_latency = float(value)
690 if key == 'scoreboard-captime':
691 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
692 if game.game_type_cd == 'ctf':
693 update_fastest_cap(session, player.player_id, game.game_id,
694 gmap.map_id, pgstat.fastest, game.mod)
696 # there is no "winning team" field, so we have to derive it
697 if wins and pgstat.team is not None and game.winner is None:
698 game.winner = pgstat.team
706 def create_anticheats(session, pgstat, game, player, events):
707 """Anticheats handler for all game types"""
711 # all anticheat events are prefixed by "anticheat"
712 for (key,value) in events.items():
713 if key.startswith("anticheat"):
715 ac = PlayerGameAnticheat(
721 anticheats.append(ac)
723 except Exception as e:
724 log.debug("Could not parse value for key %s. Ignoring." % key)
729 def create_default_team_stat(session, game_type_cd):
730 """Creates a blanked-out teamstat record for the given game type"""
732 # this is what we have to do to get partitioned records in - grab the
733 # sequence value first, then insert using the explicit ID (vs autogenerate)
734 seq = Sequence('team_game_stats_team_game_stat_id_seq')
735 teamstat_id = session.execute(seq)
736 teamstat = TeamGameStat(team_game_stat_id=teamstat_id,
737 create_dt=datetime.datetime.utcnow())
739 # all team game modes have a score, so we'll zero that out always
742 if game_type_cd in 'ca' 'ft' 'lms' 'ka':
745 if game_type_cd == 'ctf':
751 def create_team_stat(session, game, events):
752 """Team stats handler for all game types"""
755 teamstat = create_default_team_stat(session, game.game_type_cd)
756 teamstat.game_id = game.game_id
758 # we should have a team ID if we have a 'Q' event
759 if re.match(r'^team#\d+$', events.get('Q', '')):
760 team = int(events.get('Q').replace('team#', ''))
763 # gametype-specific stuff is handled here. if passed to us, we store it
764 for (key,value) in events.items():
765 if key == 'scoreboard-score': teamstat.score = int(round(float(value)))
766 if key == 'scoreboard-caps': teamstat.caps = int(value)
767 if key == 'scoreboard-goals': teamstat.caps = int(value)
768 if key == 'scoreboard-rounds': teamstat.rounds = int(value)
770 session.add(teamstat)
771 except Exception as e:
777 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
778 """Weapon stats handler for all game types"""
781 # Version 1 of stats submissions doubled the data sent.
782 # To counteract this we divide the data by 2 only for
783 # POSTs coming from version 1.
785 version = int(game_meta['V'])
788 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
794 for (key,value) in events.items():
795 matched = re.search("acc-(.*?)-cnt-fired", key)
797 weapon_cd = matched.group(1)
799 # Weapon names changed for 0.8. We'll convert the old
800 # ones to use the new scheme as well.
801 mapped_weapon_cd = weapon_map.get(weapon_cd, weapon_cd)
803 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
804 pwstat_id = session.execute(seq)
805 pwstat = PlayerWeaponStat()
806 pwstat.player_weapon_stats_id = pwstat_id
807 pwstat.player_id = player.player_id
808 pwstat.game_id = game.game_id
809 pwstat.player_game_stat_id = pgstat.player_game_stat_id
810 pwstat.weapon_cd = mapped_weapon_cd
813 pwstat.nick = events['n']
815 pwstat.nick = events['P']
817 if 'acc-' + weapon_cd + '-cnt-fired' in events:
818 pwstat.fired = int(round(float(
819 events['acc-' + weapon_cd + '-cnt-fired'])))
820 if 'acc-' + weapon_cd + '-fired' in events:
821 pwstat.max = int(round(float(
822 events['acc-' + weapon_cd + '-fired'])))
823 if 'acc-' + weapon_cd + '-cnt-hit' in events:
824 pwstat.hit = int(round(float(
825 events['acc-' + weapon_cd + '-cnt-hit'])))
826 if 'acc-' + weapon_cd + '-hit' in events:
827 pwstat.actual = int(round(float(
828 events['acc-' + weapon_cd + '-hit'])))
829 if 'acc-' + weapon_cd + '-frags' in events:
830 pwstat.frags = int(round(float(
831 events['acc-' + weapon_cd + '-frags'])))
834 pwstat.fired = pwstat.fired/2
835 pwstat.max = pwstat.max/2
836 pwstat.hit = pwstat.hit/2
837 pwstat.actual = pwstat.actual/2
838 pwstat.frags = pwstat.frags/2
841 pwstats.append(pwstat)
846 def get_ranks(session, player_ids, game_type_cd):
848 Gets the rank entries for all players in the given list, returning a dict
849 of player_id -> PlayerRank instance. The rank entry corresponds to the
850 game type of the parameter passed in as well.
853 for pr in session.query(PlayerRank).\
854 filter(PlayerRank.player_id.in_(player_ids)).\
855 filter(PlayerRank.game_type_cd == game_type_cd).\
857 ranks[pr.player_id] = pr
862 def submit_stats(request):
864 Entry handler for POST stats submissions.
867 # placeholder for the actual session
870 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
871 "----- END REQUEST BODY -----\n\n")
873 (idfp, status) = verify_request(request)
874 (game_meta, raw_players, raw_teams) = parse_stats_submission(request.body)
875 revision = game_meta.get('R', 'unknown')
876 duration = game_meta.get('D', None)
878 # only players present at the end of the match are eligible for stats
879 raw_players = filter(played_in_game, raw_players)
881 do_precondition_checks(request, game_meta, raw_players)
883 # the "duel" gametype is fake
884 if len(raw_players) == 2 \
885 and num_real_players(raw_players) == 2 \
886 and game_meta['G'] == 'dm':
887 game_meta['G'] = 'duel'
889 #----------------------------------------------------------------------
890 # Actual setup (inserts/updates) below here
891 #----------------------------------------------------------------------
892 session = DBSession()
894 game_type_cd = game_meta['G']
896 # All game types create Game, Server, Map, and Player records
898 server = get_or_create_server(
901 name = game_meta['S'],
903 ip_addr = get_remote_addr(request),
904 port = game_meta.get('U', None),
905 impure_cvars = game_meta.get('C', 0))
907 gmap = get_or_create_map(
909 name = game_meta['M'])
913 start_dt = datetime.datetime.utcnow(),
914 server_id = server.server_id,
915 game_type_cd = game_type_cd,
916 map_id = gmap.map_id,
917 match_id = game_meta['I'],
919 mod = game_meta.get('O', None))
921 # keep track of the players we've seen
925 for events in raw_players:
926 player = get_or_create_player(
928 hashkey = events['P'],
929 nick = events.get('n', None))
931 pgstat = create_game_stat(session, game_meta, game, server,
932 gmap, player, events)
933 pgstats.append(pgstat)
935 if player.player_id > 1:
936 anticheats = create_anticheats(session, pgstat, game, player, events)
938 if player.player_id > 2:
939 player_ids.append(player.player_id)
940 hashkeys[player.player_id] = events['P']
942 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
943 pwstats = create_weapon_stats(session, game_meta, game, player,
946 # store them on games for easy access
947 game.players = player_ids
949 for events in raw_teams:
951 teamstat = create_team_stat(session, game, events)
952 except Exception as e:
955 if server.elo_ind and gametype_elo_eligible(game_type_cd):
956 ep = EloProcessor(session, game, pgstats)
960 log.debug('Success! Stats recorded.')
962 # ranks are fetched after we've done the "real" processing
963 ranks = get_ranks(session, player_ids, game_type_cd)
965 # plain text response
966 request.response.content_type = 'text/plain'
969 "now" : calendar.timegm(datetime.datetime.utcnow().timetuple()),
973 "player_ids" : player_ids,
974 "hashkeys" : hashkeys,
979 except Exception as e: