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 def is_real_player(events):
21 Determines if a given set of events correspond with a non-bot
23 if not events['P'].startswith('bot'):
29 def played_in_game(events):
31 Determines if a given set of player events correspond with a player who
32 played in the game (matches 1 and scoreboardvalid 1)
34 if 'matches' in events and 'scoreboardvalid' in events:
40 class Submission(object):
41 """Parses an incoming POST request for stats submissions."""
43 def __init__(self, body, headers):
44 # a copy of the HTTP headers
45 self.headers = headers
47 # a copy of the HTTP POST body
59 # distinct weapons that we have seen fired
62 # number of real players in the match
65 # the parsing deque (we use this to allow peeking)
66 self.q = collections.deque(self.body.split("\n"))
69 """Returns the next key:value pair off the queue."""
71 items = self.q.popleft().strip().split(' ', 1)
73 # Some keys won't have values, like 'L' records where the server isn't actually
74 # participating in any ladders. These can be safely ignored.
81 def check_for_new_weapon_fired(self, sub_key):
82 """Checks if a given player key (subkey, actually) is a new weapon fired in the match."""
83 if sub_key.endswith("cnt-fired"):
84 weapon = sub_key.split("-")[1]
85 if weapon not in self.weapons:
86 self.weapons.add(weapon)
88 def parse_player(self, key, pid):
89 """Construct a player events listing from the submission."""
91 # all of the keys related to player records
92 player_keys = ['i', 'n', 't', 'e']
96 # Consume all following 'i' 'n' 't' 'e' records
97 while len(self.q) > 0:
98 (key, value) = self.next_item()
99 if key is None and value is None:
102 (sub_key, sub_value) = value.split(' ', 1)
103 player[sub_key] = sub_value
105 # keep track of the distinct weapons fired during the match
106 self.check_for_new_weapon_fired(sub_key)
108 player[key] = unicode(value, 'utf-8')
109 elif key in player_keys:
112 # something we didn't expect - put it back on the deque
113 self.q.appendleft("{} {}".format(key, value))
116 if is_real_player(player) and played_in_game(player):
117 self.real_players += 1
119 self.players.append(player)
121 def parse_team(self, key, tid):
122 """Construct a team events listing from the submission."""
125 # Consume all following 'e' records
126 while len(self.q) > 0 and self.q[0].startswith('e'):
127 (_, value) = self.next_item()
128 (sub_key, sub_value) = value.split(' ', 1)
129 team[sub_key] = sub_value
131 self.teams.append(team)
134 """Parses the request body into instance variables."""
135 while len(self.q) > 0:
136 (key, value) = self.next_item()
137 if key is None and value is None:
140 self.meta[key] = unicode(value, 'utf-8')
142 self.parse_player(key, value)
144 self.parse_team(key, value)
146 self.meta[key] = value
151 def elo_submission_category(submission):
152 """Determines the Elo category purely by what is in the submission data."""
153 mod = submission.meta.get("O", "None")
155 vanilla_allowed_weapons = {"shotgun", "devastator", "blaster", "mortar", "vortex", "electro",
156 "arc", "hagar", "crylink", "machinegun"}
157 insta_allowed_weapons = {"vaporizer", "blaster"}
158 overkill_allowed_weapons = {"hmg", "vortex", "shotgun", "blaster", "machinegun", "rpc"}
161 if len(submission.weapons - vanilla_allowed_weapons) == 0:
163 elif mod == "InstaGib":
164 if len(submission.weapons - insta_allowed_weapons) == 0:
166 elif mod == "Overkill":
167 if len(submission.weapons - overkill_allowed_weapons) == 0:
175 def parse_stats_submission(body):
177 Parses the POST request body for a stats submission
179 # storage vars for the request body
185 # we're not in either stanza to start
188 for line in body.split('\n'):
190 (key, value) = line.strip().split(' ', 1)
192 # Server (S) and Nick (n) fields can have international characters.
194 value = unicode(value, 'utf-8')
196 if key not in 'P' 'Q' 'n' 'e' 't' 'i':
197 game_meta[key] = value
199 if key == 'Q' or key == 'P':
200 #log.debug('Found a {0}'.format(key))
201 #log.debug('in_Q: {0}'.format(in_Q))
202 #log.debug('in_P: {0}'.format(in_P))
203 #log.debug('events: {0}'.format(events))
205 # check where we were before and append events accordingly
206 if in_Q and len(events) > 0:
207 #log.debug('creating a team (Q) entry')
210 elif in_P and len(events) > 0:
211 #log.debug('creating a player (P) entry')
212 players.append(events)
216 #log.debug('key == P')
220 #log.debug('key == Q')
227 (subkey, subvalue) = value.split(' ', 1)
228 events[subkey] = subvalue
234 # no key/value pair - move on to the next line
237 # add the last entity we were working on
238 if in_P and len(events) > 0:
239 players.append(events)
240 elif in_Q and len(events) > 0:
243 return (game_meta, players, teams)
246 def is_blank_game(gametype, players):
247 """Determine if this is a blank game or not. A blank game is either:
249 1) a match that ended in the warmup stage, where accuracy events are not
250 present (for non-CTS games)
252 2) a match in which no player made a positive or negative score AND was
255 ... or for CTS, which doesn't record accuracy events
257 1) a match in which no player made a fastest lap AND was
260 ... or for NB, in which not all maps have weapons
262 1) a match in which no player made a positive or negative score
264 r = re.compile(r'acc-.*-cnt-fired')
265 flg_nonzero_score = False
266 flg_acc_events = False
267 flg_fastest_lap = False
269 for events in players:
270 if is_real_player(events) and played_in_game(events):
271 for (key,value) in events.items():
272 if key == 'scoreboard-score' and value != 0:
273 flg_nonzero_score = True
275 flg_acc_events = True
276 if key == 'scoreboard-fastest':
277 flg_fastest_lap = True
279 if gametype == 'cts':
280 return not flg_fastest_lap
281 elif gametype == 'nb':
282 return not flg_nonzero_score
284 return not (flg_nonzero_score and flg_acc_events)
287 def get_remote_addr(request):
288 """Get the Xonotic server's IP address"""
289 if 'X-Forwarded-For' in request.headers:
290 return request.headers['X-Forwarded-For']
292 return request.remote_addr
295 def is_supported_gametype(gametype, version):
296 """Whether a gametype is supported or not"""
299 # if the type can be supported, but with version constraints, uncomment
300 # here and add the restriction for a specific version below
301 supported_game_types = (
320 if gametype in supported_game_types:
325 # some game types were buggy before revisions, thus this additional filter
326 if gametype == 'ca' and version <= 5:
332 def do_precondition_checks(request, game_meta, raw_players):
333 """Precondition checks for ALL gametypes.
334 These do not require a database connection."""
335 if not has_required_metadata(game_meta):
336 msg = "Missing required game metadata"
338 raise pyramid.httpexceptions.HTTPUnprocessableEntity(
340 content_type="text/plain"
344 version = int(game_meta['V'])
346 msg = "Invalid or incorrect game metadata provided"
348 raise pyramid.httpexceptions.HTTPUnprocessableEntity(
350 content_type="text/plain"
353 if not is_supported_gametype(game_meta['G'], version):
354 msg = "Unsupported game type ({})".format(game_meta['G'])
356 raise pyramid.httpexceptions.HTTPOk(
358 content_type="text/plain"
361 if not has_minimum_real_players(request.registry.settings, raw_players):
362 msg = "Not enough real players"
364 raise pyramid.httpexceptions.HTTPOk(
366 content_type="text/plain"
369 if is_blank_game(game_meta['G'], raw_players):
372 raise pyramid.httpexceptions.HTTPOk(
374 content_type="text/plain"
378 def num_real_players(player_events):
380 Returns the number of real players (those who played
381 and are on the scoreboard).
385 for events in player_events:
386 if is_real_player(events) and played_in_game(events):
392 def has_minimum_real_players(settings, player_events):
394 Determines if the collection of player events has enough "real" players
395 to store in the database. The minimum setting comes from the config file
396 under the setting xonstat.minimum_real_players.
398 flg_has_min_real_players = True
401 minimum_required_players = int(
402 settings['xonstat.minimum_required_players'])
404 minimum_required_players = 2
406 real_players = num_real_players(player_events)
408 if real_players < minimum_required_players:
409 flg_has_min_real_players = False
411 return flg_has_min_real_players
414 def has_required_metadata(metadata):
416 Determines if a give set of metadata has enough data to create a game,
417 server, and map with.
419 flg_has_req_metadata = True
421 if 'G' not in metadata or\
422 'M' not in metadata or\
423 'I' not in metadata or\
425 flg_has_req_metadata = False
427 return flg_has_req_metadata
430 def should_do_weapon_stats(game_type_cd):
431 """True of the game type should record weapon stats. False otherwise."""
432 if game_type_cd in 'cts':
438 def gametype_elo_eligible(game_type_cd):
439 """True of the game type should process Elos. False otherwise."""
440 elo_game_types = ('duel', 'dm', 'ca', 'ctf', 'tdm', 'ka', 'ft')
442 if game_type_cd in elo_game_types:
448 def register_new_nick(session, player, new_nick):
450 Change the player record's nick to the newly found nick. Store the old
451 nick in the player_nicks table for that player.
453 session - SQLAlchemy database session factory
454 player - player record whose nick is changing
455 new_nick - the new nickname
457 # see if that nick already exists
458 stripped_nick = strip_colors(qfont_decode(player.nick))
460 player_nick = session.query(PlayerNick).filter_by(
461 player_id=player.player_id, stripped_nick=stripped_nick).one()
462 except NoResultFound, e:
463 # player_id/stripped_nick not found, create one
464 # but we don't store "Anonymous Player #N"
465 if not re.search('^Anonymous Player #\d+$', player.nick):
466 player_nick = PlayerNick()
467 player_nick.player_id = player.player_id
468 player_nick.stripped_nick = stripped_nick
469 player_nick.nick = player.nick
470 session.add(player_nick)
472 # We change to the new nick regardless
473 player.nick = new_nick
474 player.stripped_nick = strip_colors(qfont_decode(new_nick))
478 def update_fastest_cap(session, player_id, game_id, map_id, captime, mod):
480 Check the fastest cap time for the player and map. If there isn't
481 one, insert one. If there is, check if the passed time is faster.
484 # we don't record fastest cap times for bots or anonymous players
488 # see if a cap entry exists already
489 # then check to see if the new captime is faster
491 cur_fastest_cap = session.query(PlayerCaptime).filter_by(
492 player_id=player_id, map_id=map_id, mod=mod).one()
494 # current captime is faster, so update
495 if captime < cur_fastest_cap.fastest_cap:
496 cur_fastest_cap.fastest_cap = captime
497 cur_fastest_cap.game_id = game_id
498 cur_fastest_cap.create_dt = datetime.datetime.utcnow()
499 session.add(cur_fastest_cap)
501 except NoResultFound, e:
502 # none exists, so insert
503 cur_fastest_cap = PlayerCaptime(player_id, game_id, map_id, captime,
505 session.add(cur_fastest_cap)
509 def update_server(server, name, hashkey, ip_addr, port, revision, impure_cvars):
511 Updates the server in the given DB session, if needed.
513 :param server: The found server instance.
514 :param name: The incoming server name.
515 :param hashkey: The incoming server hashkey.
516 :param ip_addr: The incoming server IP address.
517 :param port: The incoming server port.
518 :param revision: The incoming server revision.
519 :param impure_cvars: The incoming number of impure server cvars.
522 # ensure the two int attributes are actually ints
529 impure_cvars = int(impure_cvars)
534 if name and server.name != name:
537 if hashkey and server.hashkey != hashkey:
538 server.hashkey = hashkey
540 if ip_addr and server.ip_addr != ip_addr:
541 server.ip_addr = ip_addr
543 if port and server.port != port:
546 if revision and server.revision != revision:
547 server.revision = revision
549 if impure_cvars and server.impure_cvars != impure_cvars:
550 server.impure_cvars = impure_cvars
551 server.pure_ind = True if impure_cvars == 0 else False
557 def get_or_create_server(session, name, hashkey, ip_addr, revision, port, impure_cvars):
559 Find a server by name or create one if not found. Parameters:
561 session - SQLAlchemy database session factory
562 name - server name of the server to be found or created
563 hashkey - server hashkey
564 ip_addr - the IP address of the server
565 revision - the xonotic revision number
566 port - the port number of the server
567 impure_cvars - the number of impure cvar changes
569 servers_q = DBSession.query(Server).filter(Server.active_ind)
572 # if the hashkey is provided, we'll use that
573 servers_q = servers_q.filter((Server.name == name) or (Server.hashkey == hashkey))
575 # otherwise, it is just by name
576 servers_q = servers_q.filter(Server.name == name)
578 # order by the hashkey, which means any hashkey match will appear first if there are multiple
579 servers = servers_q.order_by(Server.hashkey, Server.create_dt).all()
581 if len(servers) == 0:
582 server = Server(name=name, hashkey=hashkey)
585 log.debug("Created server {} with hashkey {}.".format(server.server_id, server.hashkey))
588 if len(servers) == 1:
589 log.info("Found existing server {}.".format(server.server_id))
591 elif len(servers) > 1:
592 server_id_list = ", ".join(["{}".format(s.server_id) for s in servers])
593 log.warn("Multiple servers found ({})! Using the first one ({})."
594 .format(server_id_list, server.server_id))
596 if update_server(server, name, hashkey, ip_addr, port, revision, impure_cvars):
602 def get_or_create_map(session=None, name=None):
604 Find a map by name or create one if not found. Parameters:
606 session - SQLAlchemy database session factory
607 name - map name of the map to be found or created
610 # find one by the name, if it exists
611 gmap = session.query(Map).filter_by(name=name).one()
612 log.debug("Found map id {0}: {1}".format(gmap.map_id,
614 except NoResultFound, e:
615 gmap = Map(name=name)
618 log.debug("Created map id {0}: {1}".format(gmap.map_id,
620 except MultipleResultsFound, e:
621 # multiple found, so use the first one but warn
623 gmaps = session.query(Map).filter_by(name=name).order_by(
626 log.debug("Found map id {0}: {1} but found \
627 multiple".format(gmap.map_id, gmap.name))
632 def create_game(session, start_dt, game_type_cd, server_id, map_id,
633 match_id, duration, mod, winner=None):
635 Creates a game. Parameters:
637 session - SQLAlchemy database session factory
638 start_dt - when the game started (datetime object)
639 game_type_cd - the game type of the game being played
640 server_id - server identifier of the server hosting the game
641 map_id - map on which the game was played
642 winner - the team id of the team that won
643 duration - how long the game lasted
644 mod - mods in use during the game
646 seq = Sequence('games_game_id_seq')
647 game_id = session.execute(seq)
648 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
649 server_id=server_id, map_id=map_id, winner=winner)
650 game.match_id = match_id
653 # There is some drift between start_dt (provided by app) and create_dt
654 # (default in the database), so we'll make them the same until this is
656 game.create_dt = start_dt
659 game.duration = datetime.timedelta(seconds=int(round(float(duration))))
664 session.query(Game).filter(Game.server_id==server_id).\
665 filter(Game.match_id==match_id).one()
667 log.debug("Error: game with same server and match_id found! Ignoring.")
669 # if a game under the same server and match_id found,
670 # this is a duplicate game and can be ignored
671 raise pyramid.httpexceptions.HTTPOk('OK')
672 except NoResultFound, e:
673 # server_id/match_id combination not found. game is ok to insert
676 log.debug("Created game id {0} on server {1}, map {2} at \
677 {3}".format(game.game_id,
678 server_id, map_id, start_dt))
683 def get_or_create_player(session=None, hashkey=None, nick=None):
685 Finds a player by hashkey or creates a new one (along with a
686 corresponding hashkey entry. Parameters:
688 session - SQLAlchemy database session factory
689 hashkey - hashkey of the player to be found or created
690 nick - nick of the player (in case of a first time create)
693 if re.search('^bot#\d+', hashkey):
694 player = session.query(Player).filter_by(player_id=1).one()
695 # if we have an untracked player
696 elif re.search('^player#\d+$', hashkey):
697 player = session.query(Player).filter_by(player_id=2).one()
698 # else it is a tracked player
700 # see if the player is already in the database
701 # if not, create one and the hashkey along with it
703 hk = session.query(Hashkey).filter_by(
704 hashkey=hashkey).one()
705 player = session.query(Player).filter_by(
706 player_id=hk.player_id).one()
707 log.debug("Found existing player {0} with hashkey {1}".format(
708 player.player_id, hashkey))
714 # if nick is given to us, use it. If not, use "Anonymous Player"
715 # with a suffix added for uniqueness.
717 player.nick = nick[:128]
718 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
720 player.nick = "Anonymous Player #{0}".format(player.player_id)
721 player.stripped_nick = player.nick
723 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
725 log.debug("Created player {0} ({2}) with hashkey {1}".format(
726 player.player_id, hashkey, player.nick.encode('utf-8')))
731 def create_default_game_stat(session, game_type_cd):
732 """Creates a blanked-out pgstat record for the given game type"""
734 # this is what we have to do to get partitioned records in - grab the
735 # sequence value first, then insert using the explicit ID (vs autogenerate)
736 seq = Sequence('player_game_stats_player_game_stat_id_seq')
737 pgstat_id = session.execute(seq)
738 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
739 create_dt=datetime.datetime.utcnow())
741 if game_type_cd == 'as':
742 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
744 if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
745 pgstat.kills = pgstat.deaths = pgstat.suicides = 0
747 if game_type_cd == 'cq':
748 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
751 if game_type_cd == 'ctf':
752 pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
753 pgstat.returns = pgstat.carrier_frags = 0
755 if game_type_cd == 'cts':
758 if game_type_cd == 'dom':
759 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
762 if game_type_cd == 'ft':
763 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
765 if game_type_cd == 'ka':
766 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
767 pgstat.carrier_frags = 0
768 pgstat.time = datetime.timedelta(seconds=0)
770 if game_type_cd == 'kh':
771 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
772 pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
773 pgstat.carrier_frags = 0
775 if game_type_cd == 'lms':
776 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
778 if game_type_cd == 'nb':
779 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
782 if game_type_cd == 'rc':
783 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
788 def create_game_stat(session, game_meta, game, server, gmap, player, events):
789 """Game stats handler for all game types"""
791 game_type_cd = game.game_type_cd
793 pgstat = create_default_game_stat(session, game_type_cd)
795 # these fields should be on every pgstat record
796 pgstat.game_id = game.game_id
797 pgstat.player_id = player.player_id
798 pgstat.nick = events.get('n', 'Anonymous Player')[:128]
799 pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
800 pgstat.score = int(round(float(events.get('scoreboard-score', 0))))
801 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
802 pgstat.rank = int(events.get('rank', None))
803 pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
805 if pgstat.nick != player.nick \
806 and player.player_id > 2 \
807 and pgstat.nick != 'Anonymous Player':
808 register_new_nick(session, player, pgstat.nick)
812 # gametype-specific stuff is handled here. if passed to us, we store it
813 for (key,value) in events.items():
814 if key == 'wins': wins = True
815 if key == 't': pgstat.team = int(value)
817 if key == 'scoreboard-drops': pgstat.drops = int(value)
818 if key == 'scoreboard-returns': pgstat.returns = int(value)
819 if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
820 if key == 'scoreboard-pickups': pgstat.pickups = int(value)
821 if key == 'scoreboard-caps': pgstat.captures = int(value)
822 if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
823 if key == 'scoreboard-deaths': pgstat.deaths = int(value)
824 if key == 'scoreboard-kills': pgstat.kills = int(value)
825 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
826 if key == 'scoreboard-objectives': pgstat.collects = int(value)
827 if key == 'scoreboard-captured': pgstat.captures = int(value)
828 if key == 'scoreboard-released': pgstat.drops = int(value)
829 if key == 'scoreboard-fastest':
830 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
831 if key == 'scoreboard-takes': pgstat.pickups = int(value)
832 if key == 'scoreboard-ticks': pgstat.drops = int(value)
833 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
834 if key == 'scoreboard-bctime':
835 pgstat.time = datetime.timedelta(seconds=int(value))
836 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
837 if key == 'scoreboard-losses': pgstat.drops = int(value)
838 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
839 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
840 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
841 if key == 'scoreboard-lives': pgstat.lives = int(value)
842 if key == 'scoreboard-goals': pgstat.captures = int(value)
843 if key == 'scoreboard-faults': pgstat.drops = int(value)
844 if key == 'scoreboard-laps': pgstat.laps = int(value)
846 if key == 'avglatency': pgstat.avg_latency = float(value)
847 if key == 'scoreboard-captime':
848 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
849 if game.game_type_cd == 'ctf':
850 update_fastest_cap(session, player.player_id, game.game_id,
851 gmap.map_id, pgstat.fastest, game.mod)
853 # there is no "winning team" field, so we have to derive it
854 if wins and pgstat.team is not None and game.winner is None:
855 game.winner = pgstat.team
863 def create_anticheats(session, pgstat, game, player, events):
864 """Anticheats handler for all game types"""
868 # all anticheat events are prefixed by "anticheat"
869 for (key,value) in events.items():
870 if key.startswith("anticheat"):
872 ac = PlayerGameAnticheat(
878 anticheats.append(ac)
880 except Exception as e:
881 log.debug("Could not parse value for key %s. Ignoring." % key)
886 def create_default_team_stat(session, game_type_cd):
887 """Creates a blanked-out teamstat record for the given game type"""
889 # this is what we have to do to get partitioned records in - grab the
890 # sequence value first, then insert using the explicit ID (vs autogenerate)
891 seq = Sequence('team_game_stats_team_game_stat_id_seq')
892 teamstat_id = session.execute(seq)
893 teamstat = TeamGameStat(team_game_stat_id=teamstat_id,
894 create_dt=datetime.datetime.utcnow())
896 # all team game modes have a score, so we'll zero that out always
899 if game_type_cd in 'ca' 'ft' 'lms' 'ka':
902 if game_type_cd == 'ctf':
908 def create_team_stat(session, game, events):
909 """Team stats handler for all game types"""
912 teamstat = create_default_team_stat(session, game.game_type_cd)
913 teamstat.game_id = game.game_id
915 # we should have a team ID if we have a 'Q' event
916 if re.match(r'^team#\d+$', events.get('Q', '')):
917 team = int(events.get('Q').replace('team#', ''))
920 # gametype-specific stuff is handled here. if passed to us, we store it
921 for (key,value) in events.items():
922 if key == 'scoreboard-score': teamstat.score = int(round(float(value)))
923 if key == 'scoreboard-caps': teamstat.caps = int(value)
924 if key == 'scoreboard-goals': teamstat.caps = int(value)
925 if key == 'scoreboard-rounds': teamstat.rounds = int(value)
927 session.add(teamstat)
928 except Exception as e:
934 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
935 """Weapon stats handler for all game types"""
938 # Version 1 of stats submissions doubled the data sent.
939 # To counteract this we divide the data by 2 only for
940 # POSTs coming from version 1.
942 version = int(game_meta['V'])
945 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
951 for (key,value) in events.items():
952 matched = re.search("acc-(.*?)-cnt-fired", key)
954 weapon_cd = matched.group(1)
956 # Weapon names changed for 0.8. We'll convert the old
957 # ones to use the new scheme as well.
958 mapped_weapon_cd = weapon_map.get(weapon_cd, weapon_cd)
960 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
961 pwstat_id = session.execute(seq)
962 pwstat = PlayerWeaponStat()
963 pwstat.player_weapon_stats_id = pwstat_id
964 pwstat.player_id = player.player_id
965 pwstat.game_id = game.game_id
966 pwstat.player_game_stat_id = pgstat.player_game_stat_id
967 pwstat.weapon_cd = mapped_weapon_cd
970 pwstat.nick = events['n']
972 pwstat.nick = events['P']
974 if 'acc-' + weapon_cd + '-cnt-fired' in events:
975 pwstat.fired = int(round(float(
976 events['acc-' + weapon_cd + '-cnt-fired'])))
977 if 'acc-' + weapon_cd + '-fired' in events:
978 pwstat.max = int(round(float(
979 events['acc-' + weapon_cd + '-fired'])))
980 if 'acc-' + weapon_cd + '-cnt-hit' in events:
981 pwstat.hit = int(round(float(
982 events['acc-' + weapon_cd + '-cnt-hit'])))
983 if 'acc-' + weapon_cd + '-hit' in events:
984 pwstat.actual = int(round(float(
985 events['acc-' + weapon_cd + '-hit'])))
986 if 'acc-' + weapon_cd + '-frags' in events:
987 pwstat.frags = int(round(float(
988 events['acc-' + weapon_cd + '-frags'])))
991 pwstat.fired = pwstat.fired/2
992 pwstat.max = pwstat.max/2
993 pwstat.hit = pwstat.hit/2
994 pwstat.actual = pwstat.actual/2
995 pwstat.frags = pwstat.frags/2
998 pwstats.append(pwstat)
1003 def get_ranks(session, player_ids, game_type_cd):
1005 Gets the rank entries for all players in the given list, returning a dict
1006 of player_id -> PlayerRank instance. The rank entry corresponds to the
1007 game type of the parameter passed in as well.
1010 for pr in session.query(PlayerRank).\
1011 filter(PlayerRank.player_id.in_(player_ids)).\
1012 filter(PlayerRank.game_type_cd == game_type_cd).\
1014 ranks[pr.player_id] = pr
1019 def submit_stats(request):
1021 Entry handler for POST stats submissions.
1024 # placeholder for the actual session
1027 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
1028 "----- END REQUEST BODY -----\n\n")
1030 (idfp, status) = verify_request(request)
1031 (game_meta, raw_players, raw_teams) = parse_stats_submission(request.body)
1032 revision = game_meta.get('R', 'unknown')
1033 duration = game_meta.get('D', None)
1035 # only players present at the end of the match are eligible for stats
1036 raw_players = filter(played_in_game, raw_players)
1038 do_precondition_checks(request, game_meta, raw_players)
1040 # the "duel" gametype is fake
1041 if len(raw_players) == 2 \
1042 and num_real_players(raw_players) == 2 \
1043 and game_meta['G'] == 'dm':
1044 game_meta['G'] = 'duel'
1046 #----------------------------------------------------------------------
1047 # Actual setup (inserts/updates) below here
1048 #----------------------------------------------------------------------
1049 session = DBSession()
1051 game_type_cd = game_meta['G']
1053 # All game types create Game, Server, Map, and Player records
1055 server = get_or_create_server(
1058 name = game_meta['S'],
1059 revision = revision,
1060 ip_addr = get_remote_addr(request),
1061 port = game_meta.get('U', None),
1062 impure_cvars = game_meta.get('C', 0))
1064 gmap = get_or_create_map(
1066 name = game_meta['M'])
1070 start_dt = datetime.datetime.utcnow(),
1071 server_id = server.server_id,
1072 game_type_cd = game_type_cd,
1073 map_id = gmap.map_id,
1074 match_id = game_meta['I'],
1075 duration = duration,
1076 mod = game_meta.get('O', None))
1078 # keep track of the players we've seen
1082 for events in raw_players:
1083 player = get_or_create_player(
1085 hashkey = events['P'],
1086 nick = events.get('n', None))
1088 pgstat = create_game_stat(session, game_meta, game, server,
1089 gmap, player, events)
1090 pgstats.append(pgstat)
1092 if player.player_id > 1:
1093 anticheats = create_anticheats(session, pgstat, game, player, events)
1095 if player.player_id > 2:
1096 player_ids.append(player.player_id)
1097 hashkeys[player.player_id] = events['P']
1099 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
1100 pwstats = create_weapon_stats(session, game_meta, game, player,
1103 # store them on games for easy access
1104 game.players = player_ids
1106 for events in raw_teams:
1108 teamstat = create_team_stat(session, game, events)
1109 except Exception as e:
1112 if server.elo_ind and gametype_elo_eligible(game_type_cd):
1113 ep = EloProcessor(session, game, pgstats)
1117 log.debug('Success! Stats recorded.')
1119 # ranks are fetched after we've done the "real" processing
1120 ranks = get_ranks(session, player_ids, game_type_cd)
1122 # plain text response
1123 request.response.content_type = 'text/plain'
1126 "now" : calendar.timegm(datetime.datetime.utcnow().timetuple()),
1130 "player_ids" : player_ids,
1131 "hashkeys" : hashkeys,
1136 except Exception as e: