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 process_elos
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):
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).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)
348 session.add(cur_fastest_cap)
352 def get_or_create_server(session, name, hashkey, ip_addr, revision, port,
355 Find a server by name or create one if not found. Parameters:
357 session - SQLAlchemy database session factory
358 name - server name of the server to be found or created
359 hashkey - server hashkey
360 ip_addr - the IP address of the server
361 revision - the xonotic revision number
362 port - the port number of the server
363 impure_cvars - the number of impure cvar changes
373 impure_cvars = int(impure_cvars)
377 # finding by hashkey is preferred, but if not we will fall
378 # back to using name only, which can result in dupes
379 if hashkey is not None:
380 servers = session.query(Server).\
381 filter_by(hashkey=hashkey).\
382 order_by(expr.desc(Server.create_dt)).limit(1).all()
386 log.debug("Found existing server {0} by hashkey ({1})".format(
387 server.server_id, server.hashkey))
389 servers = session.query(Server).\
390 filter_by(name=name).\
391 order_by(expr.desc(Server.create_dt)).limit(1).all()
395 log.debug("Found existing server {0} by name".format(server.server_id))
397 # still haven't found a server by hashkey or name, so we need to create one
399 server = Server(name=name, hashkey=hashkey)
402 log.debug("Created server {0} with hashkey {1}".format(
403 server.server_id, server.hashkey))
405 # detect changed fields
406 if server.name != name:
410 if server.hashkey != hashkey:
411 server.hashkey = hashkey
414 if server.ip_addr != ip_addr:
415 server.ip_addr = ip_addr
418 if server.port != port:
422 if server.revision != revision:
423 server.revision = revision
426 if server.impure_cvars != impure_cvars:
427 server.impure_cvars = impure_cvars
429 server.pure_ind = False
431 server.pure_ind = True
437 def get_or_create_map(session=None, name=None):
439 Find a map by name or create one if not found. Parameters:
441 session - SQLAlchemy database session factory
442 name - map name of the map to be found or created
445 # find one by the name, if it exists
446 gmap = session.query(Map).filter_by(name=name).one()
447 log.debug("Found map id {0}: {1}".format(gmap.map_id,
449 except NoResultFound, e:
450 gmap = Map(name=name)
453 log.debug("Created map id {0}: {1}".format(gmap.map_id,
455 except MultipleResultsFound, e:
456 # multiple found, so use the first one but warn
458 gmaps = session.query(Map).filter_by(name=name).order_by(
461 log.debug("Found map id {0}: {1} but found \
462 multiple".format(gmap.map_id, gmap.name))
467 def create_game(session, start_dt, game_type_cd, server_id, map_id,
468 match_id, duration, mod, winner=None):
470 Creates a game. Parameters:
472 session - SQLAlchemy database session factory
473 start_dt - when the game started (datetime object)
474 game_type_cd - the game type of the game being played
475 server_id - server identifier of the server hosting the game
476 map_id - map on which the game was played
477 winner - the team id of the team that won
478 duration - how long the game lasted
479 mod - mods in use during the game
481 seq = Sequence('games_game_id_seq')
482 game_id = session.execute(seq)
483 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
484 server_id=server_id, map_id=map_id, winner=winner)
485 game.match_id = match_id
489 game.duration = datetime.timedelta(seconds=int(round(float(duration))))
494 session.query(Game).filter(Game.server_id==server_id).\
495 filter(Game.match_id==match_id).one()
497 log.debug("Error: game with same server and match_id found! Ignoring.")
499 # if a game under the same server and match_id found,
500 # this is a duplicate game and can be ignored
501 raise pyramid.httpexceptions.HTTPOk('OK')
502 except NoResultFound, e:
503 # server_id/match_id combination not found. game is ok to insert
506 log.debug("Created game id {0} on server {1}, map {2} at \
507 {3}".format(game.game_id,
508 server_id, map_id, start_dt))
513 def get_or_create_player(session=None, hashkey=None, nick=None):
515 Finds a player by hashkey or creates a new one (along with a
516 corresponding hashkey entry. Parameters:
518 session - SQLAlchemy database session factory
519 hashkey - hashkey of the player to be found or created
520 nick - nick of the player (in case of a first time create)
523 if re.search('^bot#\d+$', hashkey) or re.search('^bot#\d+#', hashkey):
524 player = session.query(Player).filter_by(player_id=1).one()
525 # if we have an untracked player
526 elif re.search('^player#\d+$', hashkey):
527 player = session.query(Player).filter_by(player_id=2).one()
528 # else it is a tracked player
530 # see if the player is already in the database
531 # if not, create one and the hashkey along with it
533 hk = session.query(Hashkey).filter_by(
534 hashkey=hashkey).one()
535 player = session.query(Player).filter_by(
536 player_id=hk.player_id).one()
537 log.debug("Found existing player {0} with hashkey {1}".format(
538 player.player_id, hashkey))
544 # if nick is given to us, use it. If not, use "Anonymous Player"
545 # with a suffix added for uniqueness.
547 player.nick = nick[:128]
548 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
550 player.nick = "Anonymous Player #{0}".format(player.player_id)
551 player.stripped_nick = player.nick
553 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
555 log.debug("Created player {0} ({2}) with hashkey {1}".format(
556 player.player_id, hashkey, player.nick.encode('utf-8')))
561 def create_default_game_stat(session, game_type_cd):
562 """Creates a blanked-out pgstat record for the given game type"""
564 # this is what we have to do to get partitioned records in - grab the
565 # sequence value first, then insert using the explicit ID (vs autogenerate)
566 seq = Sequence('player_game_stats_player_game_stat_id_seq')
567 pgstat_id = session.execute(seq)
568 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
569 create_dt=datetime.datetime.utcnow())
571 if game_type_cd == 'as':
572 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
574 if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
575 pgstat.kills = pgstat.deaths = pgstat.suicides = 0
577 if game_type_cd == 'cq':
578 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
581 if game_type_cd == 'ctf':
582 pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
583 pgstat.returns = pgstat.carrier_frags = 0
585 if game_type_cd == 'cts':
588 if game_type_cd == 'dom':
589 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
592 if game_type_cd == 'ft':
593 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
595 if game_type_cd == 'ka':
596 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
597 pgstat.carrier_frags = 0
598 pgstat.time = datetime.timedelta(seconds=0)
600 if game_type_cd == 'kh':
601 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
602 pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
603 pgstat.carrier_frags = 0
605 if game_type_cd == 'lms':
606 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
608 if game_type_cd == 'nb':
609 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
612 if game_type_cd == 'rc':
613 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
618 def create_game_stat(session, game_meta, game, server, gmap, player, events):
619 """Game stats handler for all game types"""
621 game_type_cd = game.game_type_cd
623 pgstat = create_default_game_stat(session, game_type_cd)
625 # these fields should be on every pgstat record
626 pgstat.game_id = game.game_id
627 pgstat.player_id = player.player_id
628 pgstat.nick = events.get('n', 'Anonymous Player')[:128]
629 pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
630 pgstat.score = int(round(float(events.get('scoreboard-score', 0))))
631 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
632 pgstat.rank = int(events.get('rank', None))
633 pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
635 if pgstat.nick != player.nick \
636 and player.player_id > 2 \
637 and pgstat.nick != 'Anonymous Player':
638 register_new_nick(session, player, pgstat.nick)
642 # gametype-specific stuff is handled here. if passed to us, we store it
643 for (key,value) in events.items():
644 if key == 'wins': wins = True
645 if key == 't': pgstat.team = int(value)
647 if key == 'scoreboard-drops': pgstat.drops = int(value)
648 if key == 'scoreboard-returns': pgstat.returns = int(value)
649 if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
650 if key == 'scoreboard-pickups': pgstat.pickups = int(value)
651 if key == 'scoreboard-caps': pgstat.captures = int(value)
652 if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
653 if key == 'scoreboard-deaths': pgstat.deaths = int(value)
654 if key == 'scoreboard-kills': pgstat.kills = int(value)
655 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
656 if key == 'scoreboard-objectives': pgstat.collects = int(value)
657 if key == 'scoreboard-captured': pgstat.captures = int(value)
658 if key == 'scoreboard-released': pgstat.drops = int(value)
659 if key == 'scoreboard-fastest':
660 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
661 if key == 'scoreboard-takes': pgstat.pickups = int(value)
662 if key == 'scoreboard-ticks': pgstat.drops = int(value)
663 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
664 if key == 'scoreboard-bctime':
665 pgstat.time = datetime.timedelta(seconds=int(value))
666 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
667 if key == 'scoreboard-losses': pgstat.drops = int(value)
668 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
669 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
670 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
671 if key == 'scoreboard-lives': pgstat.lives = int(value)
672 if key == 'scoreboard-goals': pgstat.captures = int(value)
673 if key == 'scoreboard-faults': pgstat.drops = int(value)
674 if key == 'scoreboard-laps': pgstat.laps = int(value)
676 if key == 'avglatency': pgstat.avg_latency = float(value)
677 if key == 'scoreboard-captime':
678 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
679 if game.game_type_cd == 'ctf':
680 update_fastest_cap(session, player.player_id, game.game_id,
681 gmap.map_id, pgstat.fastest)
683 # there is no "winning team" field, so we have to derive it
684 if wins and pgstat.team is not None and game.winner is None:
685 game.winner = pgstat.team
693 def create_anticheats(session, pgstat, game, player, events):
694 """Anticheats handler for all game types"""
698 # all anticheat events are prefixed by "anticheat"
699 for (key,value) in events.items():
700 if key.startswith("anticheat"):
702 ac = PlayerGameAnticheat(
708 anticheats.append(ac)
710 except Exception as e:
711 log.debug("Could not parse value for key %s. Ignoring." % key)
716 def create_default_team_stat(session, game_type_cd):
717 """Creates a blanked-out teamstat record for the given game type"""
719 # this is what we have to do to get partitioned records in - grab the
720 # sequence value first, then insert using the explicit ID (vs autogenerate)
721 seq = Sequence('team_game_stats_team_game_stat_id_seq')
722 teamstat_id = session.execute(seq)
723 teamstat = TeamGameStat(team_game_stat_id=teamstat_id,
724 create_dt=datetime.datetime.utcnow())
726 # all team game modes have a score, so we'll zero that out always
729 if game_type_cd in 'ca' 'ft' 'lms' 'ka':
732 if game_type_cd == 'ctf':
738 def create_team_stat(session, game, events):
739 """Team stats handler for all game types"""
742 teamstat = create_default_team_stat(session, game.game_type_cd)
743 teamstat.game_id = game.game_id
745 # we should have a team ID if we have a 'Q' event
746 if re.match(r'^team#\d+$', events.get('Q', '')):
747 team = int(events.get('Q').replace('team#', ''))
750 # gametype-specific stuff is handled here. if passed to us, we store it
751 for (key,value) in events.items():
752 if key == 'scoreboard-score': teamstat.score = int(round(float(value)))
753 if key == 'scoreboard-caps': teamstat.caps = int(value)
754 if key == 'scoreboard-rounds': teamstat.rounds = int(value)
756 session.add(teamstat)
757 except Exception as e:
763 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
764 """Weapon stats handler for all game types"""
767 # Version 1 of stats submissions doubled the data sent.
768 # To counteract this we divide the data by 2 only for
769 # POSTs coming from version 1.
771 version = int(game_meta['V'])
774 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
780 for (key,value) in events.items():
781 matched = re.search("acc-(.*?)-cnt-fired", key)
783 weapon_cd = matched.group(1)
785 # Weapon names changed for 0.8. We'll convert the old
786 # ones to use the new scheme as well.
787 mapped_weapon_cd = weapon_map.get(weapon_cd, weapon_cd)
789 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
790 pwstat_id = session.execute(seq)
791 pwstat = PlayerWeaponStat()
792 pwstat.player_weapon_stats_id = pwstat_id
793 pwstat.player_id = player.player_id
794 pwstat.game_id = game.game_id
795 pwstat.player_game_stat_id = pgstat.player_game_stat_id
796 pwstat.weapon_cd = mapped_weapon_cd
799 pwstat.nick = events['n']
801 pwstat.nick = events['P']
803 if 'acc-' + weapon_cd + '-cnt-fired' in events:
804 pwstat.fired = int(round(float(
805 events['acc-' + weapon_cd + '-cnt-fired'])))
806 if 'acc-' + weapon_cd + '-fired' in events:
807 pwstat.max = int(round(float(
808 events['acc-' + weapon_cd + '-fired'])))
809 if 'acc-' + weapon_cd + '-cnt-hit' in events:
810 pwstat.hit = int(round(float(
811 events['acc-' + weapon_cd + '-cnt-hit'])))
812 if 'acc-' + weapon_cd + '-hit' in events:
813 pwstat.actual = int(round(float(
814 events['acc-' + weapon_cd + '-hit'])))
815 if 'acc-' + weapon_cd + '-frags' in events:
816 pwstat.frags = int(round(float(
817 events['acc-' + weapon_cd + '-frags'])))
820 pwstat.fired = pwstat.fired/2
821 pwstat.max = pwstat.max/2
822 pwstat.hit = pwstat.hit/2
823 pwstat.actual = pwstat.actual/2
824 pwstat.frags = pwstat.frags/2
827 pwstats.append(pwstat)
832 def create_elos(session, game):
833 """Elo handler for all game types."""
835 process_elos(game, session)
836 except Exception as e:
837 log.debug('Error (non-fatal): elo processing failed.')
840 def submit_stats(request):
842 Entry handler for POST stats submissions.
845 # placeholder for the actual session
848 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
849 "----- END REQUEST BODY -----\n\n")
851 (idfp, status) = verify_request(request)
852 (game_meta, raw_players, raw_teams) = parse_stats_submission(request.body)
853 revision = game_meta.get('R', 'unknown')
854 duration = game_meta.get('D', None)
856 # only players present at the end of the match are eligible for stats
857 raw_players = filter(played_in_game, raw_players)
859 do_precondition_checks(request, game_meta, raw_players)
861 # the "duel" gametype is fake
862 if len(raw_players) == 2 \
863 and num_real_players(raw_players) == 2 \
864 and game_meta['G'] == 'dm':
865 game_meta['G'] = 'duel'
867 #----------------------------------------------------------------------
868 # Actual setup (inserts/updates) below here
869 #----------------------------------------------------------------------
870 session = DBSession()
872 game_type_cd = game_meta['G']
874 # All game types create Game, Server, Map, and Player records
876 server = get_or_create_server(
879 name = game_meta['S'],
881 ip_addr = get_remote_addr(request),
882 port = game_meta.get('U', None),
883 impure_cvars = game_meta.get('C', 0))
885 gmap = get_or_create_map(
887 name = game_meta['M'])
891 start_dt = datetime.datetime.utcnow(),
892 server_id = server.server_id,
893 game_type_cd = game_type_cd,
894 map_id = gmap.map_id,
895 match_id = game_meta['I'],
897 mod = game_meta.get('O', None))
899 # keep track of the players we've seen
901 for events in raw_players:
902 player = get_or_create_player(
904 hashkey = events['P'],
905 nick = events.get('n', None))
907 pgstat = create_game_stat(session, game_meta, game, server,
908 gmap, player, events)
910 if player.player_id > 1:
911 anticheats = create_anticheats(session, pgstat, game, player,
913 player_ids.append(player.player_id)
915 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
916 pwstats = create_weapon_stats(session, game_meta, game, player,
919 # store them on games for easy access
920 game.players = player_ids
922 for events in raw_teams:
924 teamstat = create_team_stat(session, game, events)
925 except Exception as e:
928 if should_do_elos(game_type_cd):
929 create_elos(session, game)
932 log.debug('Success! Stats recorded.')
933 return Response('200 OK')
934 except Exception as e: