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 gametype_elo_eligible(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 update_server(server, name, hashkey, ip_addr, port, revision, impure_cvars):
355 Updates the server in the given DB session, if needed.
357 :param server: The found server instance.
358 :param name: The incoming server name.
359 :param hashkey: The incoming server hashkey.
360 :param ip_addr: The incoming server IP address.
361 :param port: The incoming server port.
362 :param revision: The incoming server revision.
363 :param impure_cvars: The incoming number of impure server cvars.
366 # ensure the two int attributes are actually ints
373 impure_cvars = int(impure_cvars)
378 if name and server.name != name:
381 if hashkey and server.hashkey != hashkey:
382 server.hashkey = hashkey
384 if ip_addr and server.ip_addr != ip_addr:
385 server.ip_addr = ip_addr
387 if port and server.port != port:
390 if revision and server.revision != revision:
391 server.revision = revision
393 if impure_cvars and server.impure_cvars != impure_cvars:
394 server.impure_cvars = impure_cvars
395 server.pure_ind = True if impure_cvars == 0 else False
401 def get_or_create_server(session, name, hashkey, ip_addr, revision, port, impure_cvars):
403 Find a server by name or create one if not found. Parameters:
405 session - SQLAlchemy database session factory
406 name - server name of the server to be found or created
407 hashkey - server hashkey
408 ip_addr - the IP address of the server
409 revision - the xonotic revision number
410 port - the port number of the server
411 impure_cvars - the number of impure cvar changes
413 servers_q = DBSession.query(Server).filter(Server.active_ind)
416 # if the hashkey is provided, we'll use that
417 servers_q = servers_q.filter((Server.name == name) or (Server.hashkey == hashkey))
419 # otherwise, it is just by name
420 servers_q = servers_q.filter(Server.name == name)
422 # order by the hashkey, which means any hashkey match will appear first if there are multiple
423 servers = servers_q.order_by(Server.hashkey, Server.create_dt).all()
425 if len(servers) == 0:
426 server = Server(name=name, hashkey=hashkey)
429 log.debug("Created server {} with hashkey {}.".format(server.server_id, server.hashkey))
432 if len(servers) == 1:
433 log.info("Found existing server {}.".format(server.server_id))
435 elif len(servers) > 1:
436 server_id_list = ", ".join(["{}".format(s.server_id) for s in servers])
437 log.warn("Multiple servers found ({})! Using the first one ({})."
438 .format(server_id_list, server.server_id))
440 if update_server(server, name, hashkey, ip_addr, port, revision, impure_cvars):
446 def get_or_create_map(session=None, name=None):
448 Find a map by name or create one if not found. Parameters:
450 session - SQLAlchemy database session factory
451 name - map name of the map to be found or created
454 # find one by the name, if it exists
455 gmap = session.query(Map).filter_by(name=name).one()
456 log.debug("Found map id {0}: {1}".format(gmap.map_id,
458 except NoResultFound, e:
459 gmap = Map(name=name)
462 log.debug("Created map id {0}: {1}".format(gmap.map_id,
464 except MultipleResultsFound, e:
465 # multiple found, so use the first one but warn
467 gmaps = session.query(Map).filter_by(name=name).order_by(
470 log.debug("Found map id {0}: {1} but found \
471 multiple".format(gmap.map_id, gmap.name))
476 def create_game(session, start_dt, game_type_cd, server_id, map_id,
477 match_id, duration, mod, winner=None):
479 Creates a game. Parameters:
481 session - SQLAlchemy database session factory
482 start_dt - when the game started (datetime object)
483 game_type_cd - the game type of the game being played
484 server_id - server identifier of the server hosting the game
485 map_id - map on which the game was played
486 winner - the team id of the team that won
487 duration - how long the game lasted
488 mod - mods in use during the game
490 seq = Sequence('games_game_id_seq')
491 game_id = session.execute(seq)
492 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
493 server_id=server_id, map_id=map_id, winner=winner)
494 game.match_id = match_id
497 # There is some drift between start_dt (provided by app) and create_dt
498 # (default in the database), so we'll make them the same until this is
500 game.create_dt = start_dt
503 game.duration = datetime.timedelta(seconds=int(round(float(duration))))
508 session.query(Game).filter(Game.server_id==server_id).\
509 filter(Game.match_id==match_id).one()
511 log.debug("Error: game with same server and match_id found! Ignoring.")
513 # if a game under the same server and match_id found,
514 # this is a duplicate game and can be ignored
515 raise pyramid.httpexceptions.HTTPOk('OK')
516 except NoResultFound, e:
517 # server_id/match_id combination not found. game is ok to insert
520 log.debug("Created game id {0} on server {1}, map {2} at \
521 {3}".format(game.game_id,
522 server_id, map_id, start_dt))
527 def get_or_create_player(session=None, hashkey=None, nick=None):
529 Finds a player by hashkey or creates a new one (along with a
530 corresponding hashkey entry. Parameters:
532 session - SQLAlchemy database session factory
533 hashkey - hashkey of the player to be found or created
534 nick - nick of the player (in case of a first time create)
537 if re.search('^bot#\d+', hashkey):
538 player = session.query(Player).filter_by(player_id=1).one()
539 # if we have an untracked player
540 elif re.search('^player#\d+$', hashkey):
541 player = session.query(Player).filter_by(player_id=2).one()
542 # else it is a tracked player
544 # see if the player is already in the database
545 # if not, create one and the hashkey along with it
547 hk = session.query(Hashkey).filter_by(
548 hashkey=hashkey).one()
549 player = session.query(Player).filter_by(
550 player_id=hk.player_id).one()
551 log.debug("Found existing player {0} with hashkey {1}".format(
552 player.player_id, hashkey))
558 # if nick is given to us, use it. If not, use "Anonymous Player"
559 # with a suffix added for uniqueness.
561 player.nick = nick[:128]
562 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
564 player.nick = "Anonymous Player #{0}".format(player.player_id)
565 player.stripped_nick = player.nick
567 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
569 log.debug("Created player {0} ({2}) with hashkey {1}".format(
570 player.player_id, hashkey, player.nick.encode('utf-8')))
575 def create_default_game_stat(session, game_type_cd):
576 """Creates a blanked-out pgstat record for the given game type"""
578 # this is what we have to do to get partitioned records in - grab the
579 # sequence value first, then insert using the explicit ID (vs autogenerate)
580 seq = Sequence('player_game_stats_player_game_stat_id_seq')
581 pgstat_id = session.execute(seq)
582 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
583 create_dt=datetime.datetime.utcnow())
585 if game_type_cd == 'as':
586 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
588 if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
589 pgstat.kills = pgstat.deaths = pgstat.suicides = 0
591 if game_type_cd == 'cq':
592 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
595 if game_type_cd == 'ctf':
596 pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
597 pgstat.returns = pgstat.carrier_frags = 0
599 if game_type_cd == 'cts':
602 if game_type_cd == 'dom':
603 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
606 if game_type_cd == 'ft':
607 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
609 if game_type_cd == 'ka':
610 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
611 pgstat.carrier_frags = 0
612 pgstat.time = datetime.timedelta(seconds=0)
614 if game_type_cd == 'kh':
615 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
616 pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
617 pgstat.carrier_frags = 0
619 if game_type_cd == 'lms':
620 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
622 if game_type_cd == 'nb':
623 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
626 if game_type_cd == 'rc':
627 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
632 def create_game_stat(session, game_meta, game, server, gmap, player, events):
633 """Game stats handler for all game types"""
635 game_type_cd = game.game_type_cd
637 pgstat = create_default_game_stat(session, game_type_cd)
639 # these fields should be on every pgstat record
640 pgstat.game_id = game.game_id
641 pgstat.player_id = player.player_id
642 pgstat.nick = events.get('n', 'Anonymous Player')[:128]
643 pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
644 pgstat.score = int(round(float(events.get('scoreboard-score', 0))))
645 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
646 pgstat.rank = int(events.get('rank', None))
647 pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
649 if pgstat.nick != player.nick \
650 and player.player_id > 2 \
651 and pgstat.nick != 'Anonymous Player':
652 register_new_nick(session, player, pgstat.nick)
656 # gametype-specific stuff is handled here. if passed to us, we store it
657 for (key,value) in events.items():
658 if key == 'wins': wins = True
659 if key == 't': pgstat.team = int(value)
661 if key == 'scoreboard-drops': pgstat.drops = int(value)
662 if key == 'scoreboard-returns': pgstat.returns = int(value)
663 if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
664 if key == 'scoreboard-pickups': pgstat.pickups = int(value)
665 if key == 'scoreboard-caps': pgstat.captures = int(value)
666 if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
667 if key == 'scoreboard-deaths': pgstat.deaths = int(value)
668 if key == 'scoreboard-kills': pgstat.kills = int(value)
669 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
670 if key == 'scoreboard-objectives': pgstat.collects = int(value)
671 if key == 'scoreboard-captured': pgstat.captures = int(value)
672 if key == 'scoreboard-released': pgstat.drops = int(value)
673 if key == 'scoreboard-fastest':
674 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
675 if key == 'scoreboard-takes': pgstat.pickups = int(value)
676 if key == 'scoreboard-ticks': pgstat.drops = int(value)
677 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
678 if key == 'scoreboard-bctime':
679 pgstat.time = datetime.timedelta(seconds=int(value))
680 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
681 if key == 'scoreboard-losses': pgstat.drops = int(value)
682 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
683 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
684 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
685 if key == 'scoreboard-lives': pgstat.lives = int(value)
686 if key == 'scoreboard-goals': pgstat.captures = int(value)
687 if key == 'scoreboard-faults': pgstat.drops = int(value)
688 if key == 'scoreboard-laps': pgstat.laps = int(value)
690 if key == 'avglatency': pgstat.avg_latency = float(value)
691 if key == 'scoreboard-captime':
692 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
693 if game.game_type_cd == 'ctf':
694 update_fastest_cap(session, player.player_id, game.game_id,
695 gmap.map_id, pgstat.fastest, game.mod)
697 # there is no "winning team" field, so we have to derive it
698 if wins and pgstat.team is not None and game.winner is None:
699 game.winner = pgstat.team
707 def create_anticheats(session, pgstat, game, player, events):
708 """Anticheats handler for all game types"""
712 # all anticheat events are prefixed by "anticheat"
713 for (key,value) in events.items():
714 if key.startswith("anticheat"):
716 ac = PlayerGameAnticheat(
722 anticheats.append(ac)
724 except Exception as e:
725 log.debug("Could not parse value for key %s. Ignoring." % key)
730 def create_default_team_stat(session, game_type_cd):
731 """Creates a blanked-out teamstat record for the given game type"""
733 # this is what we have to do to get partitioned records in - grab the
734 # sequence value first, then insert using the explicit ID (vs autogenerate)
735 seq = Sequence('team_game_stats_team_game_stat_id_seq')
736 teamstat_id = session.execute(seq)
737 teamstat = TeamGameStat(team_game_stat_id=teamstat_id,
738 create_dt=datetime.datetime.utcnow())
740 # all team game modes have a score, so we'll zero that out always
743 if game_type_cd in 'ca' 'ft' 'lms' 'ka':
746 if game_type_cd == 'ctf':
752 def create_team_stat(session, game, events):
753 """Team stats handler for all game types"""
756 teamstat = create_default_team_stat(session, game.game_type_cd)
757 teamstat.game_id = game.game_id
759 # we should have a team ID if we have a 'Q' event
760 if re.match(r'^team#\d+$', events.get('Q', '')):
761 team = int(events.get('Q').replace('team#', ''))
764 # gametype-specific stuff is handled here. if passed to us, we store it
765 for (key,value) in events.items():
766 if key == 'scoreboard-score': teamstat.score = int(round(float(value)))
767 if key == 'scoreboard-caps': teamstat.caps = int(value)
768 if key == 'scoreboard-goals': teamstat.caps = int(value)
769 if key == 'scoreboard-rounds': teamstat.rounds = int(value)
771 session.add(teamstat)
772 except Exception as e:
778 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
779 """Weapon stats handler for all game types"""
782 # Version 1 of stats submissions doubled the data sent.
783 # To counteract this we divide the data by 2 only for
784 # POSTs coming from version 1.
786 version = int(game_meta['V'])
789 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
795 for (key,value) in events.items():
796 matched = re.search("acc-(.*?)-cnt-fired", key)
798 weapon_cd = matched.group(1)
800 # Weapon names changed for 0.8. We'll convert the old
801 # ones to use the new scheme as well.
802 mapped_weapon_cd = weapon_map.get(weapon_cd, weapon_cd)
804 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
805 pwstat_id = session.execute(seq)
806 pwstat = PlayerWeaponStat()
807 pwstat.player_weapon_stats_id = pwstat_id
808 pwstat.player_id = player.player_id
809 pwstat.game_id = game.game_id
810 pwstat.player_game_stat_id = pgstat.player_game_stat_id
811 pwstat.weapon_cd = mapped_weapon_cd
814 pwstat.nick = events['n']
816 pwstat.nick = events['P']
818 if 'acc-' + weapon_cd + '-cnt-fired' in events:
819 pwstat.fired = int(round(float(
820 events['acc-' + weapon_cd + '-cnt-fired'])))
821 if 'acc-' + weapon_cd + '-fired' in events:
822 pwstat.max = int(round(float(
823 events['acc-' + weapon_cd + '-fired'])))
824 if 'acc-' + weapon_cd + '-cnt-hit' in events:
825 pwstat.hit = int(round(float(
826 events['acc-' + weapon_cd + '-cnt-hit'])))
827 if 'acc-' + weapon_cd + '-hit' in events:
828 pwstat.actual = int(round(float(
829 events['acc-' + weapon_cd + '-hit'])))
830 if 'acc-' + weapon_cd + '-frags' in events:
831 pwstat.frags = int(round(float(
832 events['acc-' + weapon_cd + '-frags'])))
835 pwstat.fired = pwstat.fired/2
836 pwstat.max = pwstat.max/2
837 pwstat.hit = pwstat.hit/2
838 pwstat.actual = pwstat.actual/2
839 pwstat.frags = pwstat.frags/2
842 pwstats.append(pwstat)
847 def get_ranks(session, player_ids, game_type_cd):
849 Gets the rank entries for all players in the given list, returning a dict
850 of player_id -> PlayerRank instance. The rank entry corresponds to the
851 game type of the parameter passed in as well.
854 for pr in session.query(PlayerRank).\
855 filter(PlayerRank.player_id.in_(player_ids)).\
856 filter(PlayerRank.game_type_cd == game_type_cd).\
858 ranks[pr.player_id] = pr
863 def submit_stats(request):
865 Entry handler for POST stats submissions.
868 # placeholder for the actual session
871 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
872 "----- END REQUEST BODY -----\n\n")
874 (idfp, status) = verify_request(request)
875 (game_meta, raw_players, raw_teams) = parse_stats_submission(request.body)
876 revision = game_meta.get('R', 'unknown')
877 duration = game_meta.get('D', None)
879 # only players present at the end of the match are eligible for stats
880 raw_players = filter(played_in_game, raw_players)
882 do_precondition_checks(request, game_meta, raw_players)
884 # the "duel" gametype is fake
885 if len(raw_players) == 2 \
886 and num_real_players(raw_players) == 2 \
887 and game_meta['G'] == 'dm':
888 game_meta['G'] = 'duel'
890 #----------------------------------------------------------------------
891 # Actual setup (inserts/updates) below here
892 #----------------------------------------------------------------------
893 session = DBSession()
895 game_type_cd = game_meta['G']
897 # All game types create Game, Server, Map, and Player records
899 server = get_or_create_server(
902 name = game_meta['S'],
904 ip_addr = get_remote_addr(request),
905 port = game_meta.get('U', None),
906 impure_cvars = game_meta.get('C', 0))
908 gmap = get_or_create_map(
910 name = game_meta['M'])
914 start_dt = datetime.datetime.utcnow(),
915 server_id = server.server_id,
916 game_type_cd = game_type_cd,
917 map_id = gmap.map_id,
918 match_id = game_meta['I'],
920 mod = game_meta.get('O', None))
922 # keep track of the players we've seen
926 for events in raw_players:
927 player = get_or_create_player(
929 hashkey = events['P'],
930 nick = events.get('n', None))
932 pgstat = create_game_stat(session, game_meta, game, server,
933 gmap, player, events)
934 pgstats.append(pgstat)
936 if player.player_id > 1:
937 anticheats = create_anticheats(session, pgstat, game, player, events)
939 if player.player_id > 2:
940 player_ids.append(player.player_id)
941 hashkeys[player.player_id] = events['P']
943 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
944 pwstats = create_weapon_stats(session, game_meta, game, player,
947 # store them on games for easy access
948 game.players = player_ids
950 for events in raw_teams:
952 teamstat = create_team_stat(session, game, events)
953 except Exception as e:
956 if server.elo_ind and gametype_elo_eligible(game_type_cd):
957 ep = EloProcessor(session, game, pgstats)
961 log.debug('Success! Stats recorded.')
963 # ranks are fetched after we've done the "real" processing
964 ranks = get_ranks(session, player_ids, game_type_cd)
966 # plain text response
967 request.response.content_type = 'text/plain'
970 "now" : calendar.timegm(datetime.datetime.utcnow().timetuple()),
974 "player_ids" : player_ids,
975 "hashkeys" : hashkeys,
980 except Exception as e: