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
53 # humans and bots in the match (including spectators)
56 # humans who played in the match
59 # bots who played in the match
65 # distinct weapons that we have seen fired
68 # the parsing deque (we use this to allow peeking)
69 self.q = collections.deque(self.body.split("\n"))
72 """Returns the next key:value pair off the queue."""
74 items = self.q.popleft().strip().split(' ', 1)
76 # Some keys won't have values, like 'L' records where the server isn't actually
77 # participating in any ladders. These can be safely ignored.
84 def check_for_new_weapon_fired(self, sub_key):
85 """Checks if a given player key (subkey, actually) is a new weapon fired in the match."""
86 if sub_key.endswith("cnt-fired"):
87 weapon = sub_key.split("-")[1]
88 if weapon not in self.weapons:
89 self.weapons.add(weapon)
91 def parse_player(self, key, pid):
92 """Construct a player events listing from the submission."""
94 # all of the keys related to player records
95 player_keys = ['i', 'n', 't', 'e']
99 # Consume all following 'i' 'n' 't' 'e' records
100 while len(self.q) > 0:
101 (key, value) = self.next_item()
102 if key is None and value is None:
105 (sub_key, sub_value) = value.split(' ', 1)
106 player[sub_key] = sub_value
108 # keep track of the distinct weapons fired during the match
109 self.check_for_new_weapon_fired(sub_key)
111 player[key] = unicode(value, 'utf-8')
112 elif key in player_keys:
115 # something we didn't expect - put it back on the deque
116 self.q.appendleft("{} {}".format(key, value))
119 played = played_in_game(player)
120 human = is_real_player(player)
122 self.humans.append(player)
123 elif played and not human:
124 self.bots.append(player)
126 self.players.append(player)
128 def parse_team(self, key, tid):
129 """Construct a team events listing from the submission."""
132 # Consume all following 'e' records
133 while len(self.q) > 0 and self.q[0].startswith('e'):
134 (_, value) = self.next_item()
135 (sub_key, sub_value) = value.split(' ', 1)
136 team[sub_key] = sub_value
138 self.teams.append(team)
141 """Parses the request body into instance variables."""
142 while len(self.q) > 0:
143 (key, value) = self.next_item()
144 if key is None and value is None:
147 self.meta[key] = unicode(value, 'utf-8')
149 self.parse_player(key, value)
151 self.parse_team(key, value)
153 self.meta[key] = value
158 def elo_submission_category(submission):
159 """Determines the Elo category purely by what is in the submission data."""
160 mod = submission.meta.get("O", "None")
162 vanilla_allowed_weapons = {"shotgun", "devastator", "blaster", "mortar", "vortex", "electro",
163 "arc", "hagar", "crylink", "machinegun"}
164 insta_allowed_weapons = {"vaporizer", "blaster"}
165 overkill_allowed_weapons = {"hmg", "vortex", "shotgun", "blaster", "machinegun", "rpc"}
168 if len(submission.weapons - vanilla_allowed_weapons) == 0:
170 elif mod == "InstaGib":
171 if len(submission.weapons - insta_allowed_weapons) == 0:
173 elif mod == "Overkill":
174 if len(submission.weapons - overkill_allowed_weapons) == 0:
182 def parse_stats_submission(body):
184 Parses the POST request body for a stats submission
186 # storage vars for the request body
192 # we're not in either stanza to start
195 for line in body.split('\n'):
197 (key, value) = line.strip().split(' ', 1)
199 # Server (S) and Nick (n) fields can have international characters.
201 value = unicode(value, 'utf-8')
203 if key not in 'P' 'Q' 'n' 'e' 't' 'i':
204 game_meta[key] = value
206 if key == 'Q' or key == 'P':
207 #log.debug('Found a {0}'.format(key))
208 #log.debug('in_Q: {0}'.format(in_Q))
209 #log.debug('in_P: {0}'.format(in_P))
210 #log.debug('events: {0}'.format(events))
212 # check where we were before and append events accordingly
213 if in_Q and len(events) > 0:
214 #log.debug('creating a team (Q) entry')
217 elif in_P and len(events) > 0:
218 #log.debug('creating a player (P) entry')
219 players.append(events)
223 #log.debug('key == P')
227 #log.debug('key == Q')
234 (subkey, subvalue) = value.split(' ', 1)
235 events[subkey] = subvalue
241 # no key/value pair - move on to the next line
244 # add the last entity we were working on
245 if in_P and len(events) > 0:
246 players.append(events)
247 elif in_Q and len(events) > 0:
250 return (game_meta, players, teams)
253 def is_blank_game(gametype, players):
254 """Determine if this is a blank game or not. A blank game is either:
256 1) a match that ended in the warmup stage, where accuracy events are not
257 present (for non-CTS games)
259 2) a match in which no player made a positive or negative score AND was
262 ... or for CTS, which doesn't record accuracy events
264 1) a match in which no player made a fastest lap AND was
267 ... or for NB, in which not all maps have weapons
269 1) a match in which no player made a positive or negative score
271 r = re.compile(r'acc-.*-cnt-fired')
272 flg_nonzero_score = False
273 flg_acc_events = False
274 flg_fastest_lap = False
276 for events in players:
277 if is_real_player(events) and played_in_game(events):
278 for (key,value) in events.items():
279 if key == 'scoreboard-score' and value != 0:
280 flg_nonzero_score = True
282 flg_acc_events = True
283 if key == 'scoreboard-fastest':
284 flg_fastest_lap = True
286 if gametype == 'cts':
287 return not flg_fastest_lap
288 elif gametype == 'nb':
289 return not flg_nonzero_score
291 return not (flg_nonzero_score and flg_acc_events)
294 def get_remote_addr(request):
295 """Get the Xonotic server's IP address"""
296 if 'X-Forwarded-For' in request.headers:
297 return request.headers['X-Forwarded-For']
299 return request.remote_addr
302 def is_supported_gametype(gametype, version):
303 """Whether a gametype is supported or not"""
306 # if the type can be supported, but with version constraints, uncomment
307 # here and add the restriction for a specific version below
308 supported_game_types = (
327 if gametype in supported_game_types:
332 # some game types were buggy before revisions, thus this additional filter
333 if gametype == 'ca' and version <= 5:
339 def do_precondition_checks(request, game_meta, raw_players):
340 """Precondition checks for ALL gametypes.
341 These do not require a database connection."""
342 if not has_required_metadata(game_meta):
343 msg = "Missing required game metadata"
345 raise pyramid.httpexceptions.HTTPUnprocessableEntity(
347 content_type="text/plain"
351 version = int(game_meta['V'])
353 msg = "Invalid or incorrect game metadata provided"
355 raise pyramid.httpexceptions.HTTPUnprocessableEntity(
357 content_type="text/plain"
360 if not is_supported_gametype(game_meta['G'], version):
361 msg = "Unsupported game type ({})".format(game_meta['G'])
363 raise pyramid.httpexceptions.HTTPOk(
365 content_type="text/plain"
368 if not has_minimum_real_players(request.registry.settings, raw_players):
369 msg = "Not enough real players"
371 raise pyramid.httpexceptions.HTTPOk(
373 content_type="text/plain"
376 if is_blank_game(game_meta['G'], raw_players):
379 raise pyramid.httpexceptions.HTTPOk(
381 content_type="text/plain"
385 def num_real_players(player_events):
387 Returns the number of real players (those who played
388 and are on the scoreboard).
392 for events in player_events:
393 if is_real_player(events) and played_in_game(events):
399 def has_minimum_real_players(settings, player_events):
401 Determines if the collection of player events has enough "real" players
402 to store in the database. The minimum setting comes from the config file
403 under the setting xonstat.minimum_real_players.
405 flg_has_min_real_players = True
408 minimum_required_players = int(
409 settings['xonstat.minimum_required_players'])
411 minimum_required_players = 2
413 real_players = num_real_players(player_events)
415 if real_players < minimum_required_players:
416 flg_has_min_real_players = False
418 return flg_has_min_real_players
421 def has_required_metadata(metadata):
423 Determines if a give set of metadata has enough data to create a game,
424 server, and map with.
426 flg_has_req_metadata = True
428 if 'G' not in metadata or\
429 'M' not in metadata or\
430 'I' not in metadata or\
432 flg_has_req_metadata = False
434 return flg_has_req_metadata
437 def should_do_weapon_stats(game_type_cd):
438 """True of the game type should record weapon stats. False otherwise."""
439 if game_type_cd in 'cts':
445 def gametype_elo_eligible(game_type_cd):
446 """True of the game type should process Elos. False otherwise."""
447 elo_game_types = ('duel', 'dm', 'ca', 'ctf', 'tdm', 'ka', 'ft')
449 if game_type_cd in elo_game_types:
455 def register_new_nick(session, player, new_nick):
457 Change the player record's nick to the newly found nick. Store the old
458 nick in the player_nicks table for that player.
460 session - SQLAlchemy database session factory
461 player - player record whose nick is changing
462 new_nick - the new nickname
464 # see if that nick already exists
465 stripped_nick = strip_colors(qfont_decode(player.nick))
467 player_nick = session.query(PlayerNick).filter_by(
468 player_id=player.player_id, stripped_nick=stripped_nick).one()
469 except NoResultFound, e:
470 # player_id/stripped_nick not found, create one
471 # but we don't store "Anonymous Player #N"
472 if not re.search('^Anonymous Player #\d+$', player.nick):
473 player_nick = PlayerNick()
474 player_nick.player_id = player.player_id
475 player_nick.stripped_nick = stripped_nick
476 player_nick.nick = player.nick
477 session.add(player_nick)
479 # We change to the new nick regardless
480 player.nick = new_nick
481 player.stripped_nick = strip_colors(qfont_decode(new_nick))
485 def update_fastest_cap(session, player_id, game_id, map_id, captime, mod):
487 Check the fastest cap time for the player and map. If there isn't
488 one, insert one. If there is, check if the passed time is faster.
491 # we don't record fastest cap times for bots or anonymous players
495 # see if a cap entry exists already
496 # then check to see if the new captime is faster
498 cur_fastest_cap = session.query(PlayerCaptime).filter_by(
499 player_id=player_id, map_id=map_id, mod=mod).one()
501 # current captime is faster, so update
502 if captime < cur_fastest_cap.fastest_cap:
503 cur_fastest_cap.fastest_cap = captime
504 cur_fastest_cap.game_id = game_id
505 cur_fastest_cap.create_dt = datetime.datetime.utcnow()
506 session.add(cur_fastest_cap)
508 except NoResultFound, e:
509 # none exists, so insert
510 cur_fastest_cap = PlayerCaptime(player_id, game_id, map_id, captime,
512 session.add(cur_fastest_cap)
516 def update_server(server, name, hashkey, ip_addr, port, revision, impure_cvars):
518 Updates the server in the given DB session, if needed.
520 :param server: The found server instance.
521 :param name: The incoming server name.
522 :param hashkey: The incoming server hashkey.
523 :param ip_addr: The incoming server IP address.
524 :param port: The incoming server port.
525 :param revision: The incoming server revision.
526 :param impure_cvars: The incoming number of impure server cvars.
529 # ensure the two int attributes are actually ints
536 impure_cvars = int(impure_cvars)
541 if name and server.name != name:
544 if hashkey and server.hashkey != hashkey:
545 server.hashkey = hashkey
547 if ip_addr and server.ip_addr != ip_addr:
548 server.ip_addr = ip_addr
550 if port and server.port != port:
553 if revision and server.revision != revision:
554 server.revision = revision
556 if impure_cvars and server.impure_cvars != impure_cvars:
557 server.impure_cvars = impure_cvars
558 server.pure_ind = True if impure_cvars == 0 else False
564 def get_or_create_server(session, name, hashkey, ip_addr, revision, port, impure_cvars):
566 Find a server by name or create one if not found. Parameters:
568 session - SQLAlchemy database session factory
569 name - server name of the server to be found or created
570 hashkey - server hashkey
571 ip_addr - the IP address of the server
572 revision - the xonotic revision number
573 port - the port number of the server
574 impure_cvars - the number of impure cvar changes
576 servers_q = DBSession.query(Server).filter(Server.active_ind)
579 # if the hashkey is provided, we'll use that
580 servers_q = servers_q.filter((Server.name == name) or (Server.hashkey == hashkey))
582 # otherwise, it is just by name
583 servers_q = servers_q.filter(Server.name == name)
585 # order by the hashkey, which means any hashkey match will appear first if there are multiple
586 servers = servers_q.order_by(Server.hashkey, Server.create_dt).all()
588 if len(servers) == 0:
589 server = Server(name=name, hashkey=hashkey)
592 log.debug("Created server {} with hashkey {}.".format(server.server_id, server.hashkey))
595 if len(servers) == 1:
596 log.info("Found existing server {}.".format(server.server_id))
598 elif len(servers) > 1:
599 server_id_list = ", ".join(["{}".format(s.server_id) for s in servers])
600 log.warn("Multiple servers found ({})! Using the first one ({})."
601 .format(server_id_list, server.server_id))
603 if update_server(server, name, hashkey, ip_addr, port, revision, impure_cvars):
609 def get_or_create_map(session=None, name=None):
611 Find a map by name or create one if not found. Parameters:
613 session - SQLAlchemy database session factory
614 name - map name of the map to be found or created
617 # find one by the name, if it exists
618 gmap = session.query(Map).filter_by(name=name).one()
619 log.debug("Found map id {0}: {1}".format(gmap.map_id,
621 except NoResultFound, e:
622 gmap = Map(name=name)
625 log.debug("Created map id {0}: {1}".format(gmap.map_id,
627 except MultipleResultsFound, e:
628 # multiple found, so use the first one but warn
630 gmaps = session.query(Map).filter_by(name=name).order_by(
633 log.debug("Found map id {0}: {1} but found \
634 multiple".format(gmap.map_id, gmap.name))
639 def create_game(session, start_dt, game_type_cd, server_id, map_id,
640 match_id, duration, mod, winner=None):
642 Creates a game. Parameters:
644 session - SQLAlchemy database session factory
645 start_dt - when the game started (datetime object)
646 game_type_cd - the game type of the game being played
647 server_id - server identifier of the server hosting the game
648 map_id - map on which the game was played
649 winner - the team id of the team that won
650 duration - how long the game lasted
651 mod - mods in use during the game
653 seq = Sequence('games_game_id_seq')
654 game_id = session.execute(seq)
655 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
656 server_id=server_id, map_id=map_id, winner=winner)
657 game.match_id = match_id
660 # There is some drift between start_dt (provided by app) and create_dt
661 # (default in the database), so we'll make them the same until this is
663 game.create_dt = start_dt
666 game.duration = datetime.timedelta(seconds=int(round(float(duration))))
671 session.query(Game).filter(Game.server_id==server_id).\
672 filter(Game.match_id==match_id).one()
674 log.debug("Error: game with same server and match_id found! Ignoring.")
676 # if a game under the same server and match_id found,
677 # this is a duplicate game and can be ignored
678 raise pyramid.httpexceptions.HTTPOk('OK')
679 except NoResultFound, e:
680 # server_id/match_id combination not found. game is ok to insert
683 log.debug("Created game id {0} on server {1}, map {2} at \
684 {3}".format(game.game_id,
685 server_id, map_id, start_dt))
690 def get_or_create_player(session=None, hashkey=None, nick=None):
692 Finds a player by hashkey or creates a new one (along with a
693 corresponding hashkey entry. Parameters:
695 session - SQLAlchemy database session factory
696 hashkey - hashkey of the player to be found or created
697 nick - nick of the player (in case of a first time create)
700 if re.search('^bot#\d+', hashkey):
701 player = session.query(Player).filter_by(player_id=1).one()
702 # if we have an untracked player
703 elif re.search('^player#\d+$', hashkey):
704 player = session.query(Player).filter_by(player_id=2).one()
705 # else it is a tracked player
707 # see if the player is already in the database
708 # if not, create one and the hashkey along with it
710 hk = session.query(Hashkey).filter_by(
711 hashkey=hashkey).one()
712 player = session.query(Player).filter_by(
713 player_id=hk.player_id).one()
714 log.debug("Found existing player {0} with hashkey {1}".format(
715 player.player_id, hashkey))
721 # if nick is given to us, use it. If not, use "Anonymous Player"
722 # with a suffix added for uniqueness.
724 player.nick = nick[:128]
725 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
727 player.nick = "Anonymous Player #{0}".format(player.player_id)
728 player.stripped_nick = player.nick
730 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
732 log.debug("Created player {0} ({2}) with hashkey {1}".format(
733 player.player_id, hashkey, player.nick.encode('utf-8')))
738 def create_default_game_stat(session, game_type_cd):
739 """Creates a blanked-out pgstat record for the given game type"""
741 # this is what we have to do to get partitioned records in - grab the
742 # sequence value first, then insert using the explicit ID (vs autogenerate)
743 seq = Sequence('player_game_stats_player_game_stat_id_seq')
744 pgstat_id = session.execute(seq)
745 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
746 create_dt=datetime.datetime.utcnow())
748 if game_type_cd == 'as':
749 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
751 if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
752 pgstat.kills = pgstat.deaths = pgstat.suicides = 0
754 if game_type_cd == 'cq':
755 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
758 if game_type_cd == 'ctf':
759 pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
760 pgstat.returns = pgstat.carrier_frags = 0
762 if game_type_cd == 'cts':
765 if game_type_cd == 'dom':
766 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
769 if game_type_cd == 'ft':
770 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
772 if game_type_cd == 'ka':
773 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
774 pgstat.carrier_frags = 0
775 pgstat.time = datetime.timedelta(seconds=0)
777 if game_type_cd == 'kh':
778 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
779 pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
780 pgstat.carrier_frags = 0
782 if game_type_cd == 'lms':
783 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
785 if game_type_cd == 'nb':
786 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
789 if game_type_cd == 'rc':
790 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
795 def create_game_stat(session, game_meta, game, server, gmap, player, events):
796 """Game stats handler for all game types"""
798 game_type_cd = game.game_type_cd
800 pgstat = create_default_game_stat(session, game_type_cd)
802 # these fields should be on every pgstat record
803 pgstat.game_id = game.game_id
804 pgstat.player_id = player.player_id
805 pgstat.nick = events.get('n', 'Anonymous Player')[:128]
806 pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
807 pgstat.score = int(round(float(events.get('scoreboard-score', 0))))
808 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
809 pgstat.rank = int(events.get('rank', None))
810 pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
812 if pgstat.nick != player.nick \
813 and player.player_id > 2 \
814 and pgstat.nick != 'Anonymous Player':
815 register_new_nick(session, player, pgstat.nick)
819 # gametype-specific stuff is handled here. if passed to us, we store it
820 for (key,value) in events.items():
821 if key == 'wins': wins = True
822 if key == 't': pgstat.team = int(value)
824 if key == 'scoreboard-drops': pgstat.drops = int(value)
825 if key == 'scoreboard-returns': pgstat.returns = int(value)
826 if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
827 if key == 'scoreboard-pickups': pgstat.pickups = int(value)
828 if key == 'scoreboard-caps': pgstat.captures = int(value)
829 if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
830 if key == 'scoreboard-deaths': pgstat.deaths = int(value)
831 if key == 'scoreboard-kills': pgstat.kills = int(value)
832 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
833 if key == 'scoreboard-objectives': pgstat.collects = int(value)
834 if key == 'scoreboard-captured': pgstat.captures = int(value)
835 if key == 'scoreboard-released': pgstat.drops = int(value)
836 if key == 'scoreboard-fastest':
837 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
838 if key == 'scoreboard-takes': pgstat.pickups = int(value)
839 if key == 'scoreboard-ticks': pgstat.drops = int(value)
840 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
841 if key == 'scoreboard-bctime':
842 pgstat.time = datetime.timedelta(seconds=int(value))
843 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
844 if key == 'scoreboard-losses': pgstat.drops = int(value)
845 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
846 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
847 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
848 if key == 'scoreboard-lives': pgstat.lives = int(value)
849 if key == 'scoreboard-goals': pgstat.captures = int(value)
850 if key == 'scoreboard-faults': pgstat.drops = int(value)
851 if key == 'scoreboard-laps': pgstat.laps = int(value)
853 if key == 'avglatency': pgstat.avg_latency = float(value)
854 if key == 'scoreboard-captime':
855 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
856 if game.game_type_cd == 'ctf':
857 update_fastest_cap(session, player.player_id, game.game_id,
858 gmap.map_id, pgstat.fastest, game.mod)
860 # there is no "winning team" field, so we have to derive it
861 if wins and pgstat.team is not None and game.winner is None:
862 game.winner = pgstat.team
870 def create_anticheats(session, pgstat, game, player, events):
871 """Anticheats handler for all game types"""
875 # all anticheat events are prefixed by "anticheat"
876 for (key,value) in events.items():
877 if key.startswith("anticheat"):
879 ac = PlayerGameAnticheat(
885 anticheats.append(ac)
887 except Exception as e:
888 log.debug("Could not parse value for key %s. Ignoring." % key)
893 def create_default_team_stat(session, game_type_cd):
894 """Creates a blanked-out teamstat record for the given game type"""
896 # this is what we have to do to get partitioned records in - grab the
897 # sequence value first, then insert using the explicit ID (vs autogenerate)
898 seq = Sequence('team_game_stats_team_game_stat_id_seq')
899 teamstat_id = session.execute(seq)
900 teamstat = TeamGameStat(team_game_stat_id=teamstat_id,
901 create_dt=datetime.datetime.utcnow())
903 # all team game modes have a score, so we'll zero that out always
906 if game_type_cd in 'ca' 'ft' 'lms' 'ka':
909 if game_type_cd == 'ctf':
915 def create_team_stat(session, game, events):
916 """Team stats handler for all game types"""
919 teamstat = create_default_team_stat(session, game.game_type_cd)
920 teamstat.game_id = game.game_id
922 # we should have a team ID if we have a 'Q' event
923 if re.match(r'^team#\d+$', events.get('Q', '')):
924 team = int(events.get('Q').replace('team#', ''))
927 # gametype-specific stuff is handled here. if passed to us, we store it
928 for (key,value) in events.items():
929 if key == 'scoreboard-score': teamstat.score = int(round(float(value)))
930 if key == 'scoreboard-caps': teamstat.caps = int(value)
931 if key == 'scoreboard-goals': teamstat.caps = int(value)
932 if key == 'scoreboard-rounds': teamstat.rounds = int(value)
934 session.add(teamstat)
935 except Exception as e:
941 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
942 """Weapon stats handler for all game types"""
945 # Version 1 of stats submissions doubled the data sent.
946 # To counteract this we divide the data by 2 only for
947 # POSTs coming from version 1.
949 version = int(game_meta['V'])
952 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
958 for (key,value) in events.items():
959 matched = re.search("acc-(.*?)-cnt-fired", key)
961 weapon_cd = matched.group(1)
963 # Weapon names changed for 0.8. We'll convert the old
964 # ones to use the new scheme as well.
965 mapped_weapon_cd = weapon_map.get(weapon_cd, weapon_cd)
967 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
968 pwstat_id = session.execute(seq)
969 pwstat = PlayerWeaponStat()
970 pwstat.player_weapon_stats_id = pwstat_id
971 pwstat.player_id = player.player_id
972 pwstat.game_id = game.game_id
973 pwstat.player_game_stat_id = pgstat.player_game_stat_id
974 pwstat.weapon_cd = mapped_weapon_cd
977 pwstat.nick = events['n']
979 pwstat.nick = events['P']
981 if 'acc-' + weapon_cd + '-cnt-fired' in events:
982 pwstat.fired = int(round(float(
983 events['acc-' + weapon_cd + '-cnt-fired'])))
984 if 'acc-' + weapon_cd + '-fired' in events:
985 pwstat.max = int(round(float(
986 events['acc-' + weapon_cd + '-fired'])))
987 if 'acc-' + weapon_cd + '-cnt-hit' in events:
988 pwstat.hit = int(round(float(
989 events['acc-' + weapon_cd + '-cnt-hit'])))
990 if 'acc-' + weapon_cd + '-hit' in events:
991 pwstat.actual = int(round(float(
992 events['acc-' + weapon_cd + '-hit'])))
993 if 'acc-' + weapon_cd + '-frags' in events:
994 pwstat.frags = int(round(float(
995 events['acc-' + weapon_cd + '-frags'])))
998 pwstat.fired = pwstat.fired/2
999 pwstat.max = pwstat.max/2
1000 pwstat.hit = pwstat.hit/2
1001 pwstat.actual = pwstat.actual/2
1002 pwstat.frags = pwstat.frags/2
1005 pwstats.append(pwstat)
1010 def get_ranks(session, player_ids, game_type_cd):
1012 Gets the rank entries for all players in the given list, returning a dict
1013 of player_id -> PlayerRank instance. The rank entry corresponds to the
1014 game type of the parameter passed in as well.
1017 for pr in session.query(PlayerRank).\
1018 filter(PlayerRank.player_id.in_(player_ids)).\
1019 filter(PlayerRank.game_type_cd == game_type_cd).\
1021 ranks[pr.player_id] = pr
1026 def submit_stats(request):
1028 Entry handler for POST stats submissions.
1031 # placeholder for the actual session
1034 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
1035 "----- END REQUEST BODY -----\n\n")
1037 (idfp, status) = verify_request(request)
1038 (game_meta, raw_players, raw_teams) = parse_stats_submission(request.body)
1039 revision = game_meta.get('R', 'unknown')
1040 duration = game_meta.get('D', None)
1042 # only players present at the end of the match are eligible for stats
1043 raw_players = filter(played_in_game, raw_players)
1045 do_precondition_checks(request, game_meta, raw_players)
1047 # the "duel" gametype is fake
1048 if len(raw_players) == 2 \
1049 and num_real_players(raw_players) == 2 \
1050 and game_meta['G'] == 'dm':
1051 game_meta['G'] = 'duel'
1053 #----------------------------------------------------------------------
1054 # Actual setup (inserts/updates) below here
1055 #----------------------------------------------------------------------
1056 session = DBSession()
1058 game_type_cd = game_meta['G']
1060 # All game types create Game, Server, Map, and Player records
1062 server = get_or_create_server(
1065 name = game_meta['S'],
1066 revision = revision,
1067 ip_addr = get_remote_addr(request),
1068 port = game_meta.get('U', None),
1069 impure_cvars = game_meta.get('C', 0))
1071 gmap = get_or_create_map(
1073 name = game_meta['M'])
1077 start_dt = datetime.datetime.utcnow(),
1078 server_id = server.server_id,
1079 game_type_cd = game_type_cd,
1080 map_id = gmap.map_id,
1081 match_id = game_meta['I'],
1082 duration = duration,
1083 mod = game_meta.get('O', None))
1085 # keep track of the players we've seen
1089 for events in raw_players:
1090 player = get_or_create_player(
1092 hashkey = events['P'],
1093 nick = events.get('n', None))
1095 pgstat = create_game_stat(session, game_meta, game, server,
1096 gmap, player, events)
1097 pgstats.append(pgstat)
1099 if player.player_id > 1:
1100 anticheats = create_anticheats(session, pgstat, game, player, events)
1102 if player.player_id > 2:
1103 player_ids.append(player.player_id)
1104 hashkeys[player.player_id] = events['P']
1106 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
1107 pwstats = create_weapon_stats(session, game_meta, game, player,
1110 # store them on games for easy access
1111 game.players = player_ids
1113 for events in raw_teams:
1115 teamstat = create_team_stat(session, game, events)
1116 except Exception as e:
1119 if server.elo_ind and gametype_elo_eligible(game_type_cd):
1120 ep = EloProcessor(session, game, pgstats)
1124 log.debug('Success! Stats recorded.')
1126 # ranks are fetched after we've done the "real" processing
1127 ranks = get_ranks(session, player_ids, game_type_cd)
1129 # plain text response
1130 request.response.content_type = 'text/plain'
1133 "now" : calendar.timegm(datetime.datetime.utcnow().timetuple()),
1137 "player_ids" : player_ids,
1138 "hashkeys" : hashkeys,
1143 except Exception as e: