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
490 # There is some drift between start_dt (provided by app) and create_dt
491 # (default in the database), so we'll make them the same until this is
493 game.create_dt = start_dt
496 game.duration = datetime.timedelta(seconds=int(round(float(duration))))
501 session.query(Game).filter(Game.server_id==server_id).\
502 filter(Game.match_id==match_id).one()
504 log.debug("Error: game with same server and match_id found! Ignoring.")
506 # if a game under the same server and match_id found,
507 # this is a duplicate game and can be ignored
508 raise pyramid.httpexceptions.HTTPOk('OK')
509 except NoResultFound, e:
510 # server_id/match_id combination not found. game is ok to insert
513 log.debug("Created game id {0} on server {1}, map {2} at \
514 {3}".format(game.game_id,
515 server_id, map_id, start_dt))
520 def get_or_create_player(session=None, hashkey=None, nick=None):
522 Finds a player by hashkey or creates a new one (along with a
523 corresponding hashkey entry. Parameters:
525 session - SQLAlchemy database session factory
526 hashkey - hashkey of the player to be found or created
527 nick - nick of the player (in case of a first time create)
530 if re.search('^bot#\d+', hashkey):
531 player = session.query(Player).filter_by(player_id=1).one()
532 # if we have an untracked player
533 elif re.search('^player#\d+$', hashkey):
534 player = session.query(Player).filter_by(player_id=2).one()
535 # else it is a tracked player
537 # see if the player is already in the database
538 # if not, create one and the hashkey along with it
540 hk = session.query(Hashkey).filter_by(
541 hashkey=hashkey).one()
542 player = session.query(Player).filter_by(
543 player_id=hk.player_id).one()
544 log.debug("Found existing player {0} with hashkey {1}".format(
545 player.player_id, hashkey))
551 # if nick is given to us, use it. If not, use "Anonymous Player"
552 # with a suffix added for uniqueness.
554 player.nick = nick[:128]
555 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
557 player.nick = "Anonymous Player #{0}".format(player.player_id)
558 player.stripped_nick = player.nick
560 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
562 log.debug("Created player {0} ({2}) with hashkey {1}".format(
563 player.player_id, hashkey, player.nick.encode('utf-8')))
568 def create_default_game_stat(session, game_type_cd):
569 """Creates a blanked-out pgstat record for the given game type"""
571 # this is what we have to do to get partitioned records in - grab the
572 # sequence value first, then insert using the explicit ID (vs autogenerate)
573 seq = Sequence('player_game_stats_player_game_stat_id_seq')
574 pgstat_id = session.execute(seq)
575 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
576 create_dt=datetime.datetime.utcnow())
578 if game_type_cd == 'as':
579 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
581 if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
582 pgstat.kills = pgstat.deaths = pgstat.suicides = 0
584 if game_type_cd == 'cq':
585 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
588 if game_type_cd == 'ctf':
589 pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
590 pgstat.returns = pgstat.carrier_frags = 0
592 if game_type_cd == 'cts':
595 if game_type_cd == 'dom':
596 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
599 if game_type_cd == 'ft':
600 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
602 if game_type_cd == 'ka':
603 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
604 pgstat.carrier_frags = 0
605 pgstat.time = datetime.timedelta(seconds=0)
607 if game_type_cd == 'kh':
608 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
609 pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
610 pgstat.carrier_frags = 0
612 if game_type_cd == 'lms':
613 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
615 if game_type_cd == 'nb':
616 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
619 if game_type_cd == 'rc':
620 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
625 def create_game_stat(session, game_meta, game, server, gmap, player, events):
626 """Game stats handler for all game types"""
628 game_type_cd = game.game_type_cd
630 pgstat = create_default_game_stat(session, game_type_cd)
632 # these fields should be on every pgstat record
633 pgstat.game_id = game.game_id
634 pgstat.player_id = player.player_id
635 pgstat.nick = events.get('n', 'Anonymous Player')[:128]
636 pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
637 pgstat.score = int(round(float(events.get('scoreboard-score', 0))))
638 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
639 pgstat.rank = int(events.get('rank', None))
640 pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
642 if pgstat.nick != player.nick \
643 and player.player_id > 2 \
644 and pgstat.nick != 'Anonymous Player':
645 register_new_nick(session, player, pgstat.nick)
649 # gametype-specific stuff is handled here. if passed to us, we store it
650 for (key,value) in events.items():
651 if key == 'wins': wins = True
652 if key == 't': pgstat.team = int(value)
654 if key == 'scoreboard-drops': pgstat.drops = int(value)
655 if key == 'scoreboard-returns': pgstat.returns = int(value)
656 if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
657 if key == 'scoreboard-pickups': pgstat.pickups = int(value)
658 if key == 'scoreboard-caps': pgstat.captures = int(value)
659 if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
660 if key == 'scoreboard-deaths': pgstat.deaths = int(value)
661 if key == 'scoreboard-kills': pgstat.kills = int(value)
662 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
663 if key == 'scoreboard-objectives': pgstat.collects = int(value)
664 if key == 'scoreboard-captured': pgstat.captures = int(value)
665 if key == 'scoreboard-released': pgstat.drops = int(value)
666 if key == 'scoreboard-fastest':
667 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
668 if key == 'scoreboard-takes': pgstat.pickups = int(value)
669 if key == 'scoreboard-ticks': pgstat.drops = int(value)
670 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
671 if key == 'scoreboard-bctime':
672 pgstat.time = datetime.timedelta(seconds=int(value))
673 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
674 if key == 'scoreboard-losses': pgstat.drops = int(value)
675 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
676 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
677 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
678 if key == 'scoreboard-lives': pgstat.lives = int(value)
679 if key == 'scoreboard-goals': pgstat.captures = int(value)
680 if key == 'scoreboard-faults': pgstat.drops = int(value)
681 if key == 'scoreboard-laps': pgstat.laps = int(value)
683 if key == 'avglatency': pgstat.avg_latency = float(value)
684 if key == 'scoreboard-captime':
685 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
686 if game.game_type_cd == 'ctf':
687 update_fastest_cap(session, player.player_id, game.game_id,
688 gmap.map_id, pgstat.fastest, game.mod)
690 # there is no "winning team" field, so we have to derive it
691 if wins and pgstat.team is not None and game.winner is None:
692 game.winner = pgstat.team
700 def create_anticheats(session, pgstat, game, player, events):
701 """Anticheats handler for all game types"""
705 # all anticheat events are prefixed by "anticheat"
706 for (key,value) in events.items():
707 if key.startswith("anticheat"):
709 ac = PlayerGameAnticheat(
715 anticheats.append(ac)
717 except Exception as e:
718 log.debug("Could not parse value for key %s. Ignoring." % key)
723 def create_default_team_stat(session, game_type_cd):
724 """Creates a blanked-out teamstat record for the given game type"""
726 # this is what we have to do to get partitioned records in - grab the
727 # sequence value first, then insert using the explicit ID (vs autogenerate)
728 seq = Sequence('team_game_stats_team_game_stat_id_seq')
729 teamstat_id = session.execute(seq)
730 teamstat = TeamGameStat(team_game_stat_id=teamstat_id,
731 create_dt=datetime.datetime.utcnow())
733 # all team game modes have a score, so we'll zero that out always
736 if game_type_cd in 'ca' 'ft' 'lms' 'ka':
739 if game_type_cd == 'ctf':
745 def create_team_stat(session, game, events):
746 """Team stats handler for all game types"""
749 teamstat = create_default_team_stat(session, game.game_type_cd)
750 teamstat.game_id = game.game_id
752 # we should have a team ID if we have a 'Q' event
753 if re.match(r'^team#\d+$', events.get('Q', '')):
754 team = int(events.get('Q').replace('team#', ''))
757 # gametype-specific stuff is handled here. if passed to us, we store it
758 for (key,value) in events.items():
759 if key == 'scoreboard-score': teamstat.score = int(round(float(value)))
760 if key == 'scoreboard-caps': teamstat.caps = int(value)
761 if key == 'scoreboard-goals': teamstat.caps = int(value)
762 if key == 'scoreboard-rounds': teamstat.rounds = int(value)
764 session.add(teamstat)
765 except Exception as e:
771 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
772 """Weapon stats handler for all game types"""
775 # Version 1 of stats submissions doubled the data sent.
776 # To counteract this we divide the data by 2 only for
777 # POSTs coming from version 1.
779 version = int(game_meta['V'])
782 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
788 for (key,value) in events.items():
789 matched = re.search("acc-(.*?)-cnt-fired", key)
791 weapon_cd = matched.group(1)
793 # Weapon names changed for 0.8. We'll convert the old
794 # ones to use the new scheme as well.
795 mapped_weapon_cd = weapon_map.get(weapon_cd, weapon_cd)
797 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
798 pwstat_id = session.execute(seq)
799 pwstat = PlayerWeaponStat()
800 pwstat.player_weapon_stats_id = pwstat_id
801 pwstat.player_id = player.player_id
802 pwstat.game_id = game.game_id
803 pwstat.player_game_stat_id = pgstat.player_game_stat_id
804 pwstat.weapon_cd = mapped_weapon_cd
807 pwstat.nick = events['n']
809 pwstat.nick = events['P']
811 if 'acc-' + weapon_cd + '-cnt-fired' in events:
812 pwstat.fired = int(round(float(
813 events['acc-' + weapon_cd + '-cnt-fired'])))
814 if 'acc-' + weapon_cd + '-fired' in events:
815 pwstat.max = int(round(float(
816 events['acc-' + weapon_cd + '-fired'])))
817 if 'acc-' + weapon_cd + '-cnt-hit' in events:
818 pwstat.hit = int(round(float(
819 events['acc-' + weapon_cd + '-cnt-hit'])))
820 if 'acc-' + weapon_cd + '-hit' in events:
821 pwstat.actual = int(round(float(
822 events['acc-' + weapon_cd + '-hit'])))
823 if 'acc-' + weapon_cd + '-frags' in events:
824 pwstat.frags = int(round(float(
825 events['acc-' + weapon_cd + '-frags'])))
828 pwstat.fired = pwstat.fired/2
829 pwstat.max = pwstat.max/2
830 pwstat.hit = pwstat.hit/2
831 pwstat.actual = pwstat.actual/2
832 pwstat.frags = pwstat.frags/2
835 pwstats.append(pwstat)
840 def get_ranks(session, player_ids, game_type_cd):
842 Gets the rank entries for all players in the given list, returning a dict
843 of player_id -> PlayerRank instance. The rank entry corresponds to the
844 game type of the parameter passed in as well.
847 for pr in session.query(PlayerRank).\
848 filter(PlayerRank.player_id.in_(player_ids)).\
849 filter(PlayerRank.game_type_cd == game_type_cd).\
851 ranks[pr.player_id] = pr
856 def submit_stats(request):
858 Entry handler for POST stats submissions.
861 # placeholder for the actual session
864 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
865 "----- END REQUEST BODY -----\n\n")
867 (idfp, status) = verify_request(request)
868 (game_meta, raw_players, raw_teams) = parse_stats_submission(request.body)
869 revision = game_meta.get('R', 'unknown')
870 duration = game_meta.get('D', None)
872 # only players present at the end of the match are eligible for stats
873 raw_players = filter(played_in_game, raw_players)
875 do_precondition_checks(request, game_meta, raw_players)
877 # the "duel" gametype is fake
878 if len(raw_players) == 2 \
879 and num_real_players(raw_players) == 2 \
880 and game_meta['G'] == 'dm':
881 game_meta['G'] = 'duel'
883 #----------------------------------------------------------------------
884 # Actual setup (inserts/updates) below here
885 #----------------------------------------------------------------------
886 session = DBSession()
888 game_type_cd = game_meta['G']
890 # All game types create Game, Server, Map, and Player records
892 server = get_or_create_server(
895 name = game_meta['S'],
897 ip_addr = get_remote_addr(request),
898 port = game_meta.get('U', None),
899 impure_cvars = game_meta.get('C', 0))
901 gmap = get_or_create_map(
903 name = game_meta['M'])
907 start_dt = datetime.datetime.utcnow(),
908 server_id = server.server_id,
909 game_type_cd = game_type_cd,
910 map_id = gmap.map_id,
911 match_id = game_meta['I'],
913 mod = game_meta.get('O', None))
915 # keep track of the players we've seen
919 for events in raw_players:
920 player = get_or_create_player(
922 hashkey = events['P'],
923 nick = events.get('n', None))
925 pgstat = create_game_stat(session, game_meta, game, server,
926 gmap, player, events)
927 pgstats.append(pgstat)
929 if player.player_id > 1:
930 anticheats = create_anticheats(session, pgstat, game, player, events)
932 if player.player_id > 2:
933 player_ids.append(player.player_id)
934 hashkeys[player.player_id] = events['P']
936 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
937 pwstats = create_weapon_stats(session, game_meta, game, player,
940 # store them on games for easy access
941 game.players = player_ids
943 for events in raw_teams:
945 teamstat = create_team_stat(session, game, events)
946 except Exception as e:
949 if should_do_elos(game_type_cd):
950 ep = EloProcessor(session, game, pgstats)
954 log.debug('Success! Stats recorded.')
956 # ranks are fetched after we've done the "real" processing
957 ranks = get_ranks(session, player_ids, game_type_cd)
959 # plain text response
960 request.response.content_type = 'text/plain'
963 "now" : timegm(datetime.datetime.utcnow().timetuple()),
967 "player_ids" : player_ids,
968 "hashkeys" : hashkeys,
973 except Exception as e: