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
29 # the submission code version (from the server)
32 # the revision string of the server
35 # the game type played
36 self.game_type_cd = None
41 # the name of the map played
44 # unique identifier (string) for a match on a given server
47 # the name of the server
48 self.server_name = None
50 # the number of cvars that were changed to be different than default
51 self.impure_cvar_changes = None
53 # the port number the game server is listening on
54 self.port_number = None
56 # how long the game lasted
59 # which ladder is being used, if any
62 # players involved in the match (humans, bots, and spectators)
68 # the parsing deque (we use this to allow peeking)
69 self.q = collections.deque(self.body.split("\n"))
71 ############################################################################################
72 # Below this point are fields useful in determining if the submission is valid or
73 # performance optimizations that save us from looping over the events over and over again.
74 ############################################################################################
76 # humans who played in the match
79 # bots who played in the match
82 # distinct weapons that we have seen fired
85 # has a human player fired a shot?
86 self.human_fired_weapon = False
88 # does any human have a non-zero score?
89 self.human_nonzero_score = False
91 # does any human have a fastest cap?
92 self.human_fastest = False
95 """Returns the next key:value pair off the queue."""
97 items = self.q.popleft().strip().split(' ', 1)
99 # Some keys won't have values, like 'L' records where the server isn't actually
100 # participating in any ladders. These can be safely ignored.
107 def add_weapon_fired(self, sub_key):
108 """Adds a weapon to the set of weapons fired during the match (a set)."""
109 self.weapons.add(sub_key.split("-")[1])
112 def is_human_player(player):
114 Determines if a given set of events correspond with a non-bot
116 return not player['P'].startswith('bot')
119 def played_in_game(player):
121 Determines if a given set of player events correspond with a player who
122 played in the game (matches 1 and scoreboardvalid 1)
124 return 'matches' in player and 'scoreboardvalid' in player
126 def parse_player(self, key, pid):
127 """Construct a player events listing from the submission."""
129 # all of the keys related to player records
130 player_keys = ['i', 'n', 't', 'e']
134 player_fired_weapon = False
135 player_nonzero_score = False
136 player_fastest = False
138 # Consume all following 'i' 'n' 't' 'e' records
139 while len(self.q) > 0:
140 (key, value) = self.next_item()
141 if key is None and value is None:
144 (sub_key, sub_value) = value.split(' ', 1)
145 player[sub_key] = sub_value
147 if sub_key.endswith("cnt-fired"):
148 player_fired_weapon = True
149 self.add_weapon_fired(sub_key)
150 elif sub_key == 'scoreboard-score' and int(sub_value) != 0:
151 player_nonzero_score = True
152 elif sub_key == 'scoreboard-fastest':
153 player_fastest = True
155 player[key] = unicode(value, 'utf-8')
156 elif key in player_keys:
159 # something we didn't expect - put it back on the deque
160 self.q.appendleft("{} {}".format(key, value))
163 played = self.played_in_game(player)
164 human = self.is_human_player(player)
167 self.humans.append(player)
169 if player_fired_weapon:
170 self.human_fired_weapon = True
172 if player_nonzero_score:
173 self.human_nonzero_score = True
176 self.human_fastest = True
178 elif played and not human:
179 self.bots.append(player)
181 self.players.append(player)
183 def parse_team(self, key, tid):
184 """Construct a team events listing from the submission."""
187 # Consume all following 'e' records
188 while len(self.q) > 0 and self.q[0].startswith('e'):
189 (_, value) = self.next_item()
190 (sub_key, sub_value) = value.split(' ', 1)
191 team[sub_key] = sub_value
193 self.teams.append(team)
196 """Parses the request body into instance variables."""
197 while len(self.q) > 0:
198 (key, value) = self.next_item()
199 if key is None and value is None:
204 self.revision = value
206 self.game_type_cd = value
210 self.map_name = value
212 self.match_id = value
214 self.server_name = unicode(value, 'utf-8')
216 self.impure_cvar_changes = int(value)
218 self.port_number = int(value)
220 self.duration = datetime.timedelta(seconds=int(round(float(value))))
224 self.parse_team(key, value)
226 self.parse_player(key, value)
228 raise Exception("Invalid submission")
233 """Debugging representation of a submission."""
234 return "game_type_cd: {}, mod: {}, players: {}, humans: {}, bots: {}, weapons: {}".format(
235 self.game_type_cd, self.mod, len(self.players), len(self.humans), len(self.bots),
239 def elo_submission_category(submission):
240 """Determines the Elo category purely by what is in the submission data."""
243 vanilla_allowed_weapons = {"shotgun", "devastator", "blaster", "mortar", "vortex", "electro",
244 "arc", "hagar", "crylink", "machinegun"}
245 insta_allowed_weapons = {"vaporizer", "blaster"}
246 overkill_allowed_weapons = {"hmg", "vortex", "shotgun", "blaster", "machinegun", "rpc"}
249 if len(submission.weapons - vanilla_allowed_weapons) == 0:
251 elif mod == "InstaGib":
252 if len(submission.weapons - insta_allowed_weapons) == 0:
254 elif mod == "Overkill":
255 if len(submission.weapons - overkill_allowed_weapons) == 0:
263 def is_blank_game(submission):
265 Determine if this is a blank game or not. A blank game is either:
267 1) a match that ended in the warmup stage, where accuracy events are not
268 present (for non-CTS games)
270 2) a match in which no player made a positive or negative score AND was
273 ... or for CTS, which doesn't record accuracy events
275 1) a match in which no player made a fastest lap AND was
278 ... or for NB, in which not all maps have weapons
280 1) a match in which no player made a positive or negative score
282 if submission.game_type_cd == 'cts':
283 return not submission.human_fastest
284 elif submission.game_type_cd == 'nb':
285 return not submission.human_nonzero_score
287 return not (submission.human_nonzero_score and submission.human_fired_weapon)
290 def get_remote_addr(request):
291 """Get the Xonotic server's IP address"""
292 if 'X-Forwarded-For' in request.headers:
293 return request.headers['X-Forwarded-For']
295 return request.remote_addr
298 def is_supported_gametype(gametype, version):
299 """Whether a gametype is supported or not"""
302 # if the type can be supported, but with version constraints, uncomment
303 # here and add the restriction for a specific version below
304 supported_game_types = (
323 if gametype in supported_game_types:
328 # some game types were buggy before revisions, thus this additional filter
329 if gametype == 'ca' and version <= 5:
335 def do_precondition_checks(request, game_meta, raw_players):
336 """Precondition checks for ALL gametypes.
337 These do not require a database connection."""
338 if not has_required_metadata(game_meta):
339 msg = "Missing required game metadata"
341 raise pyramid.httpexceptions.HTTPUnprocessableEntity(
343 content_type="text/plain"
347 version = int(game_meta['V'])
349 msg = "Invalid or incorrect game metadata provided"
351 raise pyramid.httpexceptions.HTTPUnprocessableEntity(
353 content_type="text/plain"
356 if not is_supported_gametype(game_meta['G'], version):
357 msg = "Unsupported game type ({})".format(game_meta['G'])
359 raise pyramid.httpexceptions.HTTPOk(
361 content_type="text/plain"
364 if not has_minimum_real_players(request.registry.settings, raw_players):
365 msg = "Not enough real players"
367 raise pyramid.httpexceptions.HTTPOk(
369 content_type="text/plain"
372 if is_blank_game(game_meta['G'], raw_players):
375 raise pyramid.httpexceptions.HTTPOk(
377 content_type="text/plain"
381 def num_real_players(player_events):
383 Returns the number of real players (those who played
384 and are on the scoreboard).
388 for events in player_events:
389 if is_real_player(events) and played_in_game(events):
395 def has_minimum_real_players(settings, player_events):
397 Determines if the collection of player events has enough "real" players
398 to store in the database. The minimum setting comes from the config file
399 under the setting xonstat.minimum_real_players.
401 flg_has_min_real_players = True
404 minimum_required_players = int(
405 settings['xonstat.minimum_required_players'])
407 minimum_required_players = 2
409 real_players = num_real_players(player_events)
411 if real_players < minimum_required_players:
412 flg_has_min_real_players = False
414 return flg_has_min_real_players
417 def has_required_metadata(metadata):
419 Determines if a give set of metadata has enough data to create a game,
420 server, and map with.
422 flg_has_req_metadata = True
424 if 'G' not in metadata or\
425 'M' not in metadata or\
426 'I' not in metadata or\
428 flg_has_req_metadata = False
430 return flg_has_req_metadata
433 def should_do_weapon_stats(game_type_cd):
434 """True of the game type should record weapon stats. False otherwise."""
435 if game_type_cd in 'cts':
441 def gametype_elo_eligible(game_type_cd):
442 """True of the game type should process Elos. False otherwise."""
443 elo_game_types = ('duel', 'dm', 'ca', 'ctf', 'tdm', 'ka', 'ft')
445 if game_type_cd in elo_game_types:
451 def register_new_nick(session, player, new_nick):
453 Change the player record's nick to the newly found nick. Store the old
454 nick in the player_nicks table for that player.
456 session - SQLAlchemy database session factory
457 player - player record whose nick is changing
458 new_nick - the new nickname
460 # see if that nick already exists
461 stripped_nick = strip_colors(qfont_decode(player.nick))
463 player_nick = session.query(PlayerNick).filter_by(
464 player_id=player.player_id, stripped_nick=stripped_nick).one()
465 except NoResultFound, e:
466 # player_id/stripped_nick not found, create one
467 # but we don't store "Anonymous Player #N"
468 if not re.search('^Anonymous Player #\d+$', player.nick):
469 player_nick = PlayerNick()
470 player_nick.player_id = player.player_id
471 player_nick.stripped_nick = stripped_nick
472 player_nick.nick = player.nick
473 session.add(player_nick)
475 # We change to the new nick regardless
476 player.nick = new_nick
477 player.stripped_nick = strip_colors(qfont_decode(new_nick))
481 def update_fastest_cap(session, player_id, game_id, map_id, captime, mod):
483 Check the fastest cap time for the player and map. If there isn't
484 one, insert one. If there is, check if the passed time is faster.
487 # we don't record fastest cap times for bots or anonymous players
491 # see if a cap entry exists already
492 # then check to see if the new captime is faster
494 cur_fastest_cap = session.query(PlayerCaptime).filter_by(
495 player_id=player_id, map_id=map_id, mod=mod).one()
497 # current captime is faster, so update
498 if captime < cur_fastest_cap.fastest_cap:
499 cur_fastest_cap.fastest_cap = captime
500 cur_fastest_cap.game_id = game_id
501 cur_fastest_cap.create_dt = datetime.datetime.utcnow()
502 session.add(cur_fastest_cap)
504 except NoResultFound, e:
505 # none exists, so insert
506 cur_fastest_cap = PlayerCaptime(player_id, game_id, map_id, captime,
508 session.add(cur_fastest_cap)
512 def update_server(server, name, hashkey, ip_addr, port, revision, impure_cvars):
514 Updates the server in the given DB session, if needed.
516 :param server: The found server instance.
517 :param name: The incoming server name.
518 :param hashkey: The incoming server hashkey.
519 :param ip_addr: The incoming server IP address.
520 :param port: The incoming server port.
521 :param revision: The incoming server revision.
522 :param impure_cvars: The incoming number of impure server cvars.
525 # ensure the two int attributes are actually ints
532 impure_cvars = int(impure_cvars)
537 if name and server.name != name:
540 if hashkey and server.hashkey != hashkey:
541 server.hashkey = hashkey
543 if ip_addr and server.ip_addr != ip_addr:
544 server.ip_addr = ip_addr
546 if port and server.port != port:
549 if revision and server.revision != revision:
550 server.revision = revision
552 if impure_cvars and server.impure_cvars != impure_cvars:
553 server.impure_cvars = impure_cvars
554 server.pure_ind = True if impure_cvars == 0 else False
560 def get_or_create_server(session, name, hashkey, ip_addr, revision, port, impure_cvars):
562 Find a server by name or create one if not found. Parameters:
564 session - SQLAlchemy database session factory
565 name - server name of the server to be found or created
566 hashkey - server hashkey
567 ip_addr - the IP address of the server
568 revision - the xonotic revision number
569 port - the port number of the server
570 impure_cvars - the number of impure cvar changes
572 servers_q = DBSession.query(Server).filter(Server.active_ind)
575 # if the hashkey is provided, we'll use that
576 servers_q = servers_q.filter((Server.name == name) or (Server.hashkey == hashkey))
578 # otherwise, it is just by name
579 servers_q = servers_q.filter(Server.name == name)
581 # order by the hashkey, which means any hashkey match will appear first if there are multiple
582 servers = servers_q.order_by(Server.hashkey, Server.create_dt).all()
584 if len(servers) == 0:
585 server = Server(name=name, hashkey=hashkey)
588 log.debug("Created server {} with hashkey {}.".format(server.server_id, server.hashkey))
591 if len(servers) == 1:
592 log.info("Found existing server {}.".format(server.server_id))
594 elif len(servers) > 1:
595 server_id_list = ", ".join(["{}".format(s.server_id) for s in servers])
596 log.warn("Multiple servers found ({})! Using the first one ({})."
597 .format(server_id_list, server.server_id))
599 if update_server(server, name, hashkey, ip_addr, port, revision, impure_cvars):
605 def get_or_create_map(session=None, name=None):
607 Find a map by name or create one if not found. Parameters:
609 session - SQLAlchemy database session factory
610 name - map name of the map to be found or created
613 # find one by the name, if it exists
614 gmap = session.query(Map).filter_by(name=name).one()
615 log.debug("Found map id {0}: {1}".format(gmap.map_id,
617 except NoResultFound, e:
618 gmap = Map(name=name)
621 log.debug("Created map id {0}: {1}".format(gmap.map_id,
623 except MultipleResultsFound, e:
624 # multiple found, so use the first one but warn
626 gmaps = session.query(Map).filter_by(name=name).order_by(
629 log.debug("Found map id {0}: {1} but found \
630 multiple".format(gmap.map_id, gmap.name))
635 def create_game(session, start_dt, game_type_cd, server_id, map_id,
636 match_id, duration, mod, winner=None):
638 Creates a game. Parameters:
640 session - SQLAlchemy database session factory
641 start_dt - when the game started (datetime object)
642 game_type_cd - the game type of the game being played
643 server_id - server identifier of the server hosting the game
644 map_id - map on which the game was played
645 winner - the team id of the team that won
646 duration - how long the game lasted
647 mod - mods in use during the game
649 seq = Sequence('games_game_id_seq')
650 game_id = session.execute(seq)
651 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
652 server_id=server_id, map_id=map_id, winner=winner)
653 game.match_id = match_id
656 # There is some drift between start_dt (provided by app) and create_dt
657 # (default in the database), so we'll make them the same until this is
659 game.create_dt = start_dt
662 game.duration = datetime.timedelta(seconds=int(round(float(duration))))
667 session.query(Game).filter(Game.server_id==server_id).\
668 filter(Game.match_id==match_id).one()
670 log.debug("Error: game with same server and match_id found! Ignoring.")
672 # if a game under the same server and match_id found,
673 # this is a duplicate game and can be ignored
674 raise pyramid.httpexceptions.HTTPOk('OK')
675 except NoResultFound, e:
676 # server_id/match_id combination not found. game is ok to insert
679 log.debug("Created game id {0} on server {1}, map {2} at \
680 {3}".format(game.game_id,
681 server_id, map_id, start_dt))
686 def get_or_create_player(session=None, hashkey=None, nick=None):
688 Finds a player by hashkey or creates a new one (along with a
689 corresponding hashkey entry. Parameters:
691 session - SQLAlchemy database session factory
692 hashkey - hashkey of the player to be found or created
693 nick - nick of the player (in case of a first time create)
696 if re.search('^bot#\d+', hashkey):
697 player = session.query(Player).filter_by(player_id=1).one()
698 # if we have an untracked player
699 elif re.search('^player#\d+$', hashkey):
700 player = session.query(Player).filter_by(player_id=2).one()
701 # else it is a tracked player
703 # see if the player is already in the database
704 # if not, create one and the hashkey along with it
706 hk = session.query(Hashkey).filter_by(
707 hashkey=hashkey).one()
708 player = session.query(Player).filter_by(
709 player_id=hk.player_id).one()
710 log.debug("Found existing player {0} with hashkey {1}".format(
711 player.player_id, hashkey))
717 # if nick is given to us, use it. If not, use "Anonymous Player"
718 # with a suffix added for uniqueness.
720 player.nick = nick[:128]
721 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
723 player.nick = "Anonymous Player #{0}".format(player.player_id)
724 player.stripped_nick = player.nick
726 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
728 log.debug("Created player {0} ({2}) with hashkey {1}".format(
729 player.player_id, hashkey, player.nick.encode('utf-8')))
734 def create_default_game_stat(session, game_type_cd):
735 """Creates a blanked-out pgstat record for the given game type"""
737 # this is what we have to do to get partitioned records in - grab the
738 # sequence value first, then insert using the explicit ID (vs autogenerate)
739 seq = Sequence('player_game_stats_player_game_stat_id_seq')
740 pgstat_id = session.execute(seq)
741 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
742 create_dt=datetime.datetime.utcnow())
744 if game_type_cd == 'as':
745 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
747 if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
748 pgstat.kills = pgstat.deaths = pgstat.suicides = 0
750 if game_type_cd == 'cq':
751 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
754 if game_type_cd == 'ctf':
755 pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
756 pgstat.returns = pgstat.carrier_frags = 0
758 if game_type_cd == 'cts':
761 if game_type_cd == 'dom':
762 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
765 if game_type_cd == 'ft':
766 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
768 if game_type_cd == 'ka':
769 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
770 pgstat.carrier_frags = 0
771 pgstat.time = datetime.timedelta(seconds=0)
773 if game_type_cd == 'kh':
774 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
775 pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
776 pgstat.carrier_frags = 0
778 if game_type_cd == 'lms':
779 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
781 if game_type_cd == 'nb':
782 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
785 if game_type_cd == 'rc':
786 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
791 def create_game_stat(session, game_meta, game, server, gmap, player, events):
792 """Game stats handler for all game types"""
794 game_type_cd = game.game_type_cd
796 pgstat = create_default_game_stat(session, game_type_cd)
798 # these fields should be on every pgstat record
799 pgstat.game_id = game.game_id
800 pgstat.player_id = player.player_id
801 pgstat.nick = events.get('n', 'Anonymous Player')[:128]
802 pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
803 pgstat.score = int(round(float(events.get('scoreboard-score', 0))))
804 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
805 pgstat.rank = int(events.get('rank', None))
806 pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
808 if pgstat.nick != player.nick \
809 and player.player_id > 2 \
810 and pgstat.nick != 'Anonymous Player':
811 register_new_nick(session, player, pgstat.nick)
815 # gametype-specific stuff is handled here. if passed to us, we store it
816 for (key,value) in events.items():
817 if key == 'wins': wins = True
818 if key == 't': pgstat.team = int(value)
820 if key == 'scoreboard-drops': pgstat.drops = int(value)
821 if key == 'scoreboard-returns': pgstat.returns = int(value)
822 if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
823 if key == 'scoreboard-pickups': pgstat.pickups = int(value)
824 if key == 'scoreboard-caps': pgstat.captures = int(value)
825 if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
826 if key == 'scoreboard-deaths': pgstat.deaths = int(value)
827 if key == 'scoreboard-kills': pgstat.kills = int(value)
828 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
829 if key == 'scoreboard-objectives': pgstat.collects = int(value)
830 if key == 'scoreboard-captured': pgstat.captures = int(value)
831 if key == 'scoreboard-released': pgstat.drops = int(value)
832 if key == 'scoreboard-fastest':
833 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
834 if key == 'scoreboard-takes': pgstat.pickups = int(value)
835 if key == 'scoreboard-ticks': pgstat.drops = int(value)
836 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
837 if key == 'scoreboard-bctime':
838 pgstat.time = datetime.timedelta(seconds=int(value))
839 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
840 if key == 'scoreboard-losses': pgstat.drops = int(value)
841 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
842 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
843 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
844 if key == 'scoreboard-lives': pgstat.lives = int(value)
845 if key == 'scoreboard-goals': pgstat.captures = int(value)
846 if key == 'scoreboard-faults': pgstat.drops = int(value)
847 if key == 'scoreboard-laps': pgstat.laps = int(value)
849 if key == 'avglatency': pgstat.avg_latency = float(value)
850 if key == 'scoreboard-captime':
851 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
852 if game.game_type_cd == 'ctf':
853 update_fastest_cap(session, player.player_id, game.game_id,
854 gmap.map_id, pgstat.fastest, game.mod)
856 # there is no "winning team" field, so we have to derive it
857 if wins and pgstat.team is not None and game.winner is None:
858 game.winner = pgstat.team
866 def create_anticheats(session, pgstat, game, player, events):
867 """Anticheats handler for all game types"""
871 # all anticheat events are prefixed by "anticheat"
872 for (key,value) in events.items():
873 if key.startswith("anticheat"):
875 ac = PlayerGameAnticheat(
881 anticheats.append(ac)
883 except Exception as e:
884 log.debug("Could not parse value for key %s. Ignoring." % key)
889 def create_default_team_stat(session, game_type_cd):
890 """Creates a blanked-out teamstat record for the given game type"""
892 # this is what we have to do to get partitioned records in - grab the
893 # sequence value first, then insert using the explicit ID (vs autogenerate)
894 seq = Sequence('team_game_stats_team_game_stat_id_seq')
895 teamstat_id = session.execute(seq)
896 teamstat = TeamGameStat(team_game_stat_id=teamstat_id,
897 create_dt=datetime.datetime.utcnow())
899 # all team game modes have a score, so we'll zero that out always
902 if game_type_cd in 'ca' 'ft' 'lms' 'ka':
905 if game_type_cd == 'ctf':
911 def create_team_stat(session, game, events):
912 """Team stats handler for all game types"""
915 teamstat = create_default_team_stat(session, game.game_type_cd)
916 teamstat.game_id = game.game_id
918 # we should have a team ID if we have a 'Q' event
919 if re.match(r'^team#\d+$', events.get('Q', '')):
920 team = int(events.get('Q').replace('team#', ''))
923 # gametype-specific stuff is handled here. if passed to us, we store it
924 for (key,value) in events.items():
925 if key == 'scoreboard-score': teamstat.score = int(round(float(value)))
926 if key == 'scoreboard-caps': teamstat.caps = int(value)
927 if key == 'scoreboard-goals': teamstat.caps = int(value)
928 if key == 'scoreboard-rounds': teamstat.rounds = int(value)
930 session.add(teamstat)
931 except Exception as e:
937 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
938 """Weapon stats handler for all game types"""
941 # Version 1 of stats submissions doubled the data sent.
942 # To counteract this we divide the data by 2 only for
943 # POSTs coming from version 1.
945 version = int(game_meta['V'])
948 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
954 for (key,value) in events.items():
955 matched = re.search("acc-(.*?)-cnt-fired", key)
957 weapon_cd = matched.group(1)
959 # Weapon names changed for 0.8. We'll convert the old
960 # ones to use the new scheme as well.
961 mapped_weapon_cd = weapon_map.get(weapon_cd, weapon_cd)
963 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
964 pwstat_id = session.execute(seq)
965 pwstat = PlayerWeaponStat()
966 pwstat.player_weapon_stats_id = pwstat_id
967 pwstat.player_id = player.player_id
968 pwstat.game_id = game.game_id
969 pwstat.player_game_stat_id = pgstat.player_game_stat_id
970 pwstat.weapon_cd = mapped_weapon_cd
973 pwstat.nick = events['n']
975 pwstat.nick = events['P']
977 if 'acc-' + weapon_cd + '-cnt-fired' in events:
978 pwstat.fired = int(round(float(
979 events['acc-' + weapon_cd + '-cnt-fired'])))
980 if 'acc-' + weapon_cd + '-fired' in events:
981 pwstat.max = int(round(float(
982 events['acc-' + weapon_cd + '-fired'])))
983 if 'acc-' + weapon_cd + '-cnt-hit' in events:
984 pwstat.hit = int(round(float(
985 events['acc-' + weapon_cd + '-cnt-hit'])))
986 if 'acc-' + weapon_cd + '-hit' in events:
987 pwstat.actual = int(round(float(
988 events['acc-' + weapon_cd + '-hit'])))
989 if 'acc-' + weapon_cd + '-frags' in events:
990 pwstat.frags = int(round(float(
991 events['acc-' + weapon_cd + '-frags'])))
994 pwstat.fired = pwstat.fired/2
995 pwstat.max = pwstat.max/2
996 pwstat.hit = pwstat.hit/2
997 pwstat.actual = pwstat.actual/2
998 pwstat.frags = pwstat.frags/2
1001 pwstats.append(pwstat)
1006 def get_ranks(session, player_ids, game_type_cd):
1008 Gets the rank entries for all players in the given list, returning a dict
1009 of player_id -> PlayerRank instance. The rank entry corresponds to the
1010 game type of the parameter passed in as well.
1013 for pr in session.query(PlayerRank).\
1014 filter(PlayerRank.player_id.in_(player_ids)).\
1015 filter(PlayerRank.game_type_cd == game_type_cd).\
1017 ranks[pr.player_id] = pr
1022 def submit_stats(request):
1024 Entry handler for POST stats submissions.
1027 # placeholder for the actual session
1030 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
1031 "----- END REQUEST BODY -----\n\n")
1033 (idfp, status) = verify_request(request)
1034 (game_meta, raw_players, raw_teams) = parse_stats_submission(request.body)
1035 revision = game_meta.get('R', 'unknown')
1036 duration = game_meta.get('D', None)
1038 # only players present at the end of the match are eligible for stats
1039 raw_players = filter(played_in_game, raw_players)
1041 do_precondition_checks(request, game_meta, raw_players)
1043 # the "duel" gametype is fake
1044 if len(raw_players) == 2 \
1045 and num_real_players(raw_players) == 2 \
1046 and game_meta['G'] == 'dm':
1047 game_meta['G'] = 'duel'
1049 #----------------------------------------------------------------------
1050 # Actual setup (inserts/updates) below here
1051 #----------------------------------------------------------------------
1052 session = DBSession()
1054 game_type_cd = game_meta['G']
1056 # All game types create Game, Server, Map, and Player records
1058 server = get_or_create_server(
1061 name = game_meta['S'],
1062 revision = revision,
1063 ip_addr = get_remote_addr(request),
1064 port = game_meta.get('U', None),
1065 impure_cvars = game_meta.get('C', 0))
1067 gmap = get_or_create_map(
1069 name = game_meta['M'])
1073 start_dt = datetime.datetime.utcnow(),
1074 server_id = server.server_id,
1075 game_type_cd = game_type_cd,
1076 map_id = gmap.map_id,
1077 match_id = game_meta['I'],
1078 duration = duration,
1079 mod = game_meta.get('O', None))
1081 # keep track of the players we've seen
1085 for events in raw_players:
1086 player = get_or_create_player(
1088 hashkey = events['P'],
1089 nick = events.get('n', None))
1091 pgstat = create_game_stat(session, game_meta, game, server,
1092 gmap, player, events)
1093 pgstats.append(pgstat)
1095 if player.player_id > 1:
1096 anticheats = create_anticheats(session, pgstat, game, player, events)
1098 if player.player_id > 2:
1099 player_ids.append(player.player_id)
1100 hashkeys[player.player_id] = events['P']
1102 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
1103 pwstats = create_weapon_stats(session, game_meta, game, player,
1106 # store them on games for easy access
1107 game.players = player_ids
1109 for events in raw_teams:
1111 teamstat = create_team_stat(session, game, events)
1112 except Exception as e:
1115 if server.elo_ind and gametype_elo_eligible(game_type_cd):
1116 ep = EloProcessor(session, game, pgstats)
1120 log.debug('Success! Stats recorded.')
1122 # ranks are fetched after we've done the "real" processing
1123 ranks = get_ranks(session, player_ids, game_type_cd)
1125 # plain text response
1126 request.response.content_type = 'text/plain'
1129 "now" : calendar.timegm(datetime.datetime.utcnow().timetuple()),
1133 "player_ids" : player_ids,
1134 "hashkeys" : hashkeys,
1139 except Exception as e: