7 import pyramid.httpexceptions
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 class Submission(object):
20 """Parses an incoming POST request for stats submissions."""
22 def __init__(self, body, headers):
23 # a copy of the HTTP headers
24 self.headers = headers
26 # a copy of the HTTP POST body
38 # distinct weapons that we have seen fired
41 # the parsing deque (we use this to allow peeking)
42 self.q = collections.deque(self.body.split("\n"))
45 """Returns the next key:value pair off the queue."""
47 items = self.q.popleft().strip().split(' ', 1)
55 def parse_player(self, key, pid):
56 """Construct a player events listing from the submission."""
58 # all of the keys related to player records
59 player_keys = ['i', 'n', 't', 'e']
63 # Consume all following 'i' 'n' 't' 'e' records
64 while len(self.q) > 0:
65 (key, value) = self.next_item()
66 if key is None and value is None:
69 (sub_key, sub_value) = value.split(' ', 1)
70 player[sub_key] = sub_value
72 player[key] = unicode(value, 'utf-8')
73 elif key in player_keys:
76 # something we didn't expect - put it back on the deque
77 self.q.appendleft("{} {}".format(key, value))
80 self.players.append(player)
82 def parse_team(self, key, tid):
83 """Construct a team events listing from the submission."""
86 # Consume all following 'e' records
87 while len(self.q) > 0 and self.q[0].startswith('e'):
88 (_, value) = self.next_item()
89 (sub_key, sub_value) = value.split(' ', 1)
90 team[sub_key] = sub_value
92 self.teams.append(team)
95 """Parses the request body into instance variables."""
96 while len(self.q) > 0:
97 (key, value) = self.next_item()
98 if key is None and value is None:
101 self.meta[key] = unicode(value, 'utf-8')
103 self.parse_player(key, value)
105 self.parse_team(key, value)
107 self.meta[key] = value
112 def parse_stats_submission(body):
114 Parses the POST request body for a stats submission
116 # storage vars for the request body
122 # we're not in either stanza to start
125 for line in body.split('\n'):
127 (key, value) = line.strip().split(' ', 1)
129 # Server (S) and Nick (n) fields can have international characters.
131 value = unicode(value, 'utf-8')
133 if key not in 'P' 'Q' 'n' 'e' 't' 'i':
134 game_meta[key] = value
136 if key == 'Q' or key == 'P':
137 #log.debug('Found a {0}'.format(key))
138 #log.debug('in_Q: {0}'.format(in_Q))
139 #log.debug('in_P: {0}'.format(in_P))
140 #log.debug('events: {0}'.format(events))
142 # check where we were before and append events accordingly
143 if in_Q and len(events) > 0:
144 #log.debug('creating a team (Q) entry')
147 elif in_P and len(events) > 0:
148 #log.debug('creating a player (P) entry')
149 players.append(events)
153 #log.debug('key == P')
157 #log.debug('key == Q')
164 (subkey, subvalue) = value.split(' ', 1)
165 events[subkey] = subvalue
171 # no key/value pair - move on to the next line
174 # add the last entity we were working on
175 if in_P and len(events) > 0:
176 players.append(events)
177 elif in_Q and len(events) > 0:
180 return (game_meta, players, teams)
183 def is_blank_game(gametype, players):
184 """Determine if this is a blank game or not. A blank game is either:
186 1) a match that ended in the warmup stage, where accuracy events are not
187 present (for non-CTS games)
189 2) a match in which no player made a positive or negative score AND was
192 ... or for CTS, which doesn't record accuracy events
194 1) a match in which no player made a fastest lap AND was
197 ... or for NB, in which not all maps have weapons
199 1) a match in which no player made a positive or negative score
201 r = re.compile(r'acc-.*-cnt-fired')
202 flg_nonzero_score = False
203 flg_acc_events = False
204 flg_fastest_lap = False
206 for events in players:
207 if is_real_player(events) and played_in_game(events):
208 for (key,value) in events.items():
209 if key == 'scoreboard-score' and value != 0:
210 flg_nonzero_score = True
212 flg_acc_events = True
213 if key == 'scoreboard-fastest':
214 flg_fastest_lap = True
216 if gametype == 'cts':
217 return not flg_fastest_lap
218 elif gametype == 'nb':
219 return not flg_nonzero_score
221 return not (flg_nonzero_score and flg_acc_events)
224 def get_remote_addr(request):
225 """Get the Xonotic server's IP address"""
226 if 'X-Forwarded-For' in request.headers:
227 return request.headers['X-Forwarded-For']
229 return request.remote_addr
232 def is_supported_gametype(gametype, version):
233 """Whether a gametype is supported or not"""
236 # if the type can be supported, but with version constraints, uncomment
237 # here and add the restriction for a specific version below
238 supported_game_types = (
257 if gametype in supported_game_types:
262 # some game types were buggy before revisions, thus this additional filter
263 if gametype == 'ca' and version <= 5:
269 def do_precondition_checks(request, game_meta, raw_players):
270 """Precondition checks for ALL gametypes.
271 These do not require a database connection."""
272 if not has_required_metadata(game_meta):
273 msg = "Missing required game metadata"
275 raise pyramid.httpexceptions.HTTPUnprocessableEntity(
277 content_type="text/plain"
281 version = int(game_meta['V'])
283 msg = "Invalid or incorrect game metadata provided"
285 raise pyramid.httpexceptions.HTTPUnprocessableEntity(
287 content_type="text/plain"
290 if not is_supported_gametype(game_meta['G'], version):
291 msg = "Unsupported game type ({})".format(game_meta['G'])
293 raise pyramid.httpexceptions.HTTPOk(
295 content_type="text/plain"
298 if not has_minimum_real_players(request.registry.settings, raw_players):
299 msg = "Not enough real players"
301 raise pyramid.httpexceptions.HTTPOk(
303 content_type="text/plain"
306 if is_blank_game(game_meta['G'], raw_players):
309 raise pyramid.httpexceptions.HTTPOk(
311 content_type="text/plain"
315 def is_real_player(events):
317 Determines if a given set of events correspond with a non-bot
319 if not events['P'].startswith('bot'):
325 def played_in_game(events):
327 Determines if a given set of player events correspond with a player who
328 played in the game (matches 1 and scoreboardvalid 1)
330 if 'matches' in events and 'scoreboardvalid' in events:
336 def num_real_players(player_events):
338 Returns the number of real players (those who played
339 and are on the scoreboard).
343 for events in player_events:
344 if is_real_player(events) and played_in_game(events):
350 def has_minimum_real_players(settings, player_events):
352 Determines if the collection of player events has enough "real" players
353 to store in the database. The minimum setting comes from the config file
354 under the setting xonstat.minimum_real_players.
356 flg_has_min_real_players = True
359 minimum_required_players = int(
360 settings['xonstat.minimum_required_players'])
362 minimum_required_players = 2
364 real_players = num_real_players(player_events)
366 if real_players < minimum_required_players:
367 flg_has_min_real_players = False
369 return flg_has_min_real_players
372 def has_required_metadata(metadata):
374 Determines if a give set of metadata has enough data to create a game,
375 server, and map with.
377 flg_has_req_metadata = True
379 if 'G' not in metadata or\
380 'M' not in metadata or\
381 'I' not in metadata or\
383 flg_has_req_metadata = False
385 return flg_has_req_metadata
388 def should_do_weapon_stats(game_type_cd):
389 """True of the game type should record weapon stats. False otherwise."""
390 if game_type_cd in 'cts':
396 def gametype_elo_eligible(game_type_cd):
397 """True of the game type should process Elos. False otherwise."""
398 elo_game_types = ('duel', 'dm', 'ca', 'ctf', 'tdm', 'ka', 'ft')
400 if game_type_cd in elo_game_types:
406 def register_new_nick(session, player, new_nick):
408 Change the player record's nick to the newly found nick. Store the old
409 nick in the player_nicks table for that player.
411 session - SQLAlchemy database session factory
412 player - player record whose nick is changing
413 new_nick - the new nickname
415 # see if that nick already exists
416 stripped_nick = strip_colors(qfont_decode(player.nick))
418 player_nick = session.query(PlayerNick).filter_by(
419 player_id=player.player_id, stripped_nick=stripped_nick).one()
420 except NoResultFound, e:
421 # player_id/stripped_nick not found, create one
422 # but we don't store "Anonymous Player #N"
423 if not re.search('^Anonymous Player #\d+$', player.nick):
424 player_nick = PlayerNick()
425 player_nick.player_id = player.player_id
426 player_nick.stripped_nick = stripped_nick
427 player_nick.nick = player.nick
428 session.add(player_nick)
430 # We change to the new nick regardless
431 player.nick = new_nick
432 player.stripped_nick = strip_colors(qfont_decode(new_nick))
436 def update_fastest_cap(session, player_id, game_id, map_id, captime, mod):
438 Check the fastest cap time for the player and map. If there isn't
439 one, insert one. If there is, check if the passed time is faster.
442 # we don't record fastest cap times for bots or anonymous players
446 # see if a cap entry exists already
447 # then check to see if the new captime is faster
449 cur_fastest_cap = session.query(PlayerCaptime).filter_by(
450 player_id=player_id, map_id=map_id, mod=mod).one()
452 # current captime is faster, so update
453 if captime < cur_fastest_cap.fastest_cap:
454 cur_fastest_cap.fastest_cap = captime
455 cur_fastest_cap.game_id = game_id
456 cur_fastest_cap.create_dt = datetime.datetime.utcnow()
457 session.add(cur_fastest_cap)
459 except NoResultFound, e:
460 # none exists, so insert
461 cur_fastest_cap = PlayerCaptime(player_id, game_id, map_id, captime,
463 session.add(cur_fastest_cap)
467 def update_server(server, name, hashkey, ip_addr, port, revision, impure_cvars):
469 Updates the server in the given DB session, if needed.
471 :param server: The found server instance.
472 :param name: The incoming server name.
473 :param hashkey: The incoming server hashkey.
474 :param ip_addr: The incoming server IP address.
475 :param port: The incoming server port.
476 :param revision: The incoming server revision.
477 :param impure_cvars: The incoming number of impure server cvars.
480 # ensure the two int attributes are actually ints
487 impure_cvars = int(impure_cvars)
492 if name and server.name != name:
495 if hashkey and server.hashkey != hashkey:
496 server.hashkey = hashkey
498 if ip_addr and server.ip_addr != ip_addr:
499 server.ip_addr = ip_addr
501 if port and server.port != port:
504 if revision and server.revision != revision:
505 server.revision = revision
507 if impure_cvars and server.impure_cvars != impure_cvars:
508 server.impure_cvars = impure_cvars
509 server.pure_ind = True if impure_cvars == 0 else False
515 def get_or_create_server(session, name, hashkey, ip_addr, revision, port, impure_cvars):
517 Find a server by name or create one if not found. Parameters:
519 session - SQLAlchemy database session factory
520 name - server name of the server to be found or created
521 hashkey - server hashkey
522 ip_addr - the IP address of the server
523 revision - the xonotic revision number
524 port - the port number of the server
525 impure_cvars - the number of impure cvar changes
527 servers_q = DBSession.query(Server).filter(Server.active_ind)
530 # if the hashkey is provided, we'll use that
531 servers_q = servers_q.filter((Server.name == name) or (Server.hashkey == hashkey))
533 # otherwise, it is just by name
534 servers_q = servers_q.filter(Server.name == name)
536 # order by the hashkey, which means any hashkey match will appear first if there are multiple
537 servers = servers_q.order_by(Server.hashkey, Server.create_dt).all()
539 if len(servers) == 0:
540 server = Server(name=name, hashkey=hashkey)
543 log.debug("Created server {} with hashkey {}.".format(server.server_id, server.hashkey))
546 if len(servers) == 1:
547 log.info("Found existing server {}.".format(server.server_id))
549 elif len(servers) > 1:
550 server_id_list = ", ".join(["{}".format(s.server_id) for s in servers])
551 log.warn("Multiple servers found ({})! Using the first one ({})."
552 .format(server_id_list, server.server_id))
554 if update_server(server, name, hashkey, ip_addr, port, revision, impure_cvars):
560 def get_or_create_map(session=None, name=None):
562 Find a map by name or create one if not found. Parameters:
564 session - SQLAlchemy database session factory
565 name - map name of the map to be found or created
568 # find one by the name, if it exists
569 gmap = session.query(Map).filter_by(name=name).one()
570 log.debug("Found map id {0}: {1}".format(gmap.map_id,
572 except NoResultFound, e:
573 gmap = Map(name=name)
576 log.debug("Created map id {0}: {1}".format(gmap.map_id,
578 except MultipleResultsFound, e:
579 # multiple found, so use the first one but warn
581 gmaps = session.query(Map).filter_by(name=name).order_by(
584 log.debug("Found map id {0}: {1} but found \
585 multiple".format(gmap.map_id, gmap.name))
590 def create_game(session, start_dt, game_type_cd, server_id, map_id,
591 match_id, duration, mod, winner=None):
593 Creates a game. Parameters:
595 session - SQLAlchemy database session factory
596 start_dt - when the game started (datetime object)
597 game_type_cd - the game type of the game being played
598 server_id - server identifier of the server hosting the game
599 map_id - map on which the game was played
600 winner - the team id of the team that won
601 duration - how long the game lasted
602 mod - mods in use during the game
604 seq = Sequence('games_game_id_seq')
605 game_id = session.execute(seq)
606 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
607 server_id=server_id, map_id=map_id, winner=winner)
608 game.match_id = match_id
611 # There is some drift between start_dt (provided by app) and create_dt
612 # (default in the database), so we'll make them the same until this is
614 game.create_dt = start_dt
617 game.duration = datetime.timedelta(seconds=int(round(float(duration))))
622 session.query(Game).filter(Game.server_id==server_id).\
623 filter(Game.match_id==match_id).one()
625 log.debug("Error: game with same server and match_id found! Ignoring.")
627 # if a game under the same server and match_id found,
628 # this is a duplicate game and can be ignored
629 raise pyramid.httpexceptions.HTTPOk('OK')
630 except NoResultFound, e:
631 # server_id/match_id combination not found. game is ok to insert
634 log.debug("Created game id {0} on server {1}, map {2} at \
635 {3}".format(game.game_id,
636 server_id, map_id, start_dt))
641 def get_or_create_player(session=None, hashkey=None, nick=None):
643 Finds a player by hashkey or creates a new one (along with a
644 corresponding hashkey entry. Parameters:
646 session - SQLAlchemy database session factory
647 hashkey - hashkey of the player to be found or created
648 nick - nick of the player (in case of a first time create)
651 if re.search('^bot#\d+', hashkey):
652 player = session.query(Player).filter_by(player_id=1).one()
653 # if we have an untracked player
654 elif re.search('^player#\d+$', hashkey):
655 player = session.query(Player).filter_by(player_id=2).one()
656 # else it is a tracked player
658 # see if the player is already in the database
659 # if not, create one and the hashkey along with it
661 hk = session.query(Hashkey).filter_by(
662 hashkey=hashkey).one()
663 player = session.query(Player).filter_by(
664 player_id=hk.player_id).one()
665 log.debug("Found existing player {0} with hashkey {1}".format(
666 player.player_id, hashkey))
672 # if nick is given to us, use it. If not, use "Anonymous Player"
673 # with a suffix added for uniqueness.
675 player.nick = nick[:128]
676 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
678 player.nick = "Anonymous Player #{0}".format(player.player_id)
679 player.stripped_nick = player.nick
681 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
683 log.debug("Created player {0} ({2}) with hashkey {1}".format(
684 player.player_id, hashkey, player.nick.encode('utf-8')))
689 def create_default_game_stat(session, game_type_cd):
690 """Creates a blanked-out pgstat record for the given game type"""
692 # this is what we have to do to get partitioned records in - grab the
693 # sequence value first, then insert using the explicit ID (vs autogenerate)
694 seq = Sequence('player_game_stats_player_game_stat_id_seq')
695 pgstat_id = session.execute(seq)
696 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
697 create_dt=datetime.datetime.utcnow())
699 if game_type_cd == 'as':
700 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
702 if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
703 pgstat.kills = pgstat.deaths = pgstat.suicides = 0
705 if game_type_cd == 'cq':
706 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
709 if game_type_cd == 'ctf':
710 pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
711 pgstat.returns = pgstat.carrier_frags = 0
713 if game_type_cd == 'cts':
716 if game_type_cd == 'dom':
717 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
720 if game_type_cd == 'ft':
721 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
723 if game_type_cd == 'ka':
724 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
725 pgstat.carrier_frags = 0
726 pgstat.time = datetime.timedelta(seconds=0)
728 if game_type_cd == 'kh':
729 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
730 pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
731 pgstat.carrier_frags = 0
733 if game_type_cd == 'lms':
734 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
736 if game_type_cd == 'nb':
737 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
740 if game_type_cd == 'rc':
741 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
746 def create_game_stat(session, game_meta, game, server, gmap, player, events):
747 """Game stats handler for all game types"""
749 game_type_cd = game.game_type_cd
751 pgstat = create_default_game_stat(session, game_type_cd)
753 # these fields should be on every pgstat record
754 pgstat.game_id = game.game_id
755 pgstat.player_id = player.player_id
756 pgstat.nick = events.get('n', 'Anonymous Player')[:128]
757 pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
758 pgstat.score = int(round(float(events.get('scoreboard-score', 0))))
759 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
760 pgstat.rank = int(events.get('rank', None))
761 pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
763 if pgstat.nick != player.nick \
764 and player.player_id > 2 \
765 and pgstat.nick != 'Anonymous Player':
766 register_new_nick(session, player, pgstat.nick)
770 # gametype-specific stuff is handled here. if passed to us, we store it
771 for (key,value) in events.items():
772 if key == 'wins': wins = True
773 if key == 't': pgstat.team = int(value)
775 if key == 'scoreboard-drops': pgstat.drops = int(value)
776 if key == 'scoreboard-returns': pgstat.returns = int(value)
777 if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
778 if key == 'scoreboard-pickups': pgstat.pickups = int(value)
779 if key == 'scoreboard-caps': pgstat.captures = int(value)
780 if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
781 if key == 'scoreboard-deaths': pgstat.deaths = int(value)
782 if key == 'scoreboard-kills': pgstat.kills = int(value)
783 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
784 if key == 'scoreboard-objectives': pgstat.collects = int(value)
785 if key == 'scoreboard-captured': pgstat.captures = int(value)
786 if key == 'scoreboard-released': pgstat.drops = int(value)
787 if key == 'scoreboard-fastest':
788 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
789 if key == 'scoreboard-takes': pgstat.pickups = int(value)
790 if key == 'scoreboard-ticks': pgstat.drops = int(value)
791 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
792 if key == 'scoreboard-bctime':
793 pgstat.time = datetime.timedelta(seconds=int(value))
794 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
795 if key == 'scoreboard-losses': pgstat.drops = int(value)
796 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
797 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
798 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
799 if key == 'scoreboard-lives': pgstat.lives = int(value)
800 if key == 'scoreboard-goals': pgstat.captures = int(value)
801 if key == 'scoreboard-faults': pgstat.drops = int(value)
802 if key == 'scoreboard-laps': pgstat.laps = int(value)
804 if key == 'avglatency': pgstat.avg_latency = float(value)
805 if key == 'scoreboard-captime':
806 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
807 if game.game_type_cd == 'ctf':
808 update_fastest_cap(session, player.player_id, game.game_id,
809 gmap.map_id, pgstat.fastest, game.mod)
811 # there is no "winning team" field, so we have to derive it
812 if wins and pgstat.team is not None and game.winner is None:
813 game.winner = pgstat.team
821 def create_anticheats(session, pgstat, game, player, events):
822 """Anticheats handler for all game types"""
826 # all anticheat events are prefixed by "anticheat"
827 for (key,value) in events.items():
828 if key.startswith("anticheat"):
830 ac = PlayerGameAnticheat(
836 anticheats.append(ac)
838 except Exception as e:
839 log.debug("Could not parse value for key %s. Ignoring." % key)
844 def create_default_team_stat(session, game_type_cd):
845 """Creates a blanked-out teamstat record for the given game type"""
847 # this is what we have to do to get partitioned records in - grab the
848 # sequence value first, then insert using the explicit ID (vs autogenerate)
849 seq = Sequence('team_game_stats_team_game_stat_id_seq')
850 teamstat_id = session.execute(seq)
851 teamstat = TeamGameStat(team_game_stat_id=teamstat_id,
852 create_dt=datetime.datetime.utcnow())
854 # all team game modes have a score, so we'll zero that out always
857 if game_type_cd in 'ca' 'ft' 'lms' 'ka':
860 if game_type_cd == 'ctf':
866 def create_team_stat(session, game, events):
867 """Team stats handler for all game types"""
870 teamstat = create_default_team_stat(session, game.game_type_cd)
871 teamstat.game_id = game.game_id
873 # we should have a team ID if we have a 'Q' event
874 if re.match(r'^team#\d+$', events.get('Q', '')):
875 team = int(events.get('Q').replace('team#', ''))
878 # gametype-specific stuff is handled here. if passed to us, we store it
879 for (key,value) in events.items():
880 if key == 'scoreboard-score': teamstat.score = int(round(float(value)))
881 if key == 'scoreboard-caps': teamstat.caps = int(value)
882 if key == 'scoreboard-goals': teamstat.caps = int(value)
883 if key == 'scoreboard-rounds': teamstat.rounds = int(value)
885 session.add(teamstat)
886 except Exception as e:
892 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
893 """Weapon stats handler for all game types"""
896 # Version 1 of stats submissions doubled the data sent.
897 # To counteract this we divide the data by 2 only for
898 # POSTs coming from version 1.
900 version = int(game_meta['V'])
903 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
909 for (key,value) in events.items():
910 matched = re.search("acc-(.*?)-cnt-fired", key)
912 weapon_cd = matched.group(1)
914 # Weapon names changed for 0.8. We'll convert the old
915 # ones to use the new scheme as well.
916 mapped_weapon_cd = weapon_map.get(weapon_cd, weapon_cd)
918 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
919 pwstat_id = session.execute(seq)
920 pwstat = PlayerWeaponStat()
921 pwstat.player_weapon_stats_id = pwstat_id
922 pwstat.player_id = player.player_id
923 pwstat.game_id = game.game_id
924 pwstat.player_game_stat_id = pgstat.player_game_stat_id
925 pwstat.weapon_cd = mapped_weapon_cd
928 pwstat.nick = events['n']
930 pwstat.nick = events['P']
932 if 'acc-' + weapon_cd + '-cnt-fired' in events:
933 pwstat.fired = int(round(float(
934 events['acc-' + weapon_cd + '-cnt-fired'])))
935 if 'acc-' + weapon_cd + '-fired' in events:
936 pwstat.max = int(round(float(
937 events['acc-' + weapon_cd + '-fired'])))
938 if 'acc-' + weapon_cd + '-cnt-hit' in events:
939 pwstat.hit = int(round(float(
940 events['acc-' + weapon_cd + '-cnt-hit'])))
941 if 'acc-' + weapon_cd + '-hit' in events:
942 pwstat.actual = int(round(float(
943 events['acc-' + weapon_cd + '-hit'])))
944 if 'acc-' + weapon_cd + '-frags' in events:
945 pwstat.frags = int(round(float(
946 events['acc-' + weapon_cd + '-frags'])))
949 pwstat.fired = pwstat.fired/2
950 pwstat.max = pwstat.max/2
951 pwstat.hit = pwstat.hit/2
952 pwstat.actual = pwstat.actual/2
953 pwstat.frags = pwstat.frags/2
956 pwstats.append(pwstat)
961 def get_ranks(session, player_ids, game_type_cd):
963 Gets the rank entries for all players in the given list, returning a dict
964 of player_id -> PlayerRank instance. The rank entry corresponds to the
965 game type of the parameter passed in as well.
968 for pr in session.query(PlayerRank).\
969 filter(PlayerRank.player_id.in_(player_ids)).\
970 filter(PlayerRank.game_type_cd == game_type_cd).\
972 ranks[pr.player_id] = pr
977 def submit_stats(request):
979 Entry handler for POST stats submissions.
982 # placeholder for the actual session
985 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
986 "----- END REQUEST BODY -----\n\n")
988 (idfp, status) = verify_request(request)
989 (game_meta, raw_players, raw_teams) = parse_stats_submission(request.body)
990 revision = game_meta.get('R', 'unknown')
991 duration = game_meta.get('D', None)
993 # only players present at the end of the match are eligible for stats
994 raw_players = filter(played_in_game, raw_players)
996 do_precondition_checks(request, game_meta, raw_players)
998 # the "duel" gametype is fake
999 if len(raw_players) == 2 \
1000 and num_real_players(raw_players) == 2 \
1001 and game_meta['G'] == 'dm':
1002 game_meta['G'] = 'duel'
1004 #----------------------------------------------------------------------
1005 # Actual setup (inserts/updates) below here
1006 #----------------------------------------------------------------------
1007 session = DBSession()
1009 game_type_cd = game_meta['G']
1011 # All game types create Game, Server, Map, and Player records
1013 server = get_or_create_server(
1016 name = game_meta['S'],
1017 revision = revision,
1018 ip_addr = get_remote_addr(request),
1019 port = game_meta.get('U', None),
1020 impure_cvars = game_meta.get('C', 0))
1022 gmap = get_or_create_map(
1024 name = game_meta['M'])
1028 start_dt = datetime.datetime.utcnow(),
1029 server_id = server.server_id,
1030 game_type_cd = game_type_cd,
1031 map_id = gmap.map_id,
1032 match_id = game_meta['I'],
1033 duration = duration,
1034 mod = game_meta.get('O', None))
1036 # keep track of the players we've seen
1040 for events in raw_players:
1041 player = get_or_create_player(
1043 hashkey = events['P'],
1044 nick = events.get('n', None))
1046 pgstat = create_game_stat(session, game_meta, game, server,
1047 gmap, player, events)
1048 pgstats.append(pgstat)
1050 if player.player_id > 1:
1051 anticheats = create_anticheats(session, pgstat, game, player, events)
1053 if player.player_id > 2:
1054 player_ids.append(player.player_id)
1055 hashkeys[player.player_id] = events['P']
1057 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
1058 pwstats = create_weapon_stats(session, game_meta, game, player,
1061 # store them on games for easy access
1062 game.players = player_ids
1064 for events in raw_teams:
1066 teamstat = create_team_stat(session, game, events)
1067 except Exception as e:
1070 if server.elo_ind and gametype_elo_eligible(game_type_cd):
1071 ep = EloProcessor(session, game, pgstats)
1075 log.debug('Success! Stats recorded.')
1077 # ranks are fetched after we've done the "real" processing
1078 ranks = get_ranks(session, player_ids, game_type_cd)
1080 # plain text response
1081 request.response.content_type = 'text/plain'
1084 "now" : calendar.timegm(datetime.datetime.utcnow().timetuple()),
1088 "player_ids" : player_ids,
1089 "hashkeys" : hashkeys,
1094 except Exception as e: