6 import pyramid.httpexceptions
7 import sqlalchemy.sql.expression as expr
8 from sqlalchemy import Sequence
9 from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
10 from xonstat.elo import EloProcessor
11 from xonstat.models import DBSession, Server, Map, Game, PlayerGameStat, PlayerWeaponStat
12 from xonstat.models import PlayerRank, PlayerCaptime
13 from xonstat.models import TeamGameStat, PlayerGameAnticheat, Player, Hashkey, PlayerNick
14 from xonstat.util import strip_colors, qfont_decode, verify_request, weapon_map
16 log = logging.getLogger(__name__)
19 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
489 # There is some drift between start_dt (provided by app) and create_dt
490 # (default in the database), so we'll make them the same until this is
492 game.create_dt = start_dt
495 game.duration = datetime.timedelta(seconds=int(round(float(duration))))
500 session.query(Game).filter(Game.server_id==server_id).\
501 filter(Game.match_id==match_id).one()
503 log.debug("Error: game with same server and match_id found! Ignoring.")
505 # if a game under the same server and match_id found,
506 # this is a duplicate game and can be ignored
507 raise pyramid.httpexceptions.HTTPOk('OK')
508 except NoResultFound, e:
509 # server_id/match_id combination not found. game is ok to insert
512 log.debug("Created game id {0} on server {1}, map {2} at \
513 {3}".format(game.game_id,
514 server_id, map_id, start_dt))
519 def get_or_create_player(session=None, hashkey=None, nick=None):
521 Finds a player by hashkey or creates a new one (along with a
522 corresponding hashkey entry. Parameters:
524 session - SQLAlchemy database session factory
525 hashkey - hashkey of the player to be found or created
526 nick - nick of the player (in case of a first time create)
529 if re.search('^bot#\d+', hashkey):
530 player = session.query(Player).filter_by(player_id=1).one()
531 # if we have an untracked player
532 elif re.search('^player#\d+$', hashkey):
533 player = session.query(Player).filter_by(player_id=2).one()
534 # else it is a tracked player
536 # see if the player is already in the database
537 # if not, create one and the hashkey along with it
539 hk = session.query(Hashkey).filter_by(
540 hashkey=hashkey).one()
541 player = session.query(Player).filter_by(
542 player_id=hk.player_id).one()
543 log.debug("Found existing player {0} with hashkey {1}".format(
544 player.player_id, hashkey))
550 # if nick is given to us, use it. If not, use "Anonymous Player"
551 # with a suffix added for uniqueness.
553 player.nick = nick[:128]
554 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
556 player.nick = "Anonymous Player #{0}".format(player.player_id)
557 player.stripped_nick = player.nick
559 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
561 log.debug("Created player {0} ({2}) with hashkey {1}".format(
562 player.player_id, hashkey, player.nick.encode('utf-8')))
567 def create_default_game_stat(session, game_type_cd):
568 """Creates a blanked-out pgstat record for the given game type"""
570 # this is what we have to do to get partitioned records in - grab the
571 # sequence value first, then insert using the explicit ID (vs autogenerate)
572 seq = Sequence('player_game_stats_player_game_stat_id_seq')
573 pgstat_id = session.execute(seq)
574 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
575 create_dt=datetime.datetime.utcnow())
577 if game_type_cd == 'as':
578 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
580 if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
581 pgstat.kills = pgstat.deaths = pgstat.suicides = 0
583 if game_type_cd == 'cq':
584 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
587 if game_type_cd == 'ctf':
588 pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
589 pgstat.returns = pgstat.carrier_frags = 0
591 if game_type_cd == 'cts':
594 if game_type_cd == 'dom':
595 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
598 if game_type_cd == 'ft':
599 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
601 if game_type_cd == 'ka':
602 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
603 pgstat.carrier_frags = 0
604 pgstat.time = datetime.timedelta(seconds=0)
606 if game_type_cd == 'kh':
607 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
608 pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
609 pgstat.carrier_frags = 0
611 if game_type_cd == 'lms':
612 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
614 if game_type_cd == 'nb':
615 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
618 if game_type_cd == 'rc':
619 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
624 def create_game_stat(session, game_meta, game, server, gmap, player, events):
625 """Game stats handler for all game types"""
627 game_type_cd = game.game_type_cd
629 pgstat = create_default_game_stat(session, game_type_cd)
631 # these fields should be on every pgstat record
632 pgstat.game_id = game.game_id
633 pgstat.player_id = player.player_id
634 pgstat.nick = events.get('n', 'Anonymous Player')[:128]
635 pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
636 pgstat.score = int(round(float(events.get('scoreboard-score', 0))))
637 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
638 pgstat.rank = int(events.get('rank', None))
639 pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
641 if pgstat.nick != player.nick \
642 and player.player_id > 2 \
643 and pgstat.nick != 'Anonymous Player':
644 register_new_nick(session, player, pgstat.nick)
648 # gametype-specific stuff is handled here. if passed to us, we store it
649 for (key,value) in events.items():
650 if key == 'wins': wins = True
651 if key == 't': pgstat.team = int(value)
653 if key == 'scoreboard-drops': pgstat.drops = int(value)
654 if key == 'scoreboard-returns': pgstat.returns = int(value)
655 if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
656 if key == 'scoreboard-pickups': pgstat.pickups = int(value)
657 if key == 'scoreboard-caps': pgstat.captures = int(value)
658 if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
659 if key == 'scoreboard-deaths': pgstat.deaths = int(value)
660 if key == 'scoreboard-kills': pgstat.kills = int(value)
661 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
662 if key == 'scoreboard-objectives': pgstat.collects = int(value)
663 if key == 'scoreboard-captured': pgstat.captures = int(value)
664 if key == 'scoreboard-released': pgstat.drops = int(value)
665 if key == 'scoreboard-fastest':
666 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
667 if key == 'scoreboard-takes': pgstat.pickups = int(value)
668 if key == 'scoreboard-ticks': pgstat.drops = int(value)
669 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
670 if key == 'scoreboard-bctime':
671 pgstat.time = datetime.timedelta(seconds=int(value))
672 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
673 if key == 'scoreboard-losses': pgstat.drops = int(value)
674 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
675 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
676 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
677 if key == 'scoreboard-lives': pgstat.lives = int(value)
678 if key == 'scoreboard-goals': pgstat.captures = int(value)
679 if key == 'scoreboard-faults': pgstat.drops = int(value)
680 if key == 'scoreboard-laps': pgstat.laps = int(value)
682 if key == 'avglatency': pgstat.avg_latency = float(value)
683 if key == 'scoreboard-captime':
684 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
685 if game.game_type_cd == 'ctf':
686 update_fastest_cap(session, player.player_id, game.game_id,
687 gmap.map_id, pgstat.fastest, game.mod)
689 # there is no "winning team" field, so we have to derive it
690 if wins and pgstat.team is not None and game.winner is None:
691 game.winner = pgstat.team
699 def create_anticheats(session, pgstat, game, player, events):
700 """Anticheats handler for all game types"""
704 # all anticheat events are prefixed by "anticheat"
705 for (key,value) in events.items():
706 if key.startswith("anticheat"):
708 ac = PlayerGameAnticheat(
714 anticheats.append(ac)
716 except Exception as e:
717 log.debug("Could not parse value for key %s. Ignoring." % key)
722 def create_default_team_stat(session, game_type_cd):
723 """Creates a blanked-out teamstat record for the given game type"""
725 # this is what we have to do to get partitioned records in - grab the
726 # sequence value first, then insert using the explicit ID (vs autogenerate)
727 seq = Sequence('team_game_stats_team_game_stat_id_seq')
728 teamstat_id = session.execute(seq)
729 teamstat = TeamGameStat(team_game_stat_id=teamstat_id,
730 create_dt=datetime.datetime.utcnow())
732 # all team game modes have a score, so we'll zero that out always
735 if game_type_cd in 'ca' 'ft' 'lms' 'ka':
738 if game_type_cd == 'ctf':
744 def create_team_stat(session, game, events):
745 """Team stats handler for all game types"""
748 teamstat = create_default_team_stat(session, game.game_type_cd)
749 teamstat.game_id = game.game_id
751 # we should have a team ID if we have a 'Q' event
752 if re.match(r'^team#\d+$', events.get('Q', '')):
753 team = int(events.get('Q').replace('team#', ''))
756 # gametype-specific stuff is handled here. if passed to us, we store it
757 for (key,value) in events.items():
758 if key == 'scoreboard-score': teamstat.score = int(round(float(value)))
759 if key == 'scoreboard-caps': teamstat.caps = int(value)
760 if key == 'scoreboard-goals': teamstat.caps = int(value)
761 if key == 'scoreboard-rounds': teamstat.rounds = int(value)
763 session.add(teamstat)
764 except Exception as e:
770 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
771 """Weapon stats handler for all game types"""
774 # Version 1 of stats submissions doubled the data sent.
775 # To counteract this we divide the data by 2 only for
776 # POSTs coming from version 1.
778 version = int(game_meta['V'])
781 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
787 for (key,value) in events.items():
788 matched = re.search("acc-(.*?)-cnt-fired", key)
790 weapon_cd = matched.group(1)
792 # Weapon names changed for 0.8. We'll convert the old
793 # ones to use the new scheme as well.
794 mapped_weapon_cd = weapon_map.get(weapon_cd, weapon_cd)
796 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
797 pwstat_id = session.execute(seq)
798 pwstat = PlayerWeaponStat()
799 pwstat.player_weapon_stats_id = pwstat_id
800 pwstat.player_id = player.player_id
801 pwstat.game_id = game.game_id
802 pwstat.player_game_stat_id = pgstat.player_game_stat_id
803 pwstat.weapon_cd = mapped_weapon_cd
806 pwstat.nick = events['n']
808 pwstat.nick = events['P']
810 if 'acc-' + weapon_cd + '-cnt-fired' in events:
811 pwstat.fired = int(round(float(
812 events['acc-' + weapon_cd + '-cnt-fired'])))
813 if 'acc-' + weapon_cd + '-fired' in events:
814 pwstat.max = int(round(float(
815 events['acc-' + weapon_cd + '-fired'])))
816 if 'acc-' + weapon_cd + '-cnt-hit' in events:
817 pwstat.hit = int(round(float(
818 events['acc-' + weapon_cd + '-cnt-hit'])))
819 if 'acc-' + weapon_cd + '-hit' in events:
820 pwstat.actual = int(round(float(
821 events['acc-' + weapon_cd + '-hit'])))
822 if 'acc-' + weapon_cd + '-frags' in events:
823 pwstat.frags = int(round(float(
824 events['acc-' + weapon_cd + '-frags'])))
827 pwstat.fired = pwstat.fired/2
828 pwstat.max = pwstat.max/2
829 pwstat.hit = pwstat.hit/2
830 pwstat.actual = pwstat.actual/2
831 pwstat.frags = pwstat.frags/2
834 pwstats.append(pwstat)
839 def get_ranks(session, player_ids, game_type_cd):
841 Gets the rank entries for all players in the given list, returning a dict
842 of player_id -> PlayerRank instance. The rank entry corresponds to the
843 game type of the parameter passed in as well.
846 for pr in session.query(PlayerRank).\
847 filter(PlayerRank.player_id.in_(player_ids)).\
848 filter(PlayerRank.game_type_cd == game_type_cd).\
850 ranks[pr.player_id] = pr
855 def submit_stats(request):
857 Entry handler for POST stats submissions.
860 # placeholder for the actual session
863 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
864 "----- END REQUEST BODY -----\n\n")
866 (idfp, status) = verify_request(request)
867 (game_meta, raw_players, raw_teams) = parse_stats_submission(request.body)
868 revision = game_meta.get('R', 'unknown')
869 duration = game_meta.get('D', None)
871 # only players present at the end of the match are eligible for stats
872 raw_players = filter(played_in_game, raw_players)
874 do_precondition_checks(request, game_meta, raw_players)
876 # the "duel" gametype is fake
877 if len(raw_players) == 2 \
878 and num_real_players(raw_players) == 2 \
879 and game_meta['G'] == 'dm':
880 game_meta['G'] = 'duel'
882 #----------------------------------------------------------------------
883 # Actual setup (inserts/updates) below here
884 #----------------------------------------------------------------------
885 session = DBSession()
887 game_type_cd = game_meta['G']
889 # All game types create Game, Server, Map, and Player records
891 server = get_or_create_server(
894 name = game_meta['S'],
896 ip_addr = get_remote_addr(request),
897 port = game_meta.get('U', None),
898 impure_cvars = game_meta.get('C', 0))
900 gmap = get_or_create_map(
902 name = game_meta['M'])
906 start_dt = datetime.datetime.utcnow(),
907 server_id = server.server_id,
908 game_type_cd = game_type_cd,
909 map_id = gmap.map_id,
910 match_id = game_meta['I'],
912 mod = game_meta.get('O', None))
914 # keep track of the players we've seen
918 for events in raw_players:
919 player = get_or_create_player(
921 hashkey = events['P'],
922 nick = events.get('n', None))
924 pgstat = create_game_stat(session, game_meta, game, server,
925 gmap, player, events)
926 pgstats.append(pgstat)
928 if player.player_id > 1:
929 anticheats = create_anticheats(session, pgstat, game, player, events)
931 if player.player_id > 2:
932 player_ids.append(player.player_id)
933 hashkeys[player.player_id] = events['P']
935 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
936 pwstats = create_weapon_stats(session, game_meta, game, player,
939 # store them on games for easy access
940 game.players = player_ids
942 for events in raw_teams:
944 teamstat = create_team_stat(session, game, events)
945 except Exception as e:
948 if should_do_elos(game_type_cd):
949 ep = EloProcessor(session, game, pgstats)
953 log.debug('Success! Stats recorded.')
955 # ranks are fetched after we've done the "real" processing
956 ranks = get_ranks(session, player_ids, game_type_cd)
958 # plain text response
959 request.response.content_type = 'text/plain'
962 "now" : calendar.timegm(datetime.datetime.utcnow().timetuple()),
966 "player_ids" : player_ids,
967 "hashkeys" : hashkeys,
972 except Exception as e: