4 import pyramid.httpexceptions
7 import sqlalchemy.sql.expression as expr
8 from calendar import timegm
9 from pyramid.response import Response
10 from sqlalchemy import Sequence
11 from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
12 from xonstat.elo import EloProcessor
13 from xonstat.models import *
14 from xonstat.util import strip_colors, qfont_decode, verify_request, weapon_map
17 log = logging.getLogger(__name__)
20 def parse_stats_submission(body):
22 Parses the POST request body for a stats submission
24 # storage vars for the request body
30 # we're not in either stanza to start
33 for line in body.split('\n'):
35 (key, value) = line.strip().split(' ', 1)
37 # Server (S) and Nick (n) fields can have international characters.
39 value = unicode(value, 'utf-8')
41 if key not in 'P' 'Q' 'n' 'e' 't' 'i':
42 game_meta[key] = value
44 if key == 'Q' or key == 'P':
45 #log.debug('Found a {0}'.format(key))
46 #log.debug('in_Q: {0}'.format(in_Q))
47 #log.debug('in_P: {0}'.format(in_P))
48 #log.debug('events: {0}'.format(events))
50 # check where we were before and append events accordingly
51 if in_Q and len(events) > 0:
52 #log.debug('creating a team (Q) entry')
55 elif in_P and len(events) > 0:
56 #log.debug('creating a player (P) entry')
57 players.append(events)
61 #log.debug('key == P')
65 #log.debug('key == Q')
72 (subkey, subvalue) = value.split(' ', 1)
73 events[subkey] = subvalue
79 # no key/value pair - move on to the next line
82 # add the last entity we were working on
83 if in_P and len(events) > 0:
84 players.append(events)
85 elif in_Q and len(events) > 0:
88 return (game_meta, players, teams)
91 def is_blank_game(gametype, players):
92 """Determine if this is a blank game or not. A blank game is either:
94 1) a match that ended in the warmup stage, where accuracy events are not
95 present (for non-CTS games)
97 2) a match in which no player made a positive or negative score AND was
100 ... or for CTS, which doesn't record accuracy events
102 1) a match in which no player made a fastest lap AND was
105 ... or for NB, in which not all maps have weapons
107 1) a match in which no player made a positive or negative score
109 r = re.compile(r'acc-.*-cnt-fired')
110 flg_nonzero_score = False
111 flg_acc_events = False
112 flg_fastest_lap = False
114 for events in players:
115 if is_real_player(events) and played_in_game(events):
116 for (key,value) in events.items():
117 if key == 'scoreboard-score' and value != 0:
118 flg_nonzero_score = True
120 flg_acc_events = True
121 if key == 'scoreboard-fastest':
122 flg_fastest_lap = True
124 if gametype == 'cts':
125 return not flg_fastest_lap
126 elif gametype == 'nb':
127 return not flg_nonzero_score
129 return not (flg_nonzero_score and flg_acc_events)
132 def get_remote_addr(request):
133 """Get the Xonotic server's IP address"""
134 if 'X-Forwarded-For' in request.headers:
135 return request.headers['X-Forwarded-For']
137 return request.remote_addr
140 def is_supported_gametype(gametype, version):
141 """Whether a gametype is supported or not"""
144 # if the type can be supported, but with version constraints, uncomment
145 # here and add the restriction for a specific version below
146 supported_game_types = (
164 if gametype in supported_game_types:
169 # some game types were buggy before revisions, thus this additional filter
170 if gametype == 'ca' and version <= 5:
176 def do_precondition_checks(request, game_meta, raw_players):
177 """Precondition checks for ALL gametypes.
178 These do not require a database connection."""
179 if not has_required_metadata(game_meta):
180 log.debug("ERROR: Required game meta missing")
181 raise pyramid.httpexceptions.HTTPUnprocessableEntity("Missing game meta")
184 version = int(game_meta['V'])
186 log.debug("ERROR: Required game meta invalid")
187 raise pyramid.httpexceptions.HTTPUnprocessableEntity("Invalid game meta")
189 if not is_supported_gametype(game_meta['G'], version):
190 log.debug("ERROR: Unsupported gametype")
191 raise pyramid.httpexceptions.HTTPOk("OK")
193 if not has_minimum_real_players(request.registry.settings, raw_players):
194 log.debug("ERROR: Not enough real players")
195 raise pyramid.httpexceptions.HTTPOk("OK")
197 if is_blank_game(game_meta['G'], raw_players):
198 log.debug("ERROR: Blank game")
199 raise pyramid.httpexceptions.HTTPOk("OK")
202 def is_real_player(events):
204 Determines if a given set of events correspond with a non-bot
206 if not events['P'].startswith('bot'):
212 def played_in_game(events):
214 Determines if a given set of player events correspond with a player who
215 played in the game (matches 1 and scoreboardvalid 1)
217 if 'matches' in events and 'scoreboardvalid' in events:
223 def num_real_players(player_events):
225 Returns the number of real players (those who played
226 and are on the scoreboard).
230 for events in player_events:
231 if is_real_player(events) and played_in_game(events):
237 def has_minimum_real_players(settings, player_events):
239 Determines if the collection of player events has enough "real" players
240 to store in the database. The minimum setting comes from the config file
241 under the setting xonstat.minimum_real_players.
243 flg_has_min_real_players = True
246 minimum_required_players = int(
247 settings['xonstat.minimum_required_players'])
249 minimum_required_players = 2
251 real_players = num_real_players(player_events)
253 if real_players < minimum_required_players:
254 flg_has_min_real_players = False
256 return flg_has_min_real_players
259 def has_required_metadata(metadata):
261 Determines if a give set of metadata has enough data to create a game,
262 server, and map with.
264 flg_has_req_metadata = True
266 if 'G' not in metadata or\
267 'M' not in metadata or\
268 'I' not in metadata or\
270 flg_has_req_metadata = False
272 return flg_has_req_metadata
275 def should_do_weapon_stats(game_type_cd):
276 """True of the game type should record weapon stats. False otherwise."""
277 if game_type_cd in 'cts':
283 def should_do_elos(game_type_cd):
284 """True of the game type should process Elos. False otherwise."""
285 elo_game_types = ('duel', 'dm', 'ca', 'ctf', 'tdm', 'ka', 'ft')
287 if game_type_cd in elo_game_types:
293 def register_new_nick(session, player, new_nick):
295 Change the player record's nick to the newly found nick. Store the old
296 nick in the player_nicks table for that player.
298 session - SQLAlchemy database session factory
299 player - player record whose nick is changing
300 new_nick - the new nickname
302 # see if that nick already exists
303 stripped_nick = strip_colors(qfont_decode(player.nick))
305 player_nick = session.query(PlayerNick).filter_by(
306 player_id=player.player_id, stripped_nick=stripped_nick).one()
307 except NoResultFound, e:
308 # player_id/stripped_nick not found, create one
309 # but we don't store "Anonymous Player #N"
310 if not re.search('^Anonymous Player #\d+$', player.nick):
311 player_nick = PlayerNick()
312 player_nick.player_id = player.player_id
313 player_nick.stripped_nick = stripped_nick
314 player_nick.nick = player.nick
315 session.add(player_nick)
317 # We change to the new nick regardless
318 player.nick = new_nick
319 player.stripped_nick = strip_colors(qfont_decode(new_nick))
323 def update_fastest_cap(session, player_id, game_id, map_id, captime, mod):
325 Check the fastest cap time for the player and map. If there isn't
326 one, insert one. If there is, check if the passed time is faster.
329 # we don't record fastest cap times for bots or anonymous players
333 # see if a cap entry exists already
334 # then check to see if the new captime is faster
336 cur_fastest_cap = session.query(PlayerCaptime).filter_by(
337 player_id=player_id, map_id=map_id, mod=mod).one()
339 # current captime is faster, so update
340 if captime < cur_fastest_cap.fastest_cap:
341 cur_fastest_cap.fastest_cap = captime
342 cur_fastest_cap.game_id = game_id
343 cur_fastest_cap.create_dt = datetime.datetime.utcnow()
344 session.add(cur_fastest_cap)
346 except NoResultFound, e:
347 # none exists, so insert
348 cur_fastest_cap = PlayerCaptime(player_id, game_id, map_id, captime,
350 session.add(cur_fastest_cap)
354 def get_or_create_server(session, name, hashkey, ip_addr, revision, port,
357 Find a server by name or create one if not found. Parameters:
359 session - SQLAlchemy database session factory
360 name - server name of the server to be found or created
361 hashkey - server hashkey
362 ip_addr - the IP address of the server
363 revision - the xonotic revision number
364 port - the port number of the server
365 impure_cvars - the number of impure cvar changes
375 impure_cvars = int(impure_cvars)
379 # finding by hashkey is preferred, but if not we will fall
380 # back to using name only, which can result in dupes
381 if hashkey is not None:
382 servers = session.query(Server).\
383 filter_by(hashkey=hashkey).\
384 order_by(expr.desc(Server.create_dt)).limit(1).all()
388 log.debug("Found existing server {0} by hashkey ({1})".format(
389 server.server_id, server.hashkey))
391 servers = session.query(Server).\
392 filter_by(name=name).\
393 order_by(expr.desc(Server.create_dt)).limit(1).all()
397 log.debug("Found existing server {0} by name".format(server.server_id))
399 # still haven't found a server by hashkey or name, so we need to create one
401 server = Server(name=name, hashkey=hashkey)
404 log.debug("Created server {0} with hashkey {1}".format(
405 server.server_id, server.hashkey))
407 # detect changed fields
408 if server.name != name:
412 if server.hashkey != hashkey:
413 server.hashkey = hashkey
416 if server.ip_addr != ip_addr:
417 server.ip_addr = ip_addr
420 if server.port != port:
424 if server.revision != revision:
425 server.revision = revision
428 if server.impure_cvars != impure_cvars:
429 server.impure_cvars = impure_cvars
431 server.pure_ind = False
433 server.pure_ind = True
439 def get_or_create_map(session=None, name=None):
441 Find a map by name or create one if not found. Parameters:
443 session - SQLAlchemy database session factory
444 name - map name of the map to be found or created
447 # find one by the name, if it exists
448 gmap = session.query(Map).filter_by(name=name).one()
449 log.debug("Found map id {0}: {1}".format(gmap.map_id,
451 except NoResultFound, e:
452 gmap = Map(name=name)
455 log.debug("Created map id {0}: {1}".format(gmap.map_id,
457 except MultipleResultsFound, e:
458 # multiple found, so use the first one but warn
460 gmaps = session.query(Map).filter_by(name=name).order_by(
463 log.debug("Found map id {0}: {1} but found \
464 multiple".format(gmap.map_id, gmap.name))
469 def create_game(session, start_dt, game_type_cd, server_id, map_id,
470 match_id, duration, mod, winner=None):
472 Creates a game. Parameters:
474 session - SQLAlchemy database session factory
475 start_dt - when the game started (datetime object)
476 game_type_cd - the game type of the game being played
477 server_id - server identifier of the server hosting the game
478 map_id - map on which the game was played
479 winner - the team id of the team that won
480 duration - how long the game lasted
481 mod - mods in use during the game
483 seq = Sequence('games_game_id_seq')
484 game_id = session.execute(seq)
485 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
486 server_id=server_id, map_id=map_id, winner=winner)
487 game.match_id = match_id
491 game.duration = datetime.timedelta(seconds=int(round(float(duration))))
496 session.query(Game).filter(Game.server_id==server_id).\
497 filter(Game.match_id==match_id).one()
499 log.debug("Error: game with same server and match_id found! Ignoring.")
501 # if a game under the same server and match_id found,
502 # this is a duplicate game and can be ignored
503 raise pyramid.httpexceptions.HTTPOk('OK')
504 except NoResultFound, e:
505 # server_id/match_id combination not found. game is ok to insert
508 log.debug("Created game id {0} on server {1}, map {2} at \
509 {3}".format(game.game_id,
510 server_id, map_id, start_dt))
515 def get_or_create_player(session=None, hashkey=None, nick=None):
517 Finds a player by hashkey or creates a new one (along with a
518 corresponding hashkey entry. Parameters:
520 session - SQLAlchemy database session factory
521 hashkey - hashkey of the player to be found or created
522 nick - nick of the player (in case of a first time create)
525 if re.search('^bot#\d+', hashkey):
526 player = session.query(Player).filter_by(player_id=1).one()
527 # if we have an untracked player
528 elif re.search('^player#\d+$', hashkey):
529 player = session.query(Player).filter_by(player_id=2).one()
530 # else it is a tracked player
532 # see if the player is already in the database
533 # if not, create one and the hashkey along with it
535 hk = session.query(Hashkey).filter_by(
536 hashkey=hashkey).one()
537 player = session.query(Player).filter_by(
538 player_id=hk.player_id).one()
539 log.debug("Found existing player {0} with hashkey {1}".format(
540 player.player_id, hashkey))
546 # if nick is given to us, use it. If not, use "Anonymous Player"
547 # with a suffix added for uniqueness.
549 player.nick = nick[:128]
550 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
552 player.nick = "Anonymous Player #{0}".format(player.player_id)
553 player.stripped_nick = player.nick
555 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
557 log.debug("Created player {0} ({2}) with hashkey {1}".format(
558 player.player_id, hashkey, player.nick.encode('utf-8')))
563 def create_default_game_stat(session, game_type_cd):
564 """Creates a blanked-out pgstat record for the given game type"""
566 # this is what we have to do to get partitioned records in - grab the
567 # sequence value first, then insert using the explicit ID (vs autogenerate)
568 seq = Sequence('player_game_stats_player_game_stat_id_seq')
569 pgstat_id = session.execute(seq)
570 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
571 create_dt=datetime.datetime.utcnow())
573 if game_type_cd == 'as':
574 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
576 if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
577 pgstat.kills = pgstat.deaths = pgstat.suicides = 0
579 if game_type_cd == 'cq':
580 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
583 if game_type_cd == 'ctf':
584 pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
585 pgstat.returns = pgstat.carrier_frags = 0
587 if game_type_cd == 'cts':
590 if game_type_cd == 'dom':
591 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
594 if game_type_cd == 'ft':
595 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
597 if game_type_cd == 'ka':
598 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
599 pgstat.carrier_frags = 0
600 pgstat.time = datetime.timedelta(seconds=0)
602 if game_type_cd == 'kh':
603 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
604 pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
605 pgstat.carrier_frags = 0
607 if game_type_cd == 'lms':
608 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
610 if game_type_cd == 'nb':
611 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
614 if game_type_cd == 'rc':
615 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
620 def create_game_stat(session, game_meta, game, server, gmap, player, events):
621 """Game stats handler for all game types"""
623 game_type_cd = game.game_type_cd
625 pgstat = create_default_game_stat(session, game_type_cd)
627 # these fields should be on every pgstat record
628 pgstat.game_id = game.game_id
629 pgstat.player_id = player.player_id
630 pgstat.nick = events.get('n', 'Anonymous Player')[:128]
631 pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
632 pgstat.score = int(round(float(events.get('scoreboard-score', 0))))
633 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
634 pgstat.rank = int(events.get('rank', None))
635 pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
637 if pgstat.nick != player.nick \
638 and player.player_id > 2 \
639 and pgstat.nick != 'Anonymous Player':
640 register_new_nick(session, player, pgstat.nick)
644 # gametype-specific stuff is handled here. if passed to us, we store it
645 for (key,value) in events.items():
646 if key == 'wins': wins = True
647 if key == 't': pgstat.team = int(value)
649 if key == 'scoreboard-drops': pgstat.drops = int(value)
650 if key == 'scoreboard-returns': pgstat.returns = int(value)
651 if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
652 if key == 'scoreboard-pickups': pgstat.pickups = int(value)
653 if key == 'scoreboard-caps': pgstat.captures = int(value)
654 if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
655 if key == 'scoreboard-deaths': pgstat.deaths = int(value)
656 if key == 'scoreboard-kills': pgstat.kills = int(value)
657 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
658 if key == 'scoreboard-objectives': pgstat.collects = int(value)
659 if key == 'scoreboard-captured': pgstat.captures = int(value)
660 if key == 'scoreboard-released': pgstat.drops = int(value)
661 if key == 'scoreboard-fastest':
662 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
663 if key == 'scoreboard-takes': pgstat.pickups = int(value)
664 if key == 'scoreboard-ticks': pgstat.drops = int(value)
665 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
666 if key == 'scoreboard-bctime':
667 pgstat.time = datetime.timedelta(seconds=int(value))
668 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
669 if key == 'scoreboard-losses': pgstat.drops = int(value)
670 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
671 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
672 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
673 if key == 'scoreboard-lives': pgstat.lives = int(value)
674 if key == 'scoreboard-goals': pgstat.captures = int(value)
675 if key == 'scoreboard-faults': pgstat.drops = int(value)
676 if key == 'scoreboard-laps': pgstat.laps = int(value)
678 if key == 'avglatency': pgstat.avg_latency = float(value)
679 if key == 'scoreboard-captime':
680 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
681 if game.game_type_cd == 'ctf':
682 update_fastest_cap(session, player.player_id, game.game_id,
683 gmap.map_id, pgstat.fastest, game.mod)
685 # there is no "winning team" field, so we have to derive it
686 if wins and pgstat.team is not None and game.winner is None:
687 game.winner = pgstat.team
695 def create_anticheats(session, pgstat, game, player, events):
696 """Anticheats handler for all game types"""
700 # all anticheat events are prefixed by "anticheat"
701 for (key,value) in events.items():
702 if key.startswith("anticheat"):
704 ac = PlayerGameAnticheat(
710 anticheats.append(ac)
712 except Exception as e:
713 log.debug("Could not parse value for key %s. Ignoring." % key)
718 def create_default_team_stat(session, game_type_cd):
719 """Creates a blanked-out teamstat record for the given game type"""
721 # this is what we have to do to get partitioned records in - grab the
722 # sequence value first, then insert using the explicit ID (vs autogenerate)
723 seq = Sequence('team_game_stats_team_game_stat_id_seq')
724 teamstat_id = session.execute(seq)
725 teamstat = TeamGameStat(team_game_stat_id=teamstat_id,
726 create_dt=datetime.datetime.utcnow())
728 # all team game modes have a score, so we'll zero that out always
731 if game_type_cd in 'ca' 'ft' 'lms' 'ka':
734 if game_type_cd == 'ctf':
740 def create_team_stat(session, game, events):
741 """Team stats handler for all game types"""
744 teamstat = create_default_team_stat(session, game.game_type_cd)
745 teamstat.game_id = game.game_id
747 # we should have a team ID if we have a 'Q' event
748 if re.match(r'^team#\d+$', events.get('Q', '')):
749 team = int(events.get('Q').replace('team#', ''))
752 # gametype-specific stuff is handled here. if passed to us, we store it
753 for (key,value) in events.items():
754 if key == 'scoreboard-score': teamstat.score = int(round(float(value)))
755 if key == 'scoreboard-caps': teamstat.caps = int(value)
756 if key == 'scoreboard-goals': teamstat.caps = int(value)
757 if key == 'scoreboard-rounds': teamstat.rounds = int(value)
759 session.add(teamstat)
760 except Exception as e:
766 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
767 """Weapon stats handler for all game types"""
770 # Version 1 of stats submissions doubled the data sent.
771 # To counteract this we divide the data by 2 only for
772 # POSTs coming from version 1.
774 version = int(game_meta['V'])
777 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
783 for (key,value) in events.items():
784 matched = re.search("acc-(.*?)-cnt-fired", key)
786 weapon_cd = matched.group(1)
788 # Weapon names changed for 0.8. We'll convert the old
789 # ones to use the new scheme as well.
790 mapped_weapon_cd = weapon_map.get(weapon_cd, weapon_cd)
792 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
793 pwstat_id = session.execute(seq)
794 pwstat = PlayerWeaponStat()
795 pwstat.player_weapon_stats_id = pwstat_id
796 pwstat.player_id = player.player_id
797 pwstat.game_id = game.game_id
798 pwstat.player_game_stat_id = pgstat.player_game_stat_id
799 pwstat.weapon_cd = mapped_weapon_cd
802 pwstat.nick = events['n']
804 pwstat.nick = events['P']
806 if 'acc-' + weapon_cd + '-cnt-fired' in events:
807 pwstat.fired = int(round(float(
808 events['acc-' + weapon_cd + '-cnt-fired'])))
809 if 'acc-' + weapon_cd + '-fired' in events:
810 pwstat.max = int(round(float(
811 events['acc-' + weapon_cd + '-fired'])))
812 if 'acc-' + weapon_cd + '-cnt-hit' in events:
813 pwstat.hit = int(round(float(
814 events['acc-' + weapon_cd + '-cnt-hit'])))
815 if 'acc-' + weapon_cd + '-hit' in events:
816 pwstat.actual = int(round(float(
817 events['acc-' + weapon_cd + '-hit'])))
818 if 'acc-' + weapon_cd + '-frags' in events:
819 pwstat.frags = int(round(float(
820 events['acc-' + weapon_cd + '-frags'])))
823 pwstat.fired = pwstat.fired/2
824 pwstat.max = pwstat.max/2
825 pwstat.hit = pwstat.hit/2
826 pwstat.actual = pwstat.actual/2
827 pwstat.frags = pwstat.frags/2
830 pwstats.append(pwstat)
835 def get_ranks(session, player_ids, game_type_cd):
837 Gets the rank entries for all players in the given list, returning a dict
838 of player_id -> PlayerRank instance. The rank entry corresponds to the
839 game type of the parameter passed in as well.
842 for pr in session.query(PlayerRank).\
843 filter(PlayerRank.player_id.in_(player_ids)).\
844 filter(PlayerRank.game_type_cd == game_type_cd).\
846 ranks[pr.player_id] = pr
851 def submit_stats(request):
853 Entry handler for POST stats submissions.
856 # placeholder for the actual session
859 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
860 "----- END REQUEST BODY -----\n\n")
862 (idfp, status) = verify_request(request)
863 (game_meta, raw_players, raw_teams) = parse_stats_submission(request.body)
864 revision = game_meta.get('R', 'unknown')
865 duration = game_meta.get('D', None)
867 # only players present at the end of the match are eligible for stats
868 raw_players = filter(played_in_game, raw_players)
870 do_precondition_checks(request, game_meta, raw_players)
872 # the "duel" gametype is fake
873 if len(raw_players) == 2 \
874 and num_real_players(raw_players) == 2 \
875 and game_meta['G'] == 'dm':
876 game_meta['G'] = 'duel'
878 #----------------------------------------------------------------------
879 # Actual setup (inserts/updates) below here
880 #----------------------------------------------------------------------
881 session = DBSession()
883 game_type_cd = game_meta['G']
885 # All game types create Game, Server, Map, and Player records
887 server = get_or_create_server(
890 name = game_meta['S'],
892 ip_addr = get_remote_addr(request),
893 port = game_meta.get('U', None),
894 impure_cvars = game_meta.get('C', 0))
896 gmap = get_or_create_map(
898 name = game_meta['M'])
902 start_dt = datetime.datetime.utcnow(),
903 server_id = server.server_id,
904 game_type_cd = game_type_cd,
905 map_id = gmap.map_id,
906 match_id = game_meta['I'],
908 mod = game_meta.get('O', None))
910 # keep track of the players we've seen
914 for events in raw_players:
915 player = get_or_create_player(
917 hashkey = events['P'],
918 nick = events.get('n', None))
920 pgstat = create_game_stat(session, game_meta, game, server,
921 gmap, player, events)
922 pgstats.append(pgstat)
924 if player.player_id > 1:
925 anticheats = create_anticheats(session, pgstat, game, player, events)
927 if player.player_id > 2:
928 player_ids.append(player.player_id)
929 hashkeys[player.player_id] = events['P']
931 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
932 pwstats = create_weapon_stats(session, game_meta, game, player,
935 # store them on games for easy access
936 game.players = player_ids
938 for events in raw_teams:
940 teamstat = create_team_stat(session, game, events)
941 except Exception as e:
944 if should_do_elos(game_type_cd):
945 ep = EloProcessor(session, game, pgstats)
949 log.debug('Success! Stats recorded.')
951 # ranks are fetched after we've done the "real" processing
952 ranks = get_ranks(session, player_ids, game_type_cd)
954 # plain text response
955 request.response.content_type = 'text/plain'
958 "now" : timegm(datetime.datetime.utcnow().timetuple()),
962 "player_ids" : player_ids,
963 "hashkeys" : hashkeys,
968 except Exception as e: