4 import pyramid.httpexceptions
7 import sqlalchemy.sql.expression as expr
8 from pyramid.response import Response
9 from sqlalchemy import Sequence
10 from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
11 from xonstat.elo import EloProcessor
12 from xonstat.models import *
13 from xonstat.util import strip_colors, qfont_decode, verify_request, weapon_map
16 log = logging.getLogger(__name__)
19 def parse_stats_submission(body):
21 Parses the POST request body for a stats submission
23 # storage vars for the request body
29 # we're not in either stanza to start
32 for line in body.split('\n'):
34 (key, value) = line.strip().split(' ', 1)
36 # Server (S) and Nick (n) fields can have international characters.
38 value = unicode(value, 'utf-8')
40 if key not in 'P' 'Q' 'n' 'e' 't' 'i':
41 game_meta[key] = value
43 if key == 'Q' or key == 'P':
44 #log.debug('Found a {0}'.format(key))
45 #log.debug('in_Q: {0}'.format(in_Q))
46 #log.debug('in_P: {0}'.format(in_P))
47 #log.debug('events: {0}'.format(events))
49 # check where we were before and append events accordingly
50 if in_Q and len(events) > 0:
51 #log.debug('creating a team (Q) entry')
54 elif in_P and len(events) > 0:
55 #log.debug('creating a player (P) entry')
56 players.append(events)
60 #log.debug('key == P')
64 #log.debug('key == Q')
71 (subkey, subvalue) = value.split(' ', 1)
72 events[subkey] = subvalue
78 # no key/value pair - move on to the next line
81 # add the last entity we were working on
82 if in_P and len(events) > 0:
83 players.append(events)
84 elif in_Q and len(events) > 0:
87 return (game_meta, players, teams)
90 def is_blank_game(gametype, players):
91 """Determine if this is a blank game or not. A blank game is either:
93 1) a match that ended in the warmup stage, where accuracy events are not
94 present (for non-CTS games)
96 2) a match in which no player made a positive or negative score AND was
99 ... or for CTS, which doesn't record accuracy events
101 1) a match in which no player made a fastest lap AND was
104 ... or for NB, in which not all maps have weapons
106 1) a match in which no player made a positive or negative score
108 r = re.compile(r'acc-.*-cnt-fired')
109 flg_nonzero_score = False
110 flg_acc_events = False
111 flg_fastest_lap = False
113 for events in players:
114 if is_real_player(events) and played_in_game(events):
115 for (key,value) in events.items():
116 if key == 'scoreboard-score' and value != 0:
117 flg_nonzero_score = True
119 flg_acc_events = True
120 if key == 'scoreboard-fastest':
121 flg_fastest_lap = True
123 if gametype == 'cts':
124 return not flg_fastest_lap
125 elif gametype == 'nb':
126 return not flg_nonzero_score
128 return not (flg_nonzero_score and flg_acc_events)
131 def get_remote_addr(request):
132 """Get the Xonotic server's IP address"""
133 if 'X-Forwarded-For' in request.headers:
134 return request.headers['X-Forwarded-For']
136 return request.remote_addr
139 def is_supported_gametype(gametype, version):
140 """Whether a gametype is supported or not"""
143 # if the type can be supported, but with version constraints, uncomment
144 # here and add the restriction for a specific version below
145 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 log.debug("ERROR: Required game meta missing")
180 raise pyramid.httpexceptions.HTTPUnprocessableEntity("Missing game meta")
183 version = int(game_meta['V'])
185 log.debug("ERROR: Required game meta invalid")
186 raise pyramid.httpexceptions.HTTPUnprocessableEntity("Invalid game meta")
188 if not is_supported_gametype(game_meta['G'], version):
189 log.debug("ERROR: Unsupported gametype")
190 raise pyramid.httpexceptions.HTTPOk("OK")
192 if not has_minimum_real_players(request.registry.settings, raw_players):
193 log.debug("ERROR: Not enough real players")
194 raise pyramid.httpexceptions.HTTPOk("OK")
196 if is_blank_game(game_meta['G'], raw_players):
197 log.debug("ERROR: Blank game")
198 raise pyramid.httpexceptions.HTTPOk("OK")
201 def is_real_player(events):
203 Determines if a given set of events correspond with a non-bot
205 if not events['P'].startswith('bot'):
211 def played_in_game(events):
213 Determines if a given set of player events correspond with a player who
214 played in the game (matches 1 and scoreboardvalid 1)
216 if 'matches' in events and 'scoreboardvalid' in events:
222 def num_real_players(player_events):
224 Returns the number of real players (those who played
225 and are on the scoreboard).
229 for events in player_events:
230 if is_real_player(events) and played_in_game(events):
236 def has_minimum_real_players(settings, player_events):
238 Determines if the collection of player events has enough "real" players
239 to store in the database. The minimum setting comes from the config file
240 under the setting xonstat.minimum_real_players.
242 flg_has_min_real_players = True
245 minimum_required_players = int(
246 settings['xonstat.minimum_required_players'])
248 minimum_required_players = 2
250 real_players = num_real_players(player_events)
252 if real_players < minimum_required_players:
253 flg_has_min_real_players = False
255 return flg_has_min_real_players
258 def has_required_metadata(metadata):
260 Determines if a give set of metadata has enough data to create a game,
261 server, and map with.
263 flg_has_req_metadata = True
265 if 'G' not in metadata or\
266 'M' not in metadata or\
267 'I' not in metadata or\
269 flg_has_req_metadata = False
271 return flg_has_req_metadata
274 def should_do_weapon_stats(game_type_cd):
275 """True of the game type should record weapon stats. False otherwise."""
276 if game_type_cd in 'cts':
282 def should_do_elos(game_type_cd):
283 """True of the game type should process Elos. False otherwise."""
284 elo_game_types = ('duel', 'dm', 'ca', 'ctf', 'tdm', 'ka', 'ft')
286 if game_type_cd in elo_game_types:
292 def register_new_nick(session, player, new_nick):
294 Change the player record's nick to the newly found nick. Store the old
295 nick in the player_nicks table for that player.
297 session - SQLAlchemy database session factory
298 player - player record whose nick is changing
299 new_nick - the new nickname
301 # see if that nick already exists
302 stripped_nick = strip_colors(qfont_decode(player.nick))
304 player_nick = session.query(PlayerNick).filter_by(
305 player_id=player.player_id, stripped_nick=stripped_nick).one()
306 except NoResultFound, e:
307 # player_id/stripped_nick not found, create one
308 # but we don't store "Anonymous Player #N"
309 if not re.search('^Anonymous Player #\d+$', player.nick):
310 player_nick = PlayerNick()
311 player_nick.player_id = player.player_id
312 player_nick.stripped_nick = stripped_nick
313 player_nick.nick = player.nick
314 session.add(player_nick)
316 # We change to the new nick regardless
317 player.nick = new_nick
318 player.stripped_nick = strip_colors(qfont_decode(new_nick))
322 def update_fastest_cap(session, player_id, game_id, map_id, captime, mod):
324 Check the fastest cap time for the player and map. If there isn't
325 one, insert one. If there is, check if the passed time is faster.
328 # we don't record fastest cap times for bots or anonymous players
332 # see if a cap entry exists already
333 # then check to see if the new captime is faster
335 cur_fastest_cap = session.query(PlayerCaptime).filter_by(
336 player_id=player_id, map_id=map_id, mod=mod).one()
338 # current captime is faster, so update
339 if captime < cur_fastest_cap.fastest_cap:
340 cur_fastest_cap.fastest_cap = captime
341 cur_fastest_cap.game_id = game_id
342 cur_fastest_cap.create_dt = datetime.datetime.utcnow()
343 session.add(cur_fastest_cap)
345 except NoResultFound, e:
346 # none exists, so insert
347 cur_fastest_cap = PlayerCaptime(player_id, game_id, map_id, captime,
349 session.add(cur_fastest_cap)
353 def get_or_create_server(session, name, hashkey, ip_addr, revision, port,
356 Find a server by name or create one if not found. Parameters:
358 session - SQLAlchemy database session factory
359 name - server name of the server to be found or created
360 hashkey - server hashkey
361 ip_addr - the IP address of the server
362 revision - the xonotic revision number
363 port - the port number of the server
364 impure_cvars - the number of impure cvar changes
374 impure_cvars = int(impure_cvars)
378 # finding by hashkey is preferred, but if not we will fall
379 # back to using name only, which can result in dupes
380 if hashkey is not None:
381 servers = session.query(Server).\
382 filter_by(hashkey=hashkey).\
383 order_by(expr.desc(Server.create_dt)).limit(1).all()
387 log.debug("Found existing server {0} by hashkey ({1})".format(
388 server.server_id, server.hashkey))
390 servers = session.query(Server).\
391 filter_by(name=name).\
392 order_by(expr.desc(Server.create_dt)).limit(1).all()
396 log.debug("Found existing server {0} by name".format(server.server_id))
398 # still haven't found a server by hashkey or name, so we need to create one
400 server = Server(name=name, hashkey=hashkey)
403 log.debug("Created server {0} with hashkey {1}".format(
404 server.server_id, server.hashkey))
406 # detect changed fields
407 if server.name != name:
411 if server.hashkey != hashkey:
412 server.hashkey = hashkey
415 if server.ip_addr != ip_addr:
416 server.ip_addr = ip_addr
419 if server.port != port:
423 if server.revision != revision:
424 server.revision = revision
427 if server.impure_cvars != impure_cvars:
428 server.impure_cvars = impure_cvars
430 server.pure_ind = False
432 server.pure_ind = True
438 def get_or_create_map(session=None, name=None):
440 Find a map by name or create one if not found. Parameters:
442 session - SQLAlchemy database session factory
443 name - map name of the map to be found or created
446 # find one by the name, if it exists
447 gmap = session.query(Map).filter_by(name=name).one()
448 log.debug("Found map id {0}: {1}".format(gmap.map_id,
450 except NoResultFound, e:
451 gmap = Map(name=name)
454 log.debug("Created map id {0}: {1}".format(gmap.map_id,
456 except MultipleResultsFound, e:
457 # multiple found, so use the first one but warn
459 gmaps = session.query(Map).filter_by(name=name).order_by(
462 log.debug("Found map id {0}: {1} but found \
463 multiple".format(gmap.map_id, gmap.name))
468 def create_game(session, start_dt, game_type_cd, server_id, map_id,
469 match_id, duration, mod, winner=None):
471 Creates a game. Parameters:
473 session - SQLAlchemy database session factory
474 start_dt - when the game started (datetime object)
475 game_type_cd - the game type of the game being played
476 server_id - server identifier of the server hosting the game
477 map_id - map on which the game was played
478 winner - the team id of the team that won
479 duration - how long the game lasted
480 mod - mods in use during the game
482 seq = Sequence('games_game_id_seq')
483 game_id = session.execute(seq)
484 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
485 server_id=server_id, map_id=map_id, winner=winner)
486 game.match_id = match_id
490 game.duration = datetime.timedelta(seconds=int(round(float(duration))))
495 session.query(Game).filter(Game.server_id==server_id).\
496 filter(Game.match_id==match_id).one()
498 log.debug("Error: game with same server and match_id found! Ignoring.")
500 # if a game under the same server and match_id found,
501 # this is a duplicate game and can be ignored
502 raise pyramid.httpexceptions.HTTPOk('OK')
503 except NoResultFound, e:
504 # server_id/match_id combination not found. game is ok to insert
507 log.debug("Created game id {0} on server {1}, map {2} at \
508 {3}".format(game.game_id,
509 server_id, map_id, start_dt))
514 def get_or_create_player(session=None, hashkey=None, nick=None):
516 Finds a player by hashkey or creates a new one (along with a
517 corresponding hashkey entry. Parameters:
519 session - SQLAlchemy database session factory
520 hashkey - hashkey of the player to be found or created
521 nick - nick of the player (in case of a first time create)
524 if re.search('^bot#\d+', hashkey):
525 player = session.query(Player).filter_by(player_id=1).one()
526 # if we have an untracked player
527 elif re.search('^player#\d+$', hashkey):
528 player = session.query(Player).filter_by(player_id=2).one()
529 # else it is a tracked player
531 # see if the player is already in the database
532 # if not, create one and the hashkey along with it
534 hk = session.query(Hashkey).filter_by(
535 hashkey=hashkey).one()
536 player = session.query(Player).filter_by(
537 player_id=hk.player_id).one()
538 log.debug("Found existing player {0} with hashkey {1}".format(
539 player.player_id, hashkey))
545 # if nick is given to us, use it. If not, use "Anonymous Player"
546 # with a suffix added for uniqueness.
548 player.nick = nick[:128]
549 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
551 player.nick = "Anonymous Player #{0}".format(player.player_id)
552 player.stripped_nick = player.nick
554 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
556 log.debug("Created player {0} ({2}) with hashkey {1}".format(
557 player.player_id, hashkey, player.nick.encode('utf-8')))
562 def create_default_game_stat(session, game_type_cd):
563 """Creates a blanked-out pgstat record for the given game type"""
565 # this is what we have to do to get partitioned records in - grab the
566 # sequence value first, then insert using the explicit ID (vs autogenerate)
567 seq = Sequence('player_game_stats_player_game_stat_id_seq')
568 pgstat_id = session.execute(seq)
569 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
570 create_dt=datetime.datetime.utcnow())
572 if game_type_cd == 'as':
573 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
575 if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
576 pgstat.kills = pgstat.deaths = pgstat.suicides = 0
578 if game_type_cd == 'cq':
579 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
582 if game_type_cd == 'ctf':
583 pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
584 pgstat.returns = pgstat.carrier_frags = 0
586 if game_type_cd == 'cts':
589 if game_type_cd == 'dom':
590 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
593 if game_type_cd == 'ft':
594 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
596 if game_type_cd == 'ka':
597 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
598 pgstat.carrier_frags = 0
599 pgstat.time = datetime.timedelta(seconds=0)
601 if game_type_cd == 'kh':
602 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
603 pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
604 pgstat.carrier_frags = 0
606 if game_type_cd == 'lms':
607 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
609 if game_type_cd == 'nb':
610 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
613 if game_type_cd == 'rc':
614 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
619 def create_game_stat(session, game_meta, game, server, gmap, player, events):
620 """Game stats handler for all game types"""
622 game_type_cd = game.game_type_cd
624 pgstat = create_default_game_stat(session, game_type_cd)
626 # these fields should be on every pgstat record
627 pgstat.game_id = game.game_id
628 pgstat.player_id = player.player_id
629 pgstat.nick = events.get('n', 'Anonymous Player')[:128]
630 pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
631 pgstat.score = int(round(float(events.get('scoreboard-score', 0))))
632 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
633 pgstat.rank = int(events.get('rank', None))
634 pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
636 if pgstat.nick != player.nick \
637 and player.player_id > 2 \
638 and pgstat.nick != 'Anonymous Player':
639 register_new_nick(session, player, pgstat.nick)
643 # gametype-specific stuff is handled here. if passed to us, we store it
644 for (key,value) in events.items():
645 if key == 'wins': wins = True
646 if key == 't': pgstat.team = int(value)
648 if key == 'scoreboard-drops': pgstat.drops = int(value)
649 if key == 'scoreboard-returns': pgstat.returns = int(value)
650 if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
651 if key == 'scoreboard-pickups': pgstat.pickups = int(value)
652 if key == 'scoreboard-caps': pgstat.captures = int(value)
653 if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
654 if key == 'scoreboard-deaths': pgstat.deaths = int(value)
655 if key == 'scoreboard-kills': pgstat.kills = int(value)
656 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
657 if key == 'scoreboard-objectives': pgstat.collects = int(value)
658 if key == 'scoreboard-captured': pgstat.captures = int(value)
659 if key == 'scoreboard-released': pgstat.drops = int(value)
660 if key == 'scoreboard-fastest':
661 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
662 if key == 'scoreboard-takes': pgstat.pickups = int(value)
663 if key == 'scoreboard-ticks': pgstat.drops = int(value)
664 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
665 if key == 'scoreboard-bctime':
666 pgstat.time = datetime.timedelta(seconds=int(value))
667 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
668 if key == 'scoreboard-losses': pgstat.drops = int(value)
669 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
670 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
671 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
672 if key == 'scoreboard-lives': pgstat.lives = int(value)
673 if key == 'scoreboard-goals': pgstat.captures = int(value)
674 if key == 'scoreboard-faults': pgstat.drops = int(value)
675 if key == 'scoreboard-laps': pgstat.laps = int(value)
677 if key == 'avglatency': pgstat.avg_latency = float(value)
678 if key == 'scoreboard-captime':
679 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
680 if game.game_type_cd == 'ctf':
681 update_fastest_cap(session, player.player_id, game.game_id,
682 gmap.map_id, pgstat.fastest, game.mod)
684 # there is no "winning team" field, so we have to derive it
685 if wins and pgstat.team is not None and game.winner is None:
686 game.winner = pgstat.team
694 def create_anticheats(session, pgstat, game, player, events):
695 """Anticheats handler for all game types"""
699 # all anticheat events are prefixed by "anticheat"
700 for (key,value) in events.items():
701 if key.startswith("anticheat"):
703 ac = PlayerGameAnticheat(
709 anticheats.append(ac)
711 except Exception as e:
712 log.debug("Could not parse value for key %s. Ignoring." % key)
717 def create_default_team_stat(session, game_type_cd):
718 """Creates a blanked-out teamstat record for the given game type"""
720 # this is what we have to do to get partitioned records in - grab the
721 # sequence value first, then insert using the explicit ID (vs autogenerate)
722 seq = Sequence('team_game_stats_team_game_stat_id_seq')
723 teamstat_id = session.execute(seq)
724 teamstat = TeamGameStat(team_game_stat_id=teamstat_id,
725 create_dt=datetime.datetime.utcnow())
727 # all team game modes have a score, so we'll zero that out always
730 if game_type_cd in 'ca' 'ft' 'lms' 'ka':
733 if game_type_cd == 'ctf':
739 def create_team_stat(session, game, events):
740 """Team stats handler for all game types"""
743 teamstat = create_default_team_stat(session, game.game_type_cd)
744 teamstat.game_id = game.game_id
746 # we should have a team ID if we have a 'Q' event
747 if re.match(r'^team#\d+$', events.get('Q', '')):
748 team = int(events.get('Q').replace('team#', ''))
751 # gametype-specific stuff is handled here. if passed to us, we store it
752 for (key,value) in events.items():
753 if key == 'scoreboard-score': teamstat.score = int(round(float(value)))
754 if key == 'scoreboard-caps': teamstat.caps = int(value)
755 if key == 'scoreboard-goals': teamstat.caps = int(value)
756 if key == 'scoreboard-rounds': teamstat.rounds = int(value)
758 session.add(teamstat)
759 except Exception as e:
765 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
766 """Weapon stats handler for all game types"""
769 # Version 1 of stats submissions doubled the data sent.
770 # To counteract this we divide the data by 2 only for
771 # POSTs coming from version 1.
773 version = int(game_meta['V'])
776 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
782 for (key,value) in events.items():
783 matched = re.search("acc-(.*?)-cnt-fired", key)
785 weapon_cd = matched.group(1)
787 # Weapon names changed for 0.8. We'll convert the old
788 # ones to use the new scheme as well.
789 mapped_weapon_cd = weapon_map.get(weapon_cd, weapon_cd)
791 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
792 pwstat_id = session.execute(seq)
793 pwstat = PlayerWeaponStat()
794 pwstat.player_weapon_stats_id = pwstat_id
795 pwstat.player_id = player.player_id
796 pwstat.game_id = game.game_id
797 pwstat.player_game_stat_id = pgstat.player_game_stat_id
798 pwstat.weapon_cd = mapped_weapon_cd
801 pwstat.nick = events['n']
803 pwstat.nick = events['P']
805 if 'acc-' + weapon_cd + '-cnt-fired' in events:
806 pwstat.fired = int(round(float(
807 events['acc-' + weapon_cd + '-cnt-fired'])))
808 if 'acc-' + weapon_cd + '-fired' in events:
809 pwstat.max = int(round(float(
810 events['acc-' + weapon_cd + '-fired'])))
811 if 'acc-' + weapon_cd + '-cnt-hit' in events:
812 pwstat.hit = int(round(float(
813 events['acc-' + weapon_cd + '-cnt-hit'])))
814 if 'acc-' + weapon_cd + '-hit' in events:
815 pwstat.actual = int(round(float(
816 events['acc-' + weapon_cd + '-hit'])))
817 if 'acc-' + weapon_cd + '-frags' in events:
818 pwstat.frags = int(round(float(
819 events['acc-' + weapon_cd + '-frags'])))
822 pwstat.fired = pwstat.fired/2
823 pwstat.max = pwstat.max/2
824 pwstat.hit = pwstat.hit/2
825 pwstat.actual = pwstat.actual/2
826 pwstat.frags = pwstat.frags/2
829 pwstats.append(pwstat)
834 def submit_stats(request):
836 Entry handler for POST stats submissions.
839 # placeholder for the actual session
842 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
843 "----- END REQUEST BODY -----\n\n")
845 (idfp, status) = verify_request(request)
846 (game_meta, raw_players, raw_teams) = parse_stats_submission(request.body)
847 revision = game_meta.get('R', 'unknown')
848 duration = game_meta.get('D', None)
850 # only players present at the end of the match are eligible for stats
851 raw_players = filter(played_in_game, raw_players)
853 do_precondition_checks(request, game_meta, raw_players)
855 # the "duel" gametype is fake
856 if len(raw_players) == 2 \
857 and num_real_players(raw_players) == 2 \
858 and game_meta['G'] == 'dm':
859 game_meta['G'] = 'duel'
861 #----------------------------------------------------------------------
862 # Actual setup (inserts/updates) below here
863 #----------------------------------------------------------------------
864 session = DBSession()
866 game_type_cd = game_meta['G']
868 # All game types create Game, Server, Map, and Player records
870 server = get_or_create_server(
873 name = game_meta['S'],
875 ip_addr = get_remote_addr(request),
876 port = game_meta.get('U', None),
877 impure_cvars = game_meta.get('C', 0))
879 gmap = get_or_create_map(
881 name = game_meta['M'])
885 start_dt = datetime.datetime.utcnow(),
886 server_id = server.server_id,
887 game_type_cd = game_type_cd,
888 map_id = gmap.map_id,
889 match_id = game_meta['I'],
891 mod = game_meta.get('O', None))
893 # keep track of the players we've seen
896 for events in raw_players:
897 player = get_or_create_player(
899 hashkey = events['P'],
900 nick = events.get('n', None))
902 pgstat = create_game_stat(session, game_meta, game, server,
903 gmap, player, events)
904 pgstats.append(pgstat)
906 if player.player_id > 1:
907 anticheats = create_anticheats(session, pgstat, game, player,
910 if player.player_id > 2:
911 player_ids.append(player.player_id)
913 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
914 pwstats = create_weapon_stats(session, game_meta, game, player,
917 # store them on games for easy access
918 game.players = player_ids
920 for events in raw_teams:
922 teamstat = create_team_stat(session, game, events)
923 except Exception as e:
926 if should_do_elos(game_type_cd):
927 ep = EloProcessor(session, game, pgstats)
931 log.debug('Success! Stats recorded.')
932 return Response('200 OK')
933 except Exception as e: