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 check_for_new_weapon_fired(self, sub_key):
56 """Checks if a given player key (subkey, actually) is a new weapon fired in the match."""
57 if sub_key.endswith("cnt-fired"):
58 weapon = sub_key.split("-")[1]
59 if weapon not in self.weapons:
60 self.weapons.add(weapon)
62 def parse_player(self, key, pid):
63 """Construct a player events listing from the submission."""
65 # all of the keys related to player records
66 player_keys = ['i', 'n', 't', 'e']
70 # Consume all following 'i' 'n' 't' 'e' records
71 while len(self.q) > 0:
72 (key, value) = self.next_item()
73 if key is None and value is None:
76 (sub_key, sub_value) = value.split(' ', 1)
77 player[sub_key] = sub_value
79 # keep track of the distinct weapons fired during the match
80 self.check_for_new_weapon_fired(sub_key)
82 player[key] = unicode(value, 'utf-8')
83 elif key in player_keys:
86 # something we didn't expect - put it back on the deque
87 self.q.appendleft("{} {}".format(key, value))
90 self.players.append(player)
92 def parse_team(self, key, tid):
93 """Construct a team events listing from the submission."""
96 # Consume all following 'e' records
97 while len(self.q) > 0 and self.q[0].startswith('e'):
98 (_, value) = self.next_item()
99 (sub_key, sub_value) = value.split(' ', 1)
100 team[sub_key] = sub_value
102 self.teams.append(team)
105 """Parses the request body into instance variables."""
106 while len(self.q) > 0:
107 (key, value) = self.next_item()
108 if key is None and value is None:
111 self.meta[key] = unicode(value, 'utf-8')
113 self.parse_player(key, value)
115 self.parse_team(key, value)
117 self.meta[key] = value
122 def parse_stats_submission(body):
124 Parses the POST request body for a stats submission
126 # storage vars for the request body
132 # we're not in either stanza to start
135 for line in body.split('\n'):
137 (key, value) = line.strip().split(' ', 1)
139 # Server (S) and Nick (n) fields can have international characters.
141 value = unicode(value, 'utf-8')
143 if key not in 'P' 'Q' 'n' 'e' 't' 'i':
144 game_meta[key] = value
146 if key == 'Q' or key == 'P':
147 #log.debug('Found a {0}'.format(key))
148 #log.debug('in_Q: {0}'.format(in_Q))
149 #log.debug('in_P: {0}'.format(in_P))
150 #log.debug('events: {0}'.format(events))
152 # check where we were before and append events accordingly
153 if in_Q and len(events) > 0:
154 #log.debug('creating a team (Q) entry')
157 elif in_P and len(events) > 0:
158 #log.debug('creating a player (P) entry')
159 players.append(events)
163 #log.debug('key == P')
167 #log.debug('key == Q')
174 (subkey, subvalue) = value.split(' ', 1)
175 events[subkey] = subvalue
181 # no key/value pair - move on to the next line
184 # add the last entity we were working on
185 if in_P and len(events) > 0:
186 players.append(events)
187 elif in_Q and len(events) > 0:
190 return (game_meta, players, teams)
193 def is_blank_game(gametype, players):
194 """Determine if this is a blank game or not. A blank game is either:
196 1) a match that ended in the warmup stage, where accuracy events are not
197 present (for non-CTS games)
199 2) a match in which no player made a positive or negative score AND was
202 ... or for CTS, which doesn't record accuracy events
204 1) a match in which no player made a fastest lap AND was
207 ... or for NB, in which not all maps have weapons
209 1) a match in which no player made a positive or negative score
211 r = re.compile(r'acc-.*-cnt-fired')
212 flg_nonzero_score = False
213 flg_acc_events = False
214 flg_fastest_lap = False
216 for events in players:
217 if is_real_player(events) and played_in_game(events):
218 for (key,value) in events.items():
219 if key == 'scoreboard-score' and value != 0:
220 flg_nonzero_score = True
222 flg_acc_events = True
223 if key == 'scoreboard-fastest':
224 flg_fastest_lap = True
226 if gametype == 'cts':
227 return not flg_fastest_lap
228 elif gametype == 'nb':
229 return not flg_nonzero_score
231 return not (flg_nonzero_score and flg_acc_events)
234 def get_remote_addr(request):
235 """Get the Xonotic server's IP address"""
236 if 'X-Forwarded-For' in request.headers:
237 return request.headers['X-Forwarded-For']
239 return request.remote_addr
242 def is_supported_gametype(gametype, version):
243 """Whether a gametype is supported or not"""
246 # if the type can be supported, but with version constraints, uncomment
247 # here and add the restriction for a specific version below
248 supported_game_types = (
267 if gametype in supported_game_types:
272 # some game types were buggy before revisions, thus this additional filter
273 if gametype == 'ca' and version <= 5:
279 def do_precondition_checks(request, game_meta, raw_players):
280 """Precondition checks for ALL gametypes.
281 These do not require a database connection."""
282 if not has_required_metadata(game_meta):
283 msg = "Missing required game metadata"
285 raise pyramid.httpexceptions.HTTPUnprocessableEntity(
287 content_type="text/plain"
291 version = int(game_meta['V'])
293 msg = "Invalid or incorrect game metadata provided"
295 raise pyramid.httpexceptions.HTTPUnprocessableEntity(
297 content_type="text/plain"
300 if not is_supported_gametype(game_meta['G'], version):
301 msg = "Unsupported game type ({})".format(game_meta['G'])
303 raise pyramid.httpexceptions.HTTPOk(
305 content_type="text/plain"
308 if not has_minimum_real_players(request.registry.settings, raw_players):
309 msg = "Not enough real players"
311 raise pyramid.httpexceptions.HTTPOk(
313 content_type="text/plain"
316 if is_blank_game(game_meta['G'], raw_players):
319 raise pyramid.httpexceptions.HTTPOk(
321 content_type="text/plain"
325 def is_real_player(events):
327 Determines if a given set of events correspond with a non-bot
329 if not events['P'].startswith('bot'):
335 def played_in_game(events):
337 Determines if a given set of player events correspond with a player who
338 played in the game (matches 1 and scoreboardvalid 1)
340 if 'matches' in events and 'scoreboardvalid' in events:
346 def num_real_players(player_events):
348 Returns the number of real players (those who played
349 and are on the scoreboard).
353 for events in player_events:
354 if is_real_player(events) and played_in_game(events):
360 def has_minimum_real_players(settings, player_events):
362 Determines if the collection of player events has enough "real" players
363 to store in the database. The minimum setting comes from the config file
364 under the setting xonstat.minimum_real_players.
366 flg_has_min_real_players = True
369 minimum_required_players = int(
370 settings['xonstat.minimum_required_players'])
372 minimum_required_players = 2
374 real_players = num_real_players(player_events)
376 if real_players < minimum_required_players:
377 flg_has_min_real_players = False
379 return flg_has_min_real_players
382 def has_required_metadata(metadata):
384 Determines if a give set of metadata has enough data to create a game,
385 server, and map with.
387 flg_has_req_metadata = True
389 if 'G' not in metadata or\
390 'M' not in metadata or\
391 'I' not in metadata or\
393 flg_has_req_metadata = False
395 return flg_has_req_metadata
398 def should_do_weapon_stats(game_type_cd):
399 """True of the game type should record weapon stats. False otherwise."""
400 if game_type_cd in 'cts':
406 def gametype_elo_eligible(game_type_cd):
407 """True of the game type should process Elos. False otherwise."""
408 elo_game_types = ('duel', 'dm', 'ca', 'ctf', 'tdm', 'ka', 'ft')
410 if game_type_cd in elo_game_types:
416 def register_new_nick(session, player, new_nick):
418 Change the player record's nick to the newly found nick. Store the old
419 nick in the player_nicks table for that player.
421 session - SQLAlchemy database session factory
422 player - player record whose nick is changing
423 new_nick - the new nickname
425 # see if that nick already exists
426 stripped_nick = strip_colors(qfont_decode(player.nick))
428 player_nick = session.query(PlayerNick).filter_by(
429 player_id=player.player_id, stripped_nick=stripped_nick).one()
430 except NoResultFound, e:
431 # player_id/stripped_nick not found, create one
432 # but we don't store "Anonymous Player #N"
433 if not re.search('^Anonymous Player #\d+$', player.nick):
434 player_nick = PlayerNick()
435 player_nick.player_id = player.player_id
436 player_nick.stripped_nick = stripped_nick
437 player_nick.nick = player.nick
438 session.add(player_nick)
440 # We change to the new nick regardless
441 player.nick = new_nick
442 player.stripped_nick = strip_colors(qfont_decode(new_nick))
446 def update_fastest_cap(session, player_id, game_id, map_id, captime, mod):
448 Check the fastest cap time for the player and map. If there isn't
449 one, insert one. If there is, check if the passed time is faster.
452 # we don't record fastest cap times for bots or anonymous players
456 # see if a cap entry exists already
457 # then check to see if the new captime is faster
459 cur_fastest_cap = session.query(PlayerCaptime).filter_by(
460 player_id=player_id, map_id=map_id, mod=mod).one()
462 # current captime is faster, so update
463 if captime < cur_fastest_cap.fastest_cap:
464 cur_fastest_cap.fastest_cap = captime
465 cur_fastest_cap.game_id = game_id
466 cur_fastest_cap.create_dt = datetime.datetime.utcnow()
467 session.add(cur_fastest_cap)
469 except NoResultFound, e:
470 # none exists, so insert
471 cur_fastest_cap = PlayerCaptime(player_id, game_id, map_id, captime,
473 session.add(cur_fastest_cap)
477 def update_server(server, name, hashkey, ip_addr, port, revision, impure_cvars):
479 Updates the server in the given DB session, if needed.
481 :param server: The found server instance.
482 :param name: The incoming server name.
483 :param hashkey: The incoming server hashkey.
484 :param ip_addr: The incoming server IP address.
485 :param port: The incoming server port.
486 :param revision: The incoming server revision.
487 :param impure_cvars: The incoming number of impure server cvars.
490 # ensure the two int attributes are actually ints
497 impure_cvars = int(impure_cvars)
502 if name and server.name != name:
505 if hashkey and server.hashkey != hashkey:
506 server.hashkey = hashkey
508 if ip_addr and server.ip_addr != ip_addr:
509 server.ip_addr = ip_addr
511 if port and server.port != port:
514 if revision and server.revision != revision:
515 server.revision = revision
517 if impure_cvars and server.impure_cvars != impure_cvars:
518 server.impure_cvars = impure_cvars
519 server.pure_ind = True if impure_cvars == 0 else False
525 def get_or_create_server(session, name, hashkey, ip_addr, revision, port, impure_cvars):
527 Find a server by name or create one if not found. Parameters:
529 session - SQLAlchemy database session factory
530 name - server name of the server to be found or created
531 hashkey - server hashkey
532 ip_addr - the IP address of the server
533 revision - the xonotic revision number
534 port - the port number of the server
535 impure_cvars - the number of impure cvar changes
537 servers_q = DBSession.query(Server).filter(Server.active_ind)
540 # if the hashkey is provided, we'll use that
541 servers_q = servers_q.filter((Server.name == name) or (Server.hashkey == hashkey))
543 # otherwise, it is just by name
544 servers_q = servers_q.filter(Server.name == name)
546 # order by the hashkey, which means any hashkey match will appear first if there are multiple
547 servers = servers_q.order_by(Server.hashkey, Server.create_dt).all()
549 if len(servers) == 0:
550 server = Server(name=name, hashkey=hashkey)
553 log.debug("Created server {} with hashkey {}.".format(server.server_id, server.hashkey))
556 if len(servers) == 1:
557 log.info("Found existing server {}.".format(server.server_id))
559 elif len(servers) > 1:
560 server_id_list = ", ".join(["{}".format(s.server_id) for s in servers])
561 log.warn("Multiple servers found ({})! Using the first one ({})."
562 .format(server_id_list, server.server_id))
564 if update_server(server, name, hashkey, ip_addr, port, revision, impure_cvars):
570 def get_or_create_map(session=None, name=None):
572 Find a map by name or create one if not found. Parameters:
574 session - SQLAlchemy database session factory
575 name - map name of the map to be found or created
578 # find one by the name, if it exists
579 gmap = session.query(Map).filter_by(name=name).one()
580 log.debug("Found map id {0}: {1}".format(gmap.map_id,
582 except NoResultFound, e:
583 gmap = Map(name=name)
586 log.debug("Created map id {0}: {1}".format(gmap.map_id,
588 except MultipleResultsFound, e:
589 # multiple found, so use the first one but warn
591 gmaps = session.query(Map).filter_by(name=name).order_by(
594 log.debug("Found map id {0}: {1} but found \
595 multiple".format(gmap.map_id, gmap.name))
600 def create_game(session, start_dt, game_type_cd, server_id, map_id,
601 match_id, duration, mod, winner=None):
603 Creates a game. Parameters:
605 session - SQLAlchemy database session factory
606 start_dt - when the game started (datetime object)
607 game_type_cd - the game type of the game being played
608 server_id - server identifier of the server hosting the game
609 map_id - map on which the game was played
610 winner - the team id of the team that won
611 duration - how long the game lasted
612 mod - mods in use during the game
614 seq = Sequence('games_game_id_seq')
615 game_id = session.execute(seq)
616 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
617 server_id=server_id, map_id=map_id, winner=winner)
618 game.match_id = match_id
621 # There is some drift between start_dt (provided by app) and create_dt
622 # (default in the database), so we'll make them the same until this is
624 game.create_dt = start_dt
627 game.duration = datetime.timedelta(seconds=int(round(float(duration))))
632 session.query(Game).filter(Game.server_id==server_id).\
633 filter(Game.match_id==match_id).one()
635 log.debug("Error: game with same server and match_id found! Ignoring.")
637 # if a game under the same server and match_id found,
638 # this is a duplicate game and can be ignored
639 raise pyramid.httpexceptions.HTTPOk('OK')
640 except NoResultFound, e:
641 # server_id/match_id combination not found. game is ok to insert
644 log.debug("Created game id {0} on server {1}, map {2} at \
645 {3}".format(game.game_id,
646 server_id, map_id, start_dt))
651 def get_or_create_player(session=None, hashkey=None, nick=None):
653 Finds a player by hashkey or creates a new one (along with a
654 corresponding hashkey entry. Parameters:
656 session - SQLAlchemy database session factory
657 hashkey - hashkey of the player to be found or created
658 nick - nick of the player (in case of a first time create)
661 if re.search('^bot#\d+', hashkey):
662 player = session.query(Player).filter_by(player_id=1).one()
663 # if we have an untracked player
664 elif re.search('^player#\d+$', hashkey):
665 player = session.query(Player).filter_by(player_id=2).one()
666 # else it is a tracked player
668 # see if the player is already in the database
669 # if not, create one and the hashkey along with it
671 hk = session.query(Hashkey).filter_by(
672 hashkey=hashkey).one()
673 player = session.query(Player).filter_by(
674 player_id=hk.player_id).one()
675 log.debug("Found existing player {0} with hashkey {1}".format(
676 player.player_id, hashkey))
682 # if nick is given to us, use it. If not, use "Anonymous Player"
683 # with a suffix added for uniqueness.
685 player.nick = nick[:128]
686 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
688 player.nick = "Anonymous Player #{0}".format(player.player_id)
689 player.stripped_nick = player.nick
691 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
693 log.debug("Created player {0} ({2}) with hashkey {1}".format(
694 player.player_id, hashkey, player.nick.encode('utf-8')))
699 def create_default_game_stat(session, game_type_cd):
700 """Creates a blanked-out pgstat record for the given game type"""
702 # this is what we have to do to get partitioned records in - grab the
703 # sequence value first, then insert using the explicit ID (vs autogenerate)
704 seq = Sequence('player_game_stats_player_game_stat_id_seq')
705 pgstat_id = session.execute(seq)
706 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
707 create_dt=datetime.datetime.utcnow())
709 if game_type_cd == 'as':
710 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
712 if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
713 pgstat.kills = pgstat.deaths = pgstat.suicides = 0
715 if game_type_cd == 'cq':
716 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
719 if game_type_cd == 'ctf':
720 pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
721 pgstat.returns = pgstat.carrier_frags = 0
723 if game_type_cd == 'cts':
726 if game_type_cd == 'dom':
727 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
730 if game_type_cd == 'ft':
731 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
733 if game_type_cd == 'ka':
734 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
735 pgstat.carrier_frags = 0
736 pgstat.time = datetime.timedelta(seconds=0)
738 if game_type_cd == 'kh':
739 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
740 pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
741 pgstat.carrier_frags = 0
743 if game_type_cd == 'lms':
744 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
746 if game_type_cd == 'nb':
747 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
750 if game_type_cd == 'rc':
751 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
756 def create_game_stat(session, game_meta, game, server, gmap, player, events):
757 """Game stats handler for all game types"""
759 game_type_cd = game.game_type_cd
761 pgstat = create_default_game_stat(session, game_type_cd)
763 # these fields should be on every pgstat record
764 pgstat.game_id = game.game_id
765 pgstat.player_id = player.player_id
766 pgstat.nick = events.get('n', 'Anonymous Player')[:128]
767 pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
768 pgstat.score = int(round(float(events.get('scoreboard-score', 0))))
769 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
770 pgstat.rank = int(events.get('rank', None))
771 pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
773 if pgstat.nick != player.nick \
774 and player.player_id > 2 \
775 and pgstat.nick != 'Anonymous Player':
776 register_new_nick(session, player, pgstat.nick)
780 # gametype-specific stuff is handled here. if passed to us, we store it
781 for (key,value) in events.items():
782 if key == 'wins': wins = True
783 if key == 't': pgstat.team = int(value)
785 if key == 'scoreboard-drops': pgstat.drops = int(value)
786 if key == 'scoreboard-returns': pgstat.returns = int(value)
787 if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
788 if key == 'scoreboard-pickups': pgstat.pickups = int(value)
789 if key == 'scoreboard-caps': pgstat.captures = int(value)
790 if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
791 if key == 'scoreboard-deaths': pgstat.deaths = int(value)
792 if key == 'scoreboard-kills': pgstat.kills = int(value)
793 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
794 if key == 'scoreboard-objectives': pgstat.collects = int(value)
795 if key == 'scoreboard-captured': pgstat.captures = int(value)
796 if key == 'scoreboard-released': pgstat.drops = int(value)
797 if key == 'scoreboard-fastest':
798 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
799 if key == 'scoreboard-takes': pgstat.pickups = int(value)
800 if key == 'scoreboard-ticks': pgstat.drops = int(value)
801 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
802 if key == 'scoreboard-bctime':
803 pgstat.time = datetime.timedelta(seconds=int(value))
804 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
805 if key == 'scoreboard-losses': pgstat.drops = int(value)
806 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
807 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
808 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
809 if key == 'scoreboard-lives': pgstat.lives = int(value)
810 if key == 'scoreboard-goals': pgstat.captures = int(value)
811 if key == 'scoreboard-faults': pgstat.drops = int(value)
812 if key == 'scoreboard-laps': pgstat.laps = int(value)
814 if key == 'avglatency': pgstat.avg_latency = float(value)
815 if key == 'scoreboard-captime':
816 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
817 if game.game_type_cd == 'ctf':
818 update_fastest_cap(session, player.player_id, game.game_id,
819 gmap.map_id, pgstat.fastest, game.mod)
821 # there is no "winning team" field, so we have to derive it
822 if wins and pgstat.team is not None and game.winner is None:
823 game.winner = pgstat.team
831 def create_anticheats(session, pgstat, game, player, events):
832 """Anticheats handler for all game types"""
836 # all anticheat events are prefixed by "anticheat"
837 for (key,value) in events.items():
838 if key.startswith("anticheat"):
840 ac = PlayerGameAnticheat(
846 anticheats.append(ac)
848 except Exception as e:
849 log.debug("Could not parse value for key %s. Ignoring." % key)
854 def create_default_team_stat(session, game_type_cd):
855 """Creates a blanked-out teamstat record for the given game type"""
857 # this is what we have to do to get partitioned records in - grab the
858 # sequence value first, then insert using the explicit ID (vs autogenerate)
859 seq = Sequence('team_game_stats_team_game_stat_id_seq')
860 teamstat_id = session.execute(seq)
861 teamstat = TeamGameStat(team_game_stat_id=teamstat_id,
862 create_dt=datetime.datetime.utcnow())
864 # all team game modes have a score, so we'll zero that out always
867 if game_type_cd in 'ca' 'ft' 'lms' 'ka':
870 if game_type_cd == 'ctf':
876 def create_team_stat(session, game, events):
877 """Team stats handler for all game types"""
880 teamstat = create_default_team_stat(session, game.game_type_cd)
881 teamstat.game_id = game.game_id
883 # we should have a team ID if we have a 'Q' event
884 if re.match(r'^team#\d+$', events.get('Q', '')):
885 team = int(events.get('Q').replace('team#', ''))
888 # gametype-specific stuff is handled here. if passed to us, we store it
889 for (key,value) in events.items():
890 if key == 'scoreboard-score': teamstat.score = int(round(float(value)))
891 if key == 'scoreboard-caps': teamstat.caps = int(value)
892 if key == 'scoreboard-goals': teamstat.caps = int(value)
893 if key == 'scoreboard-rounds': teamstat.rounds = int(value)
895 session.add(teamstat)
896 except Exception as e:
902 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
903 """Weapon stats handler for all game types"""
906 # Version 1 of stats submissions doubled the data sent.
907 # To counteract this we divide the data by 2 only for
908 # POSTs coming from version 1.
910 version = int(game_meta['V'])
913 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
919 for (key,value) in events.items():
920 matched = re.search("acc-(.*?)-cnt-fired", key)
922 weapon_cd = matched.group(1)
924 # Weapon names changed for 0.8. We'll convert the old
925 # ones to use the new scheme as well.
926 mapped_weapon_cd = weapon_map.get(weapon_cd, weapon_cd)
928 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
929 pwstat_id = session.execute(seq)
930 pwstat = PlayerWeaponStat()
931 pwstat.player_weapon_stats_id = pwstat_id
932 pwstat.player_id = player.player_id
933 pwstat.game_id = game.game_id
934 pwstat.player_game_stat_id = pgstat.player_game_stat_id
935 pwstat.weapon_cd = mapped_weapon_cd
938 pwstat.nick = events['n']
940 pwstat.nick = events['P']
942 if 'acc-' + weapon_cd + '-cnt-fired' in events:
943 pwstat.fired = int(round(float(
944 events['acc-' + weapon_cd + '-cnt-fired'])))
945 if 'acc-' + weapon_cd + '-fired' in events:
946 pwstat.max = int(round(float(
947 events['acc-' + weapon_cd + '-fired'])))
948 if 'acc-' + weapon_cd + '-cnt-hit' in events:
949 pwstat.hit = int(round(float(
950 events['acc-' + weapon_cd + '-cnt-hit'])))
951 if 'acc-' + weapon_cd + '-hit' in events:
952 pwstat.actual = int(round(float(
953 events['acc-' + weapon_cd + '-hit'])))
954 if 'acc-' + weapon_cd + '-frags' in events:
955 pwstat.frags = int(round(float(
956 events['acc-' + weapon_cd + '-frags'])))
959 pwstat.fired = pwstat.fired/2
960 pwstat.max = pwstat.max/2
961 pwstat.hit = pwstat.hit/2
962 pwstat.actual = pwstat.actual/2
963 pwstat.frags = pwstat.frags/2
966 pwstats.append(pwstat)
971 def get_ranks(session, player_ids, game_type_cd):
973 Gets the rank entries for all players in the given list, returning a dict
974 of player_id -> PlayerRank instance. The rank entry corresponds to the
975 game type of the parameter passed in as well.
978 for pr in session.query(PlayerRank).\
979 filter(PlayerRank.player_id.in_(player_ids)).\
980 filter(PlayerRank.game_type_cd == game_type_cd).\
982 ranks[pr.player_id] = pr
987 def submit_stats(request):
989 Entry handler for POST stats submissions.
992 # placeholder for the actual session
995 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
996 "----- END REQUEST BODY -----\n\n")
998 (idfp, status) = verify_request(request)
999 (game_meta, raw_players, raw_teams) = parse_stats_submission(request.body)
1000 revision = game_meta.get('R', 'unknown')
1001 duration = game_meta.get('D', None)
1003 # only players present at the end of the match are eligible for stats
1004 raw_players = filter(played_in_game, raw_players)
1006 do_precondition_checks(request, game_meta, raw_players)
1008 # the "duel" gametype is fake
1009 if len(raw_players) == 2 \
1010 and num_real_players(raw_players) == 2 \
1011 and game_meta['G'] == 'dm':
1012 game_meta['G'] = 'duel'
1014 #----------------------------------------------------------------------
1015 # Actual setup (inserts/updates) below here
1016 #----------------------------------------------------------------------
1017 session = DBSession()
1019 game_type_cd = game_meta['G']
1021 # All game types create Game, Server, Map, and Player records
1023 server = get_or_create_server(
1026 name = game_meta['S'],
1027 revision = revision,
1028 ip_addr = get_remote_addr(request),
1029 port = game_meta.get('U', None),
1030 impure_cvars = game_meta.get('C', 0))
1032 gmap = get_or_create_map(
1034 name = game_meta['M'])
1038 start_dt = datetime.datetime.utcnow(),
1039 server_id = server.server_id,
1040 game_type_cd = game_type_cd,
1041 map_id = gmap.map_id,
1042 match_id = game_meta['I'],
1043 duration = duration,
1044 mod = game_meta.get('O', None))
1046 # keep track of the players we've seen
1050 for events in raw_players:
1051 player = get_or_create_player(
1053 hashkey = events['P'],
1054 nick = events.get('n', None))
1056 pgstat = create_game_stat(session, game_meta, game, server,
1057 gmap, player, events)
1058 pgstats.append(pgstat)
1060 if player.player_id > 1:
1061 anticheats = create_anticheats(session, pgstat, game, player, events)
1063 if player.player_id > 2:
1064 player_ids.append(player.player_id)
1065 hashkeys[player.player_id] = events['P']
1067 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
1068 pwstats = create_weapon_stats(session, game_meta, game, player,
1071 # store them on games for easy access
1072 game.players = player_ids
1074 for events in raw_teams:
1076 teamstat = create_team_stat(session, game, events)
1077 except Exception as e:
1080 if server.elo_ind and gametype_elo_eligible(game_type_cd):
1081 ep = EloProcessor(session, game, pgstats)
1085 log.debug('Success! Stats recorded.')
1087 # ranks are fetched after we've done the "real" processing
1088 ranks = get_ranks(session, player_ids, game_type_cd)
1090 # plain text response
1091 request.response.content_type = 'text/plain'
1094 "now" : calendar.timegm(datetime.datetime.utcnow().timetuple()),
1098 "player_ids" : player_ids,
1099 "hashkeys" : hashkeys,
1104 except Exception as e: