5 import pyramid.httpexceptions
6 import sqlalchemy.sql.expression as expr
7 from sqlalchemy import Sequence
8 from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
9 from xonstat.elo import EloProcessor
10 from xonstat.models import DBSession, Server, Map, Game, PlayerGameStat, PlayerWeaponStat
11 from xonstat.models import PlayerRank, PlayerCaptime
12 from xonstat.models import TeamGameStat, PlayerGameAnticheat, Player, Hashkey, PlayerNick
13 from xonstat.util import strip_colors, qfont_decode, verify_request, weapon_map
15 log = logging.getLogger(__name__)
18 def parse_stats_submission(body):
20 Parses the POST request body for a stats submission
22 # storage vars for the request body
28 # we're not in either stanza to start
31 for line in body.split('\n'):
33 (key, value) = line.strip().split(' ', 1)
35 # Server (S) and Nick (n) fields can have international characters.
37 value = unicode(value, 'utf-8')
39 if key not in 'P' 'Q' 'n' 'e' 't' 'i':
40 game_meta[key] = value
42 if key == 'Q' or key == 'P':
43 #log.debug('Found a {0}'.format(key))
44 #log.debug('in_Q: {0}'.format(in_Q))
45 #log.debug('in_P: {0}'.format(in_P))
46 #log.debug('events: {0}'.format(events))
48 # check where we were before and append events accordingly
49 if in_Q and len(events) > 0:
50 #log.debug('creating a team (Q) entry')
53 elif in_P and len(events) > 0:
54 #log.debug('creating a player (P) entry')
55 players.append(events)
59 #log.debug('key == P')
63 #log.debug('key == Q')
70 (subkey, subvalue) = value.split(' ', 1)
71 events[subkey] = subvalue
77 # no key/value pair - move on to the next line
80 # add the last entity we were working on
81 if in_P and len(events) > 0:
82 players.append(events)
83 elif in_Q and len(events) > 0:
86 return (game_meta, players, teams)
89 def is_blank_game(gametype, players):
90 """Determine if this is a blank game or not. A blank game is either:
92 1) a match that ended in the warmup stage, where accuracy events are not
93 present (for non-CTS games)
95 2) a match in which no player made a positive or negative score AND was
98 ... or for CTS, which doesn't record accuracy events
100 1) a match in which no player made a fastest lap AND was
103 ... or for NB, in which not all maps have weapons
105 1) a match in which no player made a positive or negative score
107 r = re.compile(r'acc-.*-cnt-fired')
108 flg_nonzero_score = False
109 flg_acc_events = False
110 flg_fastest_lap = False
112 for events in players:
113 if is_real_player(events) and played_in_game(events):
114 for (key,value) in events.items():
115 if key == 'scoreboard-score' and value != 0:
116 flg_nonzero_score = True
118 flg_acc_events = True
119 if key == 'scoreboard-fastest':
120 flg_fastest_lap = True
122 if gametype == 'cts':
123 return not flg_fastest_lap
124 elif gametype == 'nb':
125 return not flg_nonzero_score
127 return not (flg_nonzero_score and flg_acc_events)
130 def get_remote_addr(request):
131 """Get the Xonotic server's IP address"""
132 if 'X-Forwarded-For' in request.headers:
133 return request.headers['X-Forwarded-For']
135 return request.remote_addr
138 def is_supported_gametype(gametype, version):
139 """Whether a gametype is supported or not"""
142 # if the type can be supported, but with version constraints, uncomment
143 # here and add the restriction for a specific version below
144 supported_game_types = (
162 if gametype in supported_game_types:
167 # some game types were buggy before revisions, thus this additional filter
168 if gametype == 'ca' and version <= 5:
174 def do_precondition_checks(request, game_meta, raw_players):
175 """Precondition checks for ALL gametypes.
176 These do not require a database connection."""
177 if not has_required_metadata(game_meta):
178 log.debug("ERROR: Required game meta missing")
179 raise pyramid.httpexceptions.HTTPUnprocessableEntity("Missing game meta")
182 version = int(game_meta['V'])
184 log.debug("ERROR: Required game meta invalid")
185 raise pyramid.httpexceptions.HTTPUnprocessableEntity("Invalid game meta")
187 if not is_supported_gametype(game_meta['G'], version):
188 log.debug("ERROR: Unsupported gametype")
189 raise pyramid.httpexceptions.HTTPOk("OK")
191 if not has_minimum_real_players(request.registry.settings, raw_players):
192 log.debug("ERROR: Not enough real players")
193 raise pyramid.httpexceptions.HTTPOk("OK")
195 if is_blank_game(game_meta['G'], raw_players):
196 log.debug("ERROR: Blank game")
197 raise pyramid.httpexceptions.HTTPOk("OK")
200 def is_real_player(events):
202 Determines if a given set of events correspond with a non-bot
204 if not events['P'].startswith('bot'):
210 def played_in_game(events):
212 Determines if a given set of player events correspond with a player who
213 played in the game (matches 1 and scoreboardvalid 1)
215 if 'matches' in events and 'scoreboardvalid' in events:
221 def num_real_players(player_events):
223 Returns the number of real players (those who played
224 and are on the scoreboard).
228 for events in player_events:
229 if is_real_player(events) and played_in_game(events):
235 def has_minimum_real_players(settings, player_events):
237 Determines if the collection of player events has enough "real" players
238 to store in the database. The minimum setting comes from the config file
239 under the setting xonstat.minimum_real_players.
241 flg_has_min_real_players = True
244 minimum_required_players = int(
245 settings['xonstat.minimum_required_players'])
247 minimum_required_players = 2
249 real_players = num_real_players(player_events)
251 if real_players < minimum_required_players:
252 flg_has_min_real_players = False
254 return flg_has_min_real_players
257 def has_required_metadata(metadata):
259 Determines if a give set of metadata has enough data to create a game,
260 server, and map with.
262 flg_has_req_metadata = True
264 if 'G' not in metadata or\
265 'M' not in metadata or\
266 'I' not in metadata or\
268 flg_has_req_metadata = False
270 return flg_has_req_metadata
273 def should_do_weapon_stats(game_type_cd):
274 """True of the game type should record weapon stats. False otherwise."""
275 if game_type_cd in 'cts':
281 def should_do_elos(game_type_cd):
282 """True of the game type should process Elos. False otherwise."""
283 elo_game_types = ('duel', 'dm', 'ca', 'ctf', 'tdm', 'ka', 'ft')
285 if game_type_cd in elo_game_types:
291 def register_new_nick(session, player, new_nick):
293 Change the player record's nick to the newly found nick. Store the old
294 nick in the player_nicks table for that player.
296 session - SQLAlchemy database session factory
297 player - player record whose nick is changing
298 new_nick - the new nickname
300 # see if that nick already exists
301 stripped_nick = strip_colors(qfont_decode(player.nick))
303 player_nick = session.query(PlayerNick).filter_by(
304 player_id=player.player_id, stripped_nick=stripped_nick).one()
305 except NoResultFound, e:
306 # player_id/stripped_nick not found, create one
307 # but we don't store "Anonymous Player #N"
308 if not re.search('^Anonymous Player #\d+$', player.nick):
309 player_nick = PlayerNick()
310 player_nick.player_id = player.player_id
311 player_nick.stripped_nick = stripped_nick
312 player_nick.nick = player.nick
313 session.add(player_nick)
315 # We change to the new nick regardless
316 player.nick = new_nick
317 player.stripped_nick = strip_colors(qfont_decode(new_nick))
321 def update_fastest_cap(session, player_id, game_id, map_id, captime, mod):
323 Check the fastest cap time for the player and map. If there isn't
324 one, insert one. If there is, check if the passed time is faster.
327 # we don't record fastest cap times for bots or anonymous players
331 # see if a cap entry exists already
332 # then check to see if the new captime is faster
334 cur_fastest_cap = session.query(PlayerCaptime).filter_by(
335 player_id=player_id, map_id=map_id, mod=mod).one()
337 # current captime is faster, so update
338 if captime < cur_fastest_cap.fastest_cap:
339 cur_fastest_cap.fastest_cap = captime
340 cur_fastest_cap.game_id = game_id
341 cur_fastest_cap.create_dt = datetime.datetime.utcnow()
342 session.add(cur_fastest_cap)
344 except NoResultFound, e:
345 # none exists, so insert
346 cur_fastest_cap = PlayerCaptime(player_id, game_id, map_id, captime,
348 session.add(cur_fastest_cap)
352 def 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
488 # There is some drift between start_dt (provided by app) and create_dt
489 # (default in the database), so we'll make them the same until this is
491 game.create_dt = start_dt
494 game.duration = datetime.timedelta(seconds=int(round(float(duration))))
499 session.query(Game).filter(Game.server_id==server_id).\
500 filter(Game.match_id==match_id).one()
502 log.debug("Error: game with same server and match_id found! Ignoring.")
504 # if a game under the same server and match_id found,
505 # this is a duplicate game and can be ignored
506 raise pyramid.httpexceptions.HTTPOk('OK')
507 except NoResultFound, e:
508 # server_id/match_id combination not found. game is ok to insert
511 log.debug("Created game id {0} on server {1}, map {2} at \
512 {3}".format(game.game_id,
513 server_id, map_id, start_dt))
518 def get_or_create_player(session=None, hashkey=None, nick=None):
520 Finds a player by hashkey or creates a new one (along with a
521 corresponding hashkey entry. Parameters:
523 session - SQLAlchemy database session factory
524 hashkey - hashkey of the player to be found or created
525 nick - nick of the player (in case of a first time create)
528 if re.search('^bot#\d+', hashkey):
529 player = session.query(Player).filter_by(player_id=1).one()
530 # if we have an untracked player
531 elif re.search('^player#\d+$', hashkey):
532 player = session.query(Player).filter_by(player_id=2).one()
533 # else it is a tracked player
535 # see if the player is already in the database
536 # if not, create one and the hashkey along with it
538 hk = session.query(Hashkey).filter_by(
539 hashkey=hashkey).one()
540 player = session.query(Player).filter_by(
541 player_id=hk.player_id).one()
542 log.debug("Found existing player {0} with hashkey {1}".format(
543 player.player_id, hashkey))
549 # if nick is given to us, use it. If not, use "Anonymous Player"
550 # with a suffix added for uniqueness.
552 player.nick = nick[:128]
553 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
555 player.nick = "Anonymous Player #{0}".format(player.player_id)
556 player.stripped_nick = player.nick
558 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
560 log.debug("Created player {0} ({2}) with hashkey {1}".format(
561 player.player_id, hashkey, player.nick.encode('utf-8')))
566 def create_default_game_stat(session, game_type_cd):
567 """Creates a blanked-out pgstat record for the given game type"""
569 # this is what we have to do to get partitioned records in - grab the
570 # sequence value first, then insert using the explicit ID (vs autogenerate)
571 seq = Sequence('player_game_stats_player_game_stat_id_seq')
572 pgstat_id = session.execute(seq)
573 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
574 create_dt=datetime.datetime.utcnow())
576 if game_type_cd == 'as':
577 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
579 if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
580 pgstat.kills = pgstat.deaths = pgstat.suicides = 0
582 if game_type_cd == 'cq':
583 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
586 if game_type_cd == 'ctf':
587 pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
588 pgstat.returns = pgstat.carrier_frags = 0
590 if game_type_cd == 'cts':
593 if game_type_cd == 'dom':
594 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
597 if game_type_cd == 'ft':
598 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
600 if game_type_cd == 'ka':
601 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
602 pgstat.carrier_frags = 0
603 pgstat.time = datetime.timedelta(seconds=0)
605 if game_type_cd == 'kh':
606 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
607 pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
608 pgstat.carrier_frags = 0
610 if game_type_cd == 'lms':
611 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
613 if game_type_cd == 'nb':
614 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
617 if game_type_cd == 'rc':
618 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
623 def create_game_stat(session, game_meta, game, server, gmap, player, events):
624 """Game stats handler for all game types"""
626 game_type_cd = game.game_type_cd
628 pgstat = create_default_game_stat(session, game_type_cd)
630 # these fields should be on every pgstat record
631 pgstat.game_id = game.game_id
632 pgstat.player_id = player.player_id
633 pgstat.nick = events.get('n', 'Anonymous Player')[:128]
634 pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
635 pgstat.score = int(round(float(events.get('scoreboard-score', 0))))
636 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
637 pgstat.rank = int(events.get('rank', None))
638 pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
640 if pgstat.nick != player.nick \
641 and player.player_id > 2 \
642 and pgstat.nick != 'Anonymous Player':
643 register_new_nick(session, player, pgstat.nick)
647 # gametype-specific stuff is handled here. if passed to us, we store it
648 for (key,value) in events.items():
649 if key == 'wins': wins = True
650 if key == 't': pgstat.team = int(value)
652 if key == 'scoreboard-drops': pgstat.drops = int(value)
653 if key == 'scoreboard-returns': pgstat.returns = int(value)
654 if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
655 if key == 'scoreboard-pickups': pgstat.pickups = int(value)
656 if key == 'scoreboard-caps': pgstat.captures = int(value)
657 if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
658 if key == 'scoreboard-deaths': pgstat.deaths = int(value)
659 if key == 'scoreboard-kills': pgstat.kills = int(value)
660 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
661 if key == 'scoreboard-objectives': pgstat.collects = int(value)
662 if key == 'scoreboard-captured': pgstat.captures = int(value)
663 if key == 'scoreboard-released': pgstat.drops = int(value)
664 if key == 'scoreboard-fastest':
665 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
666 if key == 'scoreboard-takes': pgstat.pickups = int(value)
667 if key == 'scoreboard-ticks': pgstat.drops = int(value)
668 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
669 if key == 'scoreboard-bctime':
670 pgstat.time = datetime.timedelta(seconds=int(value))
671 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
672 if key == 'scoreboard-losses': pgstat.drops = int(value)
673 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
674 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
675 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
676 if key == 'scoreboard-lives': pgstat.lives = int(value)
677 if key == 'scoreboard-goals': pgstat.captures = int(value)
678 if key == 'scoreboard-faults': pgstat.drops = int(value)
679 if key == 'scoreboard-laps': pgstat.laps = int(value)
681 if key == 'avglatency': pgstat.avg_latency = float(value)
682 if key == 'scoreboard-captime':
683 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
684 if game.game_type_cd == 'ctf':
685 update_fastest_cap(session, player.player_id, game.game_id,
686 gmap.map_id, pgstat.fastest, game.mod)
688 # there is no "winning team" field, so we have to derive it
689 if wins and pgstat.team is not None and game.winner is None:
690 game.winner = pgstat.team
698 def create_anticheats(session, pgstat, game, player, events):
699 """Anticheats handler for all game types"""
703 # all anticheat events are prefixed by "anticheat"
704 for (key,value) in events.items():
705 if key.startswith("anticheat"):
707 ac = PlayerGameAnticheat(
713 anticheats.append(ac)
715 except Exception as e:
716 log.debug("Could not parse value for key %s. Ignoring." % key)
721 def create_default_team_stat(session, game_type_cd):
722 """Creates a blanked-out teamstat record for the given game type"""
724 # this is what we have to do to get partitioned records in - grab the
725 # sequence value first, then insert using the explicit ID (vs autogenerate)
726 seq = Sequence('team_game_stats_team_game_stat_id_seq')
727 teamstat_id = session.execute(seq)
728 teamstat = TeamGameStat(team_game_stat_id=teamstat_id,
729 create_dt=datetime.datetime.utcnow())
731 # all team game modes have a score, so we'll zero that out always
734 if game_type_cd in 'ca' 'ft' 'lms' 'ka':
737 if game_type_cd == 'ctf':
743 def create_team_stat(session, game, events):
744 """Team stats handler for all game types"""
747 teamstat = create_default_team_stat(session, game.game_type_cd)
748 teamstat.game_id = game.game_id
750 # we should have a team ID if we have a 'Q' event
751 if re.match(r'^team#\d+$', events.get('Q', '')):
752 team = int(events.get('Q').replace('team#', ''))
755 # gametype-specific stuff is handled here. if passed to us, we store it
756 for (key,value) in events.items():
757 if key == 'scoreboard-score': teamstat.score = int(round(float(value)))
758 if key == 'scoreboard-caps': teamstat.caps = int(value)
759 if key == 'scoreboard-goals': teamstat.caps = int(value)
760 if key == 'scoreboard-rounds': teamstat.rounds = int(value)
762 session.add(teamstat)
763 except Exception as e:
769 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
770 """Weapon stats handler for all game types"""
773 # Version 1 of stats submissions doubled the data sent.
774 # To counteract this we divide the data by 2 only for
775 # POSTs coming from version 1.
777 version = int(game_meta['V'])
780 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
786 for (key,value) in events.items():
787 matched = re.search("acc-(.*?)-cnt-fired", key)
789 weapon_cd = matched.group(1)
791 # Weapon names changed for 0.8. We'll convert the old
792 # ones to use the new scheme as well.
793 mapped_weapon_cd = weapon_map.get(weapon_cd, weapon_cd)
795 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
796 pwstat_id = session.execute(seq)
797 pwstat = PlayerWeaponStat()
798 pwstat.player_weapon_stats_id = pwstat_id
799 pwstat.player_id = player.player_id
800 pwstat.game_id = game.game_id
801 pwstat.player_game_stat_id = pgstat.player_game_stat_id
802 pwstat.weapon_cd = mapped_weapon_cd
805 pwstat.nick = events['n']
807 pwstat.nick = events['P']
809 if 'acc-' + weapon_cd + '-cnt-fired' in events:
810 pwstat.fired = int(round(float(
811 events['acc-' + weapon_cd + '-cnt-fired'])))
812 if 'acc-' + weapon_cd + '-fired' in events:
813 pwstat.max = int(round(float(
814 events['acc-' + weapon_cd + '-fired'])))
815 if 'acc-' + weapon_cd + '-cnt-hit' in events:
816 pwstat.hit = int(round(float(
817 events['acc-' + weapon_cd + '-cnt-hit'])))
818 if 'acc-' + weapon_cd + '-hit' in events:
819 pwstat.actual = int(round(float(
820 events['acc-' + weapon_cd + '-hit'])))
821 if 'acc-' + weapon_cd + '-frags' in events:
822 pwstat.frags = int(round(float(
823 events['acc-' + weapon_cd + '-frags'])))
826 pwstat.fired = pwstat.fired/2
827 pwstat.max = pwstat.max/2
828 pwstat.hit = pwstat.hit/2
829 pwstat.actual = pwstat.actual/2
830 pwstat.frags = pwstat.frags/2
833 pwstats.append(pwstat)
838 def get_ranks(session, player_ids, game_type_cd):
840 Gets the rank entries for all players in the given list, returning a dict
841 of player_id -> PlayerRank instance. The rank entry corresponds to the
842 game type of the parameter passed in as well.
845 for pr in session.query(PlayerRank).\
846 filter(PlayerRank.player_id.in_(player_ids)).\
847 filter(PlayerRank.game_type_cd == game_type_cd).\
849 ranks[pr.player_id] = pr
854 def submit_stats(request):
856 Entry handler for POST stats submissions.
859 # placeholder for the actual session
862 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
863 "----- END REQUEST BODY -----\n\n")
865 (idfp, status) = verify_request(request)
866 (game_meta, raw_players, raw_teams) = parse_stats_submission(request.body)
867 revision = game_meta.get('R', 'unknown')
868 duration = game_meta.get('D', None)
870 # only players present at the end of the match are eligible for stats
871 raw_players = filter(played_in_game, raw_players)
873 do_precondition_checks(request, game_meta, raw_players)
875 # the "duel" gametype is fake
876 if len(raw_players) == 2 \
877 and num_real_players(raw_players) == 2 \
878 and game_meta['G'] == 'dm':
879 game_meta['G'] = 'duel'
881 #----------------------------------------------------------------------
882 # Actual setup (inserts/updates) below here
883 #----------------------------------------------------------------------
884 session = DBSession()
886 game_type_cd = game_meta['G']
888 # All game types create Game, Server, Map, and Player records
890 server = get_or_create_server(
893 name = game_meta['S'],
895 ip_addr = get_remote_addr(request),
896 port = game_meta.get('U', None),
897 impure_cvars = game_meta.get('C', 0))
899 gmap = get_or_create_map(
901 name = game_meta['M'])
905 start_dt = datetime.datetime.utcnow(),
906 server_id = server.server_id,
907 game_type_cd = game_type_cd,
908 map_id = gmap.map_id,
909 match_id = game_meta['I'],
911 mod = game_meta.get('O', None))
913 # keep track of the players we've seen
917 for events in raw_players:
918 player = get_or_create_player(
920 hashkey = events['P'],
921 nick = events.get('n', None))
923 pgstat = create_game_stat(session, game_meta, game, server,
924 gmap, player, events)
925 pgstats.append(pgstat)
927 if player.player_id > 1:
928 anticheats = create_anticheats(session, pgstat, game, player, events)
930 if player.player_id > 2:
931 player_ids.append(player.player_id)
932 hashkeys[player.player_id] = events['P']
934 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
935 pwstats = create_weapon_stats(session, game_meta, game, player,
938 # store them on games for easy access
939 game.players = player_ids
941 for events in raw_teams:
943 teamstat = create_team_stat(session, game, events)
944 except Exception as e:
947 if should_do_elos(game_type_cd):
948 ep = EloProcessor(session, game, pgstats)
952 log.debug('Success! Stats recorded.')
954 # ranks are fetched after we've done the "real" processing
955 ranks = get_ranks(session, player_ids, game_type_cd)
957 # plain text response
958 request.response.content_type = 'text/plain'
961 "now" : timegm(datetime.datetime.utcnow().timetuple()),
965 "player_ids" : player_ids,
966 "hashkeys" : hashkeys,
971 except Exception as e: