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 = (
163 if gametype in supported_game_types:
168 # some game types were buggy before revisions, thus this additional filter
169 if gametype == 'ca' and version <= 5:
175 def do_precondition_checks(request, game_meta, raw_players):
176 """Precondition checks for ALL gametypes.
177 These do not require a database connection."""
178 if not has_required_metadata(game_meta):
179 msg = "Missing required game metadata"
181 raise pyramid.httpexceptions.HTTPUnprocessableEntity(
183 content_type="text/plain"
187 version = int(game_meta['V'])
189 msg = "Invalid or incorrect game metadata provided"
191 raise pyramid.httpexceptions.HTTPUnprocessableEntity(
193 content_type="text/plain"
196 if not is_supported_gametype(game_meta['G'], version):
197 msg = "Unsupported game type ({})".format(game_meta['G'])
199 raise pyramid.httpexceptions.HTTPOk(
201 content_type="text/plain"
204 if not has_minimum_real_players(request.registry.settings, raw_players):
205 msg = "Not enough real players"
207 raise pyramid.httpexceptions.HTTPOk(
209 content_type="text/plain"
212 if is_blank_game(game_meta['G'], raw_players):
215 raise pyramid.httpexceptions.HTTPOk(
217 content_type="text/plain"
221 def is_real_player(events):
223 Determines if a given set of events correspond with a non-bot
225 if not events['P'].startswith('bot'):
231 def played_in_game(events):
233 Determines if a given set of player events correspond with a player who
234 played in the game (matches 1 and scoreboardvalid 1)
236 if 'matches' in events and 'scoreboardvalid' in events:
242 def num_real_players(player_events):
244 Returns the number of real players (those who played
245 and are on the scoreboard).
249 for events in player_events:
250 if is_real_player(events) and played_in_game(events):
256 def has_minimum_real_players(settings, player_events):
258 Determines if the collection of player events has enough "real" players
259 to store in the database. The minimum setting comes from the config file
260 under the setting xonstat.minimum_real_players.
262 flg_has_min_real_players = True
265 minimum_required_players = int(
266 settings['xonstat.minimum_required_players'])
268 minimum_required_players = 2
270 real_players = num_real_players(player_events)
272 if real_players < minimum_required_players:
273 flg_has_min_real_players = False
275 return flg_has_min_real_players
278 def has_required_metadata(metadata):
280 Determines if a give set of metadata has enough data to create a game,
281 server, and map with.
283 flg_has_req_metadata = True
285 if 'G' not in metadata or\
286 'M' not in metadata or\
287 'I' not in metadata or\
289 flg_has_req_metadata = False
291 return flg_has_req_metadata
294 def should_do_weapon_stats(game_type_cd):
295 """True of the game type should record weapon stats. False otherwise."""
296 if game_type_cd in 'cts':
302 def gametype_elo_eligible(game_type_cd):
303 """True of the game type should process Elos. False otherwise."""
304 elo_game_types = ('duel', 'dm', 'ca', 'ctf', 'tdm', 'ka', 'ft')
306 if game_type_cd in elo_game_types:
312 def register_new_nick(session, player, new_nick):
314 Change the player record's nick to the newly found nick. Store the old
315 nick in the player_nicks table for that player.
317 session - SQLAlchemy database session factory
318 player - player record whose nick is changing
319 new_nick - the new nickname
321 # see if that nick already exists
322 stripped_nick = strip_colors(qfont_decode(player.nick))
324 player_nick = session.query(PlayerNick).filter_by(
325 player_id=player.player_id, stripped_nick=stripped_nick).one()
326 except NoResultFound, e:
327 # player_id/stripped_nick not found, create one
328 # but we don't store "Anonymous Player #N"
329 if not re.search('^Anonymous Player #\d+$', player.nick):
330 player_nick = PlayerNick()
331 player_nick.player_id = player.player_id
332 player_nick.stripped_nick = stripped_nick
333 player_nick.nick = player.nick
334 session.add(player_nick)
336 # We change to the new nick regardless
337 player.nick = new_nick
338 player.stripped_nick = strip_colors(qfont_decode(new_nick))
342 def update_fastest_cap(session, player_id, game_id, map_id, captime, mod):
344 Check the fastest cap time for the player and map. If there isn't
345 one, insert one. If there is, check if the passed time is faster.
348 # we don't record fastest cap times for bots or anonymous players
352 # see if a cap entry exists already
353 # then check to see if the new captime is faster
355 cur_fastest_cap = session.query(PlayerCaptime).filter_by(
356 player_id=player_id, map_id=map_id, mod=mod).one()
358 # current captime is faster, so update
359 if captime < cur_fastest_cap.fastest_cap:
360 cur_fastest_cap.fastest_cap = captime
361 cur_fastest_cap.game_id = game_id
362 cur_fastest_cap.create_dt = datetime.datetime.utcnow()
363 session.add(cur_fastest_cap)
365 except NoResultFound, e:
366 # none exists, so insert
367 cur_fastest_cap = PlayerCaptime(player_id, game_id, map_id, captime,
369 session.add(cur_fastest_cap)
373 def update_server(server, name, hashkey, ip_addr, port, revision, impure_cvars):
375 Updates the server in the given DB session, if needed.
377 :param server: The found server instance.
378 :param name: The incoming server name.
379 :param hashkey: The incoming server hashkey.
380 :param ip_addr: The incoming server IP address.
381 :param port: The incoming server port.
382 :param revision: The incoming server revision.
383 :param impure_cvars: The incoming number of impure server cvars.
386 # ensure the two int attributes are actually ints
393 impure_cvars = int(impure_cvars)
398 if name and server.name != name:
401 if hashkey and server.hashkey != hashkey:
402 server.hashkey = hashkey
404 if ip_addr and server.ip_addr != ip_addr:
405 server.ip_addr = ip_addr
407 if port and server.port != port:
410 if revision and server.revision != revision:
411 server.revision = revision
413 if impure_cvars and server.impure_cvars != impure_cvars:
414 server.impure_cvars = impure_cvars
415 server.pure_ind = True if impure_cvars == 0 else False
421 def get_or_create_server(session, name, hashkey, ip_addr, revision, port, impure_cvars):
423 Find a server by name or create one if not found. Parameters:
425 session - SQLAlchemy database session factory
426 name - server name of the server to be found or created
427 hashkey - server hashkey
428 ip_addr - the IP address of the server
429 revision - the xonotic revision number
430 port - the port number of the server
431 impure_cvars - the number of impure cvar changes
433 servers_q = DBSession.query(Server).filter(Server.active_ind)
436 # if the hashkey is provided, we'll use that
437 servers_q = servers_q.filter((Server.name == name) or (Server.hashkey == hashkey))
439 # otherwise, it is just by name
440 servers_q = servers_q.filter(Server.name == name)
442 # order by the hashkey, which means any hashkey match will appear first if there are multiple
443 servers = servers_q.order_by(Server.hashkey, Server.create_dt).all()
445 if len(servers) == 0:
446 server = Server(name=name, hashkey=hashkey)
449 log.debug("Created server {} with hashkey {}.".format(server.server_id, server.hashkey))
452 if len(servers) == 1:
453 log.info("Found existing server {}.".format(server.server_id))
455 elif len(servers) > 1:
456 server_id_list = ", ".join(["{}".format(s.server_id) for s in servers])
457 log.warn("Multiple servers found ({})! Using the first one ({})."
458 .format(server_id_list, server.server_id))
460 if update_server(server, name, hashkey, ip_addr, port, revision, impure_cvars):
466 def get_or_create_map(session=None, name=None):
468 Find a map by name or create one if not found. Parameters:
470 session - SQLAlchemy database session factory
471 name - map name of the map to be found or created
474 # find one by the name, if it exists
475 gmap = session.query(Map).filter_by(name=name).one()
476 log.debug("Found map id {0}: {1}".format(gmap.map_id,
478 except NoResultFound, e:
479 gmap = Map(name=name)
482 log.debug("Created map id {0}: {1}".format(gmap.map_id,
484 except MultipleResultsFound, e:
485 # multiple found, so use the first one but warn
487 gmaps = session.query(Map).filter_by(name=name).order_by(
490 log.debug("Found map id {0}: {1} but found \
491 multiple".format(gmap.map_id, gmap.name))
496 def create_game(session, start_dt, game_type_cd, server_id, map_id,
497 match_id, duration, mod, winner=None):
499 Creates a game. Parameters:
501 session - SQLAlchemy database session factory
502 start_dt - when the game started (datetime object)
503 game_type_cd - the game type of the game being played
504 server_id - server identifier of the server hosting the game
505 map_id - map on which the game was played
506 winner - the team id of the team that won
507 duration - how long the game lasted
508 mod - mods in use during the game
510 seq = Sequence('games_game_id_seq')
511 game_id = session.execute(seq)
512 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
513 server_id=server_id, map_id=map_id, winner=winner)
514 game.match_id = match_id
517 # There is some drift between start_dt (provided by app) and create_dt
518 # (default in the database), so we'll make them the same until this is
520 game.create_dt = start_dt
523 game.duration = datetime.timedelta(seconds=int(round(float(duration))))
528 session.query(Game).filter(Game.server_id==server_id).\
529 filter(Game.match_id==match_id).one()
531 log.debug("Error: game with same server and match_id found! Ignoring.")
533 # if a game under the same server and match_id found,
534 # this is a duplicate game and can be ignored
535 raise pyramid.httpexceptions.HTTPOk('OK')
536 except NoResultFound, e:
537 # server_id/match_id combination not found. game is ok to insert
540 log.debug("Created game id {0} on server {1}, map {2} at \
541 {3}".format(game.game_id,
542 server_id, map_id, start_dt))
547 def get_or_create_player(session=None, hashkey=None, nick=None):
549 Finds a player by hashkey or creates a new one (along with a
550 corresponding hashkey entry. Parameters:
552 session - SQLAlchemy database session factory
553 hashkey - hashkey of the player to be found or created
554 nick - nick of the player (in case of a first time create)
557 if re.search('^bot#\d+', hashkey):
558 player = session.query(Player).filter_by(player_id=1).one()
559 # if we have an untracked player
560 elif re.search('^player#\d+$', hashkey):
561 player = session.query(Player).filter_by(player_id=2).one()
562 # else it is a tracked player
564 # see if the player is already in the database
565 # if not, create one and the hashkey along with it
567 hk = session.query(Hashkey).filter_by(
568 hashkey=hashkey).one()
569 player = session.query(Player).filter_by(
570 player_id=hk.player_id).one()
571 log.debug("Found existing player {0} with hashkey {1}".format(
572 player.player_id, hashkey))
578 # if nick is given to us, use it. If not, use "Anonymous Player"
579 # with a suffix added for uniqueness.
581 player.nick = nick[:128]
582 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
584 player.nick = "Anonymous Player #{0}".format(player.player_id)
585 player.stripped_nick = player.nick
587 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
589 log.debug("Created player {0} ({2}) with hashkey {1}".format(
590 player.player_id, hashkey, player.nick.encode('utf-8')))
595 def create_default_game_stat(session, game_type_cd):
596 """Creates a blanked-out pgstat record for the given game type"""
598 # this is what we have to do to get partitioned records in - grab the
599 # sequence value first, then insert using the explicit ID (vs autogenerate)
600 seq = Sequence('player_game_stats_player_game_stat_id_seq')
601 pgstat_id = session.execute(seq)
602 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
603 create_dt=datetime.datetime.utcnow())
605 if game_type_cd == 'as':
606 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
608 if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
609 pgstat.kills = pgstat.deaths = pgstat.suicides = 0
611 if game_type_cd == 'cq':
612 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
615 if game_type_cd == 'ctf':
616 pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
617 pgstat.returns = pgstat.carrier_frags = 0
619 if game_type_cd == 'cts':
622 if game_type_cd == 'dom':
623 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
626 if game_type_cd == 'ft':
627 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
629 if game_type_cd == 'ka':
630 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
631 pgstat.carrier_frags = 0
632 pgstat.time = datetime.timedelta(seconds=0)
634 if game_type_cd == 'kh':
635 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
636 pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
637 pgstat.carrier_frags = 0
639 if game_type_cd == 'lms':
640 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
642 if game_type_cd == 'nb':
643 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
646 if game_type_cd == 'rc':
647 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
652 def create_game_stat(session, game_meta, game, server, gmap, player, events):
653 """Game stats handler for all game types"""
655 game_type_cd = game.game_type_cd
657 pgstat = create_default_game_stat(session, game_type_cd)
659 # these fields should be on every pgstat record
660 pgstat.game_id = game.game_id
661 pgstat.player_id = player.player_id
662 pgstat.nick = events.get('n', 'Anonymous Player')[:128]
663 pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
664 pgstat.score = int(round(float(events.get('scoreboard-score', 0))))
665 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
666 pgstat.rank = int(events.get('rank', None))
667 pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
669 if pgstat.nick != player.nick \
670 and player.player_id > 2 \
671 and pgstat.nick != 'Anonymous Player':
672 register_new_nick(session, player, pgstat.nick)
676 # gametype-specific stuff is handled here. if passed to us, we store it
677 for (key,value) in events.items():
678 if key == 'wins': wins = True
679 if key == 't': pgstat.team = int(value)
681 if key == 'scoreboard-drops': pgstat.drops = int(value)
682 if key == 'scoreboard-returns': pgstat.returns = int(value)
683 if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
684 if key == 'scoreboard-pickups': pgstat.pickups = int(value)
685 if key == 'scoreboard-caps': pgstat.captures = int(value)
686 if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
687 if key == 'scoreboard-deaths': pgstat.deaths = int(value)
688 if key == 'scoreboard-kills': pgstat.kills = int(value)
689 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
690 if key == 'scoreboard-objectives': pgstat.collects = int(value)
691 if key == 'scoreboard-captured': pgstat.captures = int(value)
692 if key == 'scoreboard-released': pgstat.drops = int(value)
693 if key == 'scoreboard-fastest':
694 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
695 if key == 'scoreboard-takes': pgstat.pickups = int(value)
696 if key == 'scoreboard-ticks': pgstat.drops = int(value)
697 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
698 if key == 'scoreboard-bctime':
699 pgstat.time = datetime.timedelta(seconds=int(value))
700 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
701 if key == 'scoreboard-losses': pgstat.drops = int(value)
702 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
703 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
704 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
705 if key == 'scoreboard-lives': pgstat.lives = int(value)
706 if key == 'scoreboard-goals': pgstat.captures = int(value)
707 if key == 'scoreboard-faults': pgstat.drops = int(value)
708 if key == 'scoreboard-laps': pgstat.laps = int(value)
710 if key == 'avglatency': pgstat.avg_latency = float(value)
711 if key == 'scoreboard-captime':
712 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
713 if game.game_type_cd == 'ctf':
714 update_fastest_cap(session, player.player_id, game.game_id,
715 gmap.map_id, pgstat.fastest, game.mod)
717 # there is no "winning team" field, so we have to derive it
718 if wins and pgstat.team is not None and game.winner is None:
719 game.winner = pgstat.team
727 def create_anticheats(session, pgstat, game, player, events):
728 """Anticheats handler for all game types"""
732 # all anticheat events are prefixed by "anticheat"
733 for (key,value) in events.items():
734 if key.startswith("anticheat"):
736 ac = PlayerGameAnticheat(
742 anticheats.append(ac)
744 except Exception as e:
745 log.debug("Could not parse value for key %s. Ignoring." % key)
750 def create_default_team_stat(session, game_type_cd):
751 """Creates a blanked-out teamstat record for the given game type"""
753 # this is what we have to do to get partitioned records in - grab the
754 # sequence value first, then insert using the explicit ID (vs autogenerate)
755 seq = Sequence('team_game_stats_team_game_stat_id_seq')
756 teamstat_id = session.execute(seq)
757 teamstat = TeamGameStat(team_game_stat_id=teamstat_id,
758 create_dt=datetime.datetime.utcnow())
760 # all team game modes have a score, so we'll zero that out always
763 if game_type_cd in 'ca' 'ft' 'lms' 'ka':
766 if game_type_cd == 'ctf':
772 def create_team_stat(session, game, events):
773 """Team stats handler for all game types"""
776 teamstat = create_default_team_stat(session, game.game_type_cd)
777 teamstat.game_id = game.game_id
779 # we should have a team ID if we have a 'Q' event
780 if re.match(r'^team#\d+$', events.get('Q', '')):
781 team = int(events.get('Q').replace('team#', ''))
784 # gametype-specific stuff is handled here. if passed to us, we store it
785 for (key,value) in events.items():
786 if key == 'scoreboard-score': teamstat.score = int(round(float(value)))
787 if key == 'scoreboard-caps': teamstat.caps = int(value)
788 if key == 'scoreboard-goals': teamstat.caps = int(value)
789 if key == 'scoreboard-rounds': teamstat.rounds = int(value)
791 session.add(teamstat)
792 except Exception as e:
798 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
799 """Weapon stats handler for all game types"""
802 # Version 1 of stats submissions doubled the data sent.
803 # To counteract this we divide the data by 2 only for
804 # POSTs coming from version 1.
806 version = int(game_meta['V'])
809 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
815 for (key,value) in events.items():
816 matched = re.search("acc-(.*?)-cnt-fired", key)
818 weapon_cd = matched.group(1)
820 # Weapon names changed for 0.8. We'll convert the old
821 # ones to use the new scheme as well.
822 mapped_weapon_cd = weapon_map.get(weapon_cd, weapon_cd)
824 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
825 pwstat_id = session.execute(seq)
826 pwstat = PlayerWeaponStat()
827 pwstat.player_weapon_stats_id = pwstat_id
828 pwstat.player_id = player.player_id
829 pwstat.game_id = game.game_id
830 pwstat.player_game_stat_id = pgstat.player_game_stat_id
831 pwstat.weapon_cd = mapped_weapon_cd
834 pwstat.nick = events['n']
836 pwstat.nick = events['P']
838 if 'acc-' + weapon_cd + '-cnt-fired' in events:
839 pwstat.fired = int(round(float(
840 events['acc-' + weapon_cd + '-cnt-fired'])))
841 if 'acc-' + weapon_cd + '-fired' in events:
842 pwstat.max = int(round(float(
843 events['acc-' + weapon_cd + '-fired'])))
844 if 'acc-' + weapon_cd + '-cnt-hit' in events:
845 pwstat.hit = int(round(float(
846 events['acc-' + weapon_cd + '-cnt-hit'])))
847 if 'acc-' + weapon_cd + '-hit' in events:
848 pwstat.actual = int(round(float(
849 events['acc-' + weapon_cd + '-hit'])))
850 if 'acc-' + weapon_cd + '-frags' in events:
851 pwstat.frags = int(round(float(
852 events['acc-' + weapon_cd + '-frags'])))
855 pwstat.fired = pwstat.fired/2
856 pwstat.max = pwstat.max/2
857 pwstat.hit = pwstat.hit/2
858 pwstat.actual = pwstat.actual/2
859 pwstat.frags = pwstat.frags/2
862 pwstats.append(pwstat)
867 def get_ranks(session, player_ids, game_type_cd):
869 Gets the rank entries for all players in the given list, returning a dict
870 of player_id -> PlayerRank instance. The rank entry corresponds to the
871 game type of the parameter passed in as well.
874 for pr in session.query(PlayerRank).\
875 filter(PlayerRank.player_id.in_(player_ids)).\
876 filter(PlayerRank.game_type_cd == game_type_cd).\
878 ranks[pr.player_id] = pr
883 def submit_stats(request):
885 Entry handler for POST stats submissions.
888 # placeholder for the actual session
891 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
892 "----- END REQUEST BODY -----\n\n")
894 (idfp, status) = verify_request(request)
895 (game_meta, raw_players, raw_teams) = parse_stats_submission(request.body)
896 revision = game_meta.get('R', 'unknown')
897 duration = game_meta.get('D', None)
899 # only players present at the end of the match are eligible for stats
900 raw_players = filter(played_in_game, raw_players)
902 do_precondition_checks(request, game_meta, raw_players)
904 # the "duel" gametype is fake
905 if len(raw_players) == 2 \
906 and num_real_players(raw_players) == 2 \
907 and game_meta['G'] == 'dm':
908 game_meta['G'] = 'duel'
910 #----------------------------------------------------------------------
911 # Actual setup (inserts/updates) below here
912 #----------------------------------------------------------------------
913 session = DBSession()
915 game_type_cd = game_meta['G']
917 # All game types create Game, Server, Map, and Player records
919 server = get_or_create_server(
922 name = game_meta['S'],
924 ip_addr = get_remote_addr(request),
925 port = game_meta.get('U', None),
926 impure_cvars = game_meta.get('C', 0))
928 gmap = get_or_create_map(
930 name = game_meta['M'])
934 start_dt = datetime.datetime.utcnow(),
935 server_id = server.server_id,
936 game_type_cd = game_type_cd,
937 map_id = gmap.map_id,
938 match_id = game_meta['I'],
940 mod = game_meta.get('O', None))
942 # keep track of the players we've seen
946 for events in raw_players:
947 player = get_or_create_player(
949 hashkey = events['P'],
950 nick = events.get('n', None))
952 pgstat = create_game_stat(session, game_meta, game, server,
953 gmap, player, events)
954 pgstats.append(pgstat)
956 if player.player_id > 1:
957 anticheats = create_anticheats(session, pgstat, game, player, events)
959 if player.player_id > 2:
960 player_ids.append(player.player_id)
961 hashkeys[player.player_id] = events['P']
963 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
964 pwstats = create_weapon_stats(session, game_meta, game, player,
967 # store them on games for easy access
968 game.players = player_ids
970 for events in raw_teams:
972 teamstat = create_team_stat(session, game, events)
973 except Exception as e:
976 if server.elo_ind and gametype_elo_eligible(game_type_cd):
977 ep = EloProcessor(session, game, pgstats)
981 log.debug('Success! Stats recorded.')
983 # ranks are fetched after we've done the "real" processing
984 ranks = get_ranks(session, player_ids, game_type_cd)
986 # plain text response
987 request.response.content_type = 'text/plain'
990 "now" : calendar.timegm(datetime.datetime.utcnow().timetuple()),
994 "player_ids" : player_ids,
995 "hashkeys" : hashkeys,
1000 except Exception as e: