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 has_required_metadata(submission):
291 """Determines if a submission has all the required metadata fields."""
292 return (submission.game_type_cd is not None
293 and submission.map_name is not None
294 and submission.match_id is not None
295 and submission.server_name is not None)
298 def is_supported_gametype(submission):
299 """Determines if a submission is of a valid and supported game type."""
301 # if the type can be supported, but with version constraints, uncomment
302 # here and add the restriction for a specific version below
303 supported_game_types = (
322 is_supported = submission.game_type_cd in supported_game_types
324 # some game types were buggy before revisions, thus this additional filter
325 if submission.game_type_cd == 'ca' and submission.version <= 5:
331 def has_minimum_real_players(settings, submission):
333 Determines if the submission has enough human players to store in the database. The minimum
334 setting comes from the config file under the setting xonstat.minimum_real_players.
337 minimum_required_players = int(settings.get("xonstat.minimum_required_players"))
339 minimum_required_players = 2
341 return len(submission.human_players) >= minimum_required_players
344 def do_precondition_checks(settings, submission):
345 """Precondition checks for ALL gametypes. These do not require a database connection."""
346 if not has_required_metadata(submission):
347 msg = "Missing required game metadata"
349 raise pyramid.httpexceptions.HTTPUnprocessableEntity(
351 content_type="text/plain"
354 if submission.version is None:
355 msg = "Invalid or incorrect game metadata provided"
357 raise pyramid.httpexceptions.HTTPUnprocessableEntity(
359 content_type="text/plain"
362 if not is_supported_gametype(submission):
363 msg = "Unsupported game type ({})".format(submission.game_type_cd)
365 raise pyramid.httpexceptions.HTTPOk(
367 content_type="text/plain"
370 if not has_minimum_real_players(settings, submission):
371 msg = "Not enough real players"
373 raise pyramid.httpexceptions.HTTPOk(
375 content_type="text/plain"
378 if is_blank_game(submission):
381 raise pyramid.httpexceptions.HTTPOk(
383 content_type="text/plain"
387 def get_remote_addr(request):
388 """Get the Xonotic server's IP address"""
389 if 'X-Forwarded-For' in request.headers:
390 return request.headers['X-Forwarded-For']
392 return request.remote_addr
395 def num_real_players(player_events):
397 Returns the number of real players (those who played
398 and are on the scoreboard).
402 for events in player_events:
403 if is_real_player(events) and played_in_game(events):
409 def should_do_weapon_stats(game_type_cd):
410 """True of the game type should record weapon stats. False otherwise."""
411 if game_type_cd in 'cts':
417 def gametype_elo_eligible(game_type_cd):
418 """True of the game type should process Elos. False otherwise."""
419 elo_game_types = ('duel', 'dm', 'ca', 'ctf', 'tdm', 'ka', 'ft')
421 if game_type_cd in elo_game_types:
427 def register_new_nick(session, player, new_nick):
429 Change the player record's nick to the newly found nick. Store the old
430 nick in the player_nicks table for that player.
432 session - SQLAlchemy database session factory
433 player - player record whose nick is changing
434 new_nick - the new nickname
436 # see if that nick already exists
437 stripped_nick = strip_colors(qfont_decode(player.nick))
439 player_nick = session.query(PlayerNick).filter_by(
440 player_id=player.player_id, stripped_nick=stripped_nick).one()
441 except NoResultFound, e:
442 # player_id/stripped_nick not found, create one
443 # but we don't store "Anonymous Player #N"
444 if not re.search('^Anonymous Player #\d+$', player.nick):
445 player_nick = PlayerNick()
446 player_nick.player_id = player.player_id
447 player_nick.stripped_nick = stripped_nick
448 player_nick.nick = player.nick
449 session.add(player_nick)
451 # We change to the new nick regardless
452 player.nick = new_nick
453 player.stripped_nick = strip_colors(qfont_decode(new_nick))
457 def update_fastest_cap(session, player_id, game_id, map_id, captime, mod):
459 Check the fastest cap time for the player and map. If there isn't
460 one, insert one. If there is, check if the passed time is faster.
463 # we don't record fastest cap times for bots or anonymous players
467 # see if a cap entry exists already
468 # then check to see if the new captime is faster
470 cur_fastest_cap = session.query(PlayerCaptime).filter_by(
471 player_id=player_id, map_id=map_id, mod=mod).one()
473 # current captime is faster, so update
474 if captime < cur_fastest_cap.fastest_cap:
475 cur_fastest_cap.fastest_cap = captime
476 cur_fastest_cap.game_id = game_id
477 cur_fastest_cap.create_dt = datetime.datetime.utcnow()
478 session.add(cur_fastest_cap)
480 except NoResultFound, e:
481 # none exists, so insert
482 cur_fastest_cap = PlayerCaptime(player_id, game_id, map_id, captime,
484 session.add(cur_fastest_cap)
488 def update_server(server, name, hashkey, ip_addr, port, revision, impure_cvars):
490 Updates the server in the given DB session, if needed.
492 :param server: The found server instance.
493 :param name: The incoming server name.
494 :param hashkey: The incoming server hashkey.
495 :param ip_addr: The incoming server IP address.
496 :param port: The incoming server port.
497 :param revision: The incoming server revision.
498 :param impure_cvars: The incoming number of impure server cvars.
501 # ensure the two int attributes are actually ints
508 impure_cvars = int(impure_cvars)
513 if name and server.name != name:
516 if hashkey and server.hashkey != hashkey:
517 server.hashkey = hashkey
519 if ip_addr and server.ip_addr != ip_addr:
520 server.ip_addr = ip_addr
522 if port and server.port != port:
525 if revision and server.revision != revision:
526 server.revision = revision
528 if impure_cvars and server.impure_cvars != impure_cvars:
529 server.impure_cvars = impure_cvars
530 server.pure_ind = True if impure_cvars == 0 else False
536 def get_or_create_server(session, name, hashkey, ip_addr, revision, port, impure_cvars):
538 Find a server by name or create one if not found. Parameters:
540 session - SQLAlchemy database session factory
541 name - server name of the server to be found or created
542 hashkey - server hashkey
543 ip_addr - the IP address of the server
544 revision - the xonotic revision number
545 port - the port number of the server
546 impure_cvars - the number of impure cvar changes
548 servers_q = DBSession.query(Server).filter(Server.active_ind)
551 # if the hashkey is provided, we'll use that
552 servers_q = servers_q.filter((Server.name == name) or (Server.hashkey == hashkey))
554 # otherwise, it is just by name
555 servers_q = servers_q.filter(Server.name == name)
557 # order by the hashkey, which means any hashkey match will appear first if there are multiple
558 servers = servers_q.order_by(Server.hashkey, Server.create_dt).all()
560 if len(servers) == 0:
561 server = Server(name=name, hashkey=hashkey)
564 log.debug("Created server {} with hashkey {}.".format(server.server_id, server.hashkey))
567 if len(servers) == 1:
568 log.info("Found existing server {}.".format(server.server_id))
570 elif len(servers) > 1:
571 server_id_list = ", ".join(["{}".format(s.server_id) for s in servers])
572 log.warn("Multiple servers found ({})! Using the first one ({})."
573 .format(server_id_list, server.server_id))
575 if update_server(server, name, hashkey, ip_addr, port, revision, impure_cvars):
581 def get_or_create_map(session=None, name=None):
583 Find a map by name or create one if not found. Parameters:
585 session - SQLAlchemy database session factory
586 name - map name of the map to be found or created
589 # find one by the name, if it exists
590 gmap = session.query(Map).filter_by(name=name).one()
591 log.debug("Found map id {0}: {1}".format(gmap.map_id,
593 except NoResultFound, e:
594 gmap = Map(name=name)
597 log.debug("Created map id {0}: {1}".format(gmap.map_id,
599 except MultipleResultsFound, e:
600 # multiple found, so use the first one but warn
602 gmaps = session.query(Map).filter_by(name=name).order_by(
605 log.debug("Found map id {0}: {1} but found \
606 multiple".format(gmap.map_id, gmap.name))
611 def create_game(session, start_dt, game_type_cd, server_id, map_id,
612 match_id, duration, mod, winner=None):
614 Creates a game. Parameters:
616 session - SQLAlchemy database session factory
617 start_dt - when the game started (datetime object)
618 game_type_cd - the game type of the game being played
619 server_id - server identifier of the server hosting the game
620 map_id - map on which the game was played
621 winner - the team id of the team that won
622 duration - how long the game lasted
623 mod - mods in use during the game
625 seq = Sequence('games_game_id_seq')
626 game_id = session.execute(seq)
627 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
628 server_id=server_id, map_id=map_id, winner=winner)
629 game.match_id = match_id
632 # There is some drift between start_dt (provided by app) and create_dt
633 # (default in the database), so we'll make them the same until this is
635 game.create_dt = start_dt
638 game.duration = datetime.timedelta(seconds=int(round(float(duration))))
643 session.query(Game).filter(Game.server_id==server_id).\
644 filter(Game.match_id==match_id).one()
646 log.debug("Error: game with same server and match_id found! Ignoring.")
648 # if a game under the same server and match_id found,
649 # this is a duplicate game and can be ignored
650 raise pyramid.httpexceptions.HTTPOk('OK')
651 except NoResultFound, e:
652 # server_id/match_id combination not found. game is ok to insert
655 log.debug("Created game id {0} on server {1}, map {2} at \
656 {3}".format(game.game_id,
657 server_id, map_id, start_dt))
662 def get_or_create_player(session=None, hashkey=None, nick=None):
664 Finds a player by hashkey or creates a new one (along with a
665 corresponding hashkey entry. Parameters:
667 session - SQLAlchemy database session factory
668 hashkey - hashkey of the player to be found or created
669 nick - nick of the player (in case of a first time create)
672 if re.search('^bot#\d+', hashkey):
673 player = session.query(Player).filter_by(player_id=1).one()
674 # if we have an untracked player
675 elif re.search('^player#\d+$', hashkey):
676 player = session.query(Player).filter_by(player_id=2).one()
677 # else it is a tracked player
679 # see if the player is already in the database
680 # if not, create one and the hashkey along with it
682 hk = session.query(Hashkey).filter_by(
683 hashkey=hashkey).one()
684 player = session.query(Player).filter_by(
685 player_id=hk.player_id).one()
686 log.debug("Found existing player {0} with hashkey {1}".format(
687 player.player_id, hashkey))
693 # if nick is given to us, use it. If not, use "Anonymous Player"
694 # with a suffix added for uniqueness.
696 player.nick = nick[:128]
697 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
699 player.nick = "Anonymous Player #{0}".format(player.player_id)
700 player.stripped_nick = player.nick
702 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
704 log.debug("Created player {0} ({2}) with hashkey {1}".format(
705 player.player_id, hashkey, player.nick.encode('utf-8')))
710 def create_default_game_stat(session, game_type_cd):
711 """Creates a blanked-out pgstat record for the given game type"""
713 # this is what we have to do to get partitioned records in - grab the
714 # sequence value first, then insert using the explicit ID (vs autogenerate)
715 seq = Sequence('player_game_stats_player_game_stat_id_seq')
716 pgstat_id = session.execute(seq)
717 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
718 create_dt=datetime.datetime.utcnow())
720 if game_type_cd == 'as':
721 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
723 if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
724 pgstat.kills = pgstat.deaths = pgstat.suicides = 0
726 if game_type_cd == 'cq':
727 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
730 if game_type_cd == 'ctf':
731 pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
732 pgstat.returns = pgstat.carrier_frags = 0
734 if game_type_cd == 'cts':
737 if game_type_cd == 'dom':
738 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
741 if game_type_cd == 'ft':
742 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
744 if game_type_cd == 'ka':
745 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
746 pgstat.carrier_frags = 0
747 pgstat.time = datetime.timedelta(seconds=0)
749 if game_type_cd == 'kh':
750 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
751 pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
752 pgstat.carrier_frags = 0
754 if game_type_cd == 'lms':
755 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
757 if game_type_cd == 'nb':
758 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
761 if game_type_cd == 'rc':
762 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
767 def create_game_stat(session, game_meta, game, server, gmap, player, events):
768 """Game stats handler for all game types"""
770 game_type_cd = game.game_type_cd
772 pgstat = create_default_game_stat(session, game_type_cd)
774 # these fields should be on every pgstat record
775 pgstat.game_id = game.game_id
776 pgstat.player_id = player.player_id
777 pgstat.nick = events.get('n', 'Anonymous Player')[:128]
778 pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
779 pgstat.score = int(round(float(events.get('scoreboard-score', 0))))
780 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
781 pgstat.rank = int(events.get('rank', None))
782 pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
784 if pgstat.nick != player.nick \
785 and player.player_id > 2 \
786 and pgstat.nick != 'Anonymous Player':
787 register_new_nick(session, player, pgstat.nick)
791 # gametype-specific stuff is handled here. if passed to us, we store it
792 for (key,value) in events.items():
793 if key == 'wins': wins = True
794 if key == 't': pgstat.team = int(value)
796 if key == 'scoreboard-drops': pgstat.drops = int(value)
797 if key == 'scoreboard-returns': pgstat.returns = int(value)
798 if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
799 if key == 'scoreboard-pickups': pgstat.pickups = int(value)
800 if key == 'scoreboard-caps': pgstat.captures = int(value)
801 if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
802 if key == 'scoreboard-deaths': pgstat.deaths = int(value)
803 if key == 'scoreboard-kills': pgstat.kills = int(value)
804 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
805 if key == 'scoreboard-objectives': pgstat.collects = int(value)
806 if key == 'scoreboard-captured': pgstat.captures = int(value)
807 if key == 'scoreboard-released': pgstat.drops = int(value)
808 if key == 'scoreboard-fastest':
809 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
810 if key == 'scoreboard-takes': pgstat.pickups = int(value)
811 if key == 'scoreboard-ticks': pgstat.drops = int(value)
812 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
813 if key == 'scoreboard-bctime':
814 pgstat.time = datetime.timedelta(seconds=int(value))
815 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
816 if key == 'scoreboard-losses': pgstat.drops = int(value)
817 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
818 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
819 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
820 if key == 'scoreboard-lives': pgstat.lives = int(value)
821 if key == 'scoreboard-goals': pgstat.captures = int(value)
822 if key == 'scoreboard-faults': pgstat.drops = int(value)
823 if key == 'scoreboard-laps': pgstat.laps = int(value)
825 if key == 'avglatency': pgstat.avg_latency = float(value)
826 if key == 'scoreboard-captime':
827 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
828 if game.game_type_cd == 'ctf':
829 update_fastest_cap(session, player.player_id, game.game_id,
830 gmap.map_id, pgstat.fastest, game.mod)
832 # there is no "winning team" field, so we have to derive it
833 if wins and pgstat.team is not None and game.winner is None:
834 game.winner = pgstat.team
842 def create_anticheats(session, pgstat, game, player, events):
843 """Anticheats handler for all game types"""
847 # all anticheat events are prefixed by "anticheat"
848 for (key,value) in events.items():
849 if key.startswith("anticheat"):
851 ac = PlayerGameAnticheat(
857 anticheats.append(ac)
859 except Exception as e:
860 log.debug("Could not parse value for key %s. Ignoring." % key)
865 def create_default_team_stat(session, game_type_cd):
866 """Creates a blanked-out teamstat record for the given game type"""
868 # this is what we have to do to get partitioned records in - grab the
869 # sequence value first, then insert using the explicit ID (vs autogenerate)
870 seq = Sequence('team_game_stats_team_game_stat_id_seq')
871 teamstat_id = session.execute(seq)
872 teamstat = TeamGameStat(team_game_stat_id=teamstat_id,
873 create_dt=datetime.datetime.utcnow())
875 # all team game modes have a score, so we'll zero that out always
878 if game_type_cd in 'ca' 'ft' 'lms' 'ka':
881 if game_type_cd == 'ctf':
887 def create_team_stat(session, game, events):
888 """Team stats handler for all game types"""
891 teamstat = create_default_team_stat(session, game.game_type_cd)
892 teamstat.game_id = game.game_id
894 # we should have a team ID if we have a 'Q' event
895 if re.match(r'^team#\d+$', events.get('Q', '')):
896 team = int(events.get('Q').replace('team#', ''))
899 # gametype-specific stuff is handled here. if passed to us, we store it
900 for (key,value) in events.items():
901 if key == 'scoreboard-score': teamstat.score = int(round(float(value)))
902 if key == 'scoreboard-caps': teamstat.caps = int(value)
903 if key == 'scoreboard-goals': teamstat.caps = int(value)
904 if key == 'scoreboard-rounds': teamstat.rounds = int(value)
906 session.add(teamstat)
907 except Exception as e:
913 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
914 """Weapon stats handler for all game types"""
917 # Version 1 of stats submissions doubled the data sent.
918 # To counteract this we divide the data by 2 only for
919 # POSTs coming from version 1.
921 version = int(game_meta['V'])
924 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
930 for (key,value) in events.items():
931 matched = re.search("acc-(.*?)-cnt-fired", key)
933 weapon_cd = matched.group(1)
935 # Weapon names changed for 0.8. We'll convert the old
936 # ones to use the new scheme as well.
937 mapped_weapon_cd = weapon_map.get(weapon_cd, weapon_cd)
939 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
940 pwstat_id = session.execute(seq)
941 pwstat = PlayerWeaponStat()
942 pwstat.player_weapon_stats_id = pwstat_id
943 pwstat.player_id = player.player_id
944 pwstat.game_id = game.game_id
945 pwstat.player_game_stat_id = pgstat.player_game_stat_id
946 pwstat.weapon_cd = mapped_weapon_cd
949 pwstat.nick = events['n']
951 pwstat.nick = events['P']
953 if 'acc-' + weapon_cd + '-cnt-fired' in events:
954 pwstat.fired = int(round(float(
955 events['acc-' + weapon_cd + '-cnt-fired'])))
956 if 'acc-' + weapon_cd + '-fired' in events:
957 pwstat.max = int(round(float(
958 events['acc-' + weapon_cd + '-fired'])))
959 if 'acc-' + weapon_cd + '-cnt-hit' in events:
960 pwstat.hit = int(round(float(
961 events['acc-' + weapon_cd + '-cnt-hit'])))
962 if 'acc-' + weapon_cd + '-hit' in events:
963 pwstat.actual = int(round(float(
964 events['acc-' + weapon_cd + '-hit'])))
965 if 'acc-' + weapon_cd + '-frags' in events:
966 pwstat.frags = int(round(float(
967 events['acc-' + weapon_cd + '-frags'])))
970 pwstat.fired = pwstat.fired/2
971 pwstat.max = pwstat.max/2
972 pwstat.hit = pwstat.hit/2
973 pwstat.actual = pwstat.actual/2
974 pwstat.frags = pwstat.frags/2
977 pwstats.append(pwstat)
982 def get_ranks(session, player_ids, game_type_cd):
984 Gets the rank entries for all players in the given list, returning a dict
985 of player_id -> PlayerRank instance. The rank entry corresponds to the
986 game type of the parameter passed in as well.
989 for pr in session.query(PlayerRank).\
990 filter(PlayerRank.player_id.in_(player_ids)).\
991 filter(PlayerRank.game_type_cd == game_type_cd).\
993 ranks[pr.player_id] = pr
998 def submit_stats(request):
1000 Entry handler for POST stats submissions.
1003 # placeholder for the actual session
1006 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
1007 "----- END REQUEST BODY -----\n\n")
1009 (idfp, status) = verify_request(request)
1010 (game_meta, raw_players, raw_teams) = parse_stats_submission(request.body)
1011 revision = game_meta.get('R', 'unknown')
1012 duration = game_meta.get('D', None)
1014 # only players present at the end of the match are eligible for stats
1015 raw_players = filter(played_in_game, raw_players)
1017 do_precondition_checks(request, game_meta, raw_players)
1019 # the "duel" gametype is fake
1020 if len(raw_players) == 2 \
1021 and num_real_players(raw_players) == 2 \
1022 and game_meta['G'] == 'dm':
1023 game_meta['G'] = 'duel'
1025 #----------------------------------------------------------------------
1026 # Actual setup (inserts/updates) below here
1027 #----------------------------------------------------------------------
1028 session = DBSession()
1030 game_type_cd = game_meta['G']
1032 # All game types create Game, Server, Map, and Player records
1034 server = get_or_create_server(
1037 name = game_meta['S'],
1038 revision = revision,
1039 ip_addr = get_remote_addr(request),
1040 port = game_meta.get('U', None),
1041 impure_cvars = game_meta.get('C', 0))
1043 gmap = get_or_create_map(
1045 name = game_meta['M'])
1049 start_dt = datetime.datetime.utcnow(),
1050 server_id = server.server_id,
1051 game_type_cd = game_type_cd,
1052 map_id = gmap.map_id,
1053 match_id = game_meta['I'],
1054 duration = duration,
1055 mod = game_meta.get('O', None))
1057 # keep track of the players we've seen
1061 for events in raw_players:
1062 player = get_or_create_player(
1064 hashkey = events['P'],
1065 nick = events.get('n', None))
1067 pgstat = create_game_stat(session, game_meta, game, server,
1068 gmap, player, events)
1069 pgstats.append(pgstat)
1071 if player.player_id > 1:
1072 anticheats = create_anticheats(session, pgstat, game, player, events)
1074 if player.player_id > 2:
1075 player_ids.append(player.player_id)
1076 hashkeys[player.player_id] = events['P']
1078 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
1079 pwstats = create_weapon_stats(session, game_meta, game, player,
1082 # store them on games for easy access
1083 game.players = player_ids
1085 for events in raw_teams:
1087 teamstat = create_team_stat(session, game, events)
1088 except Exception as e:
1091 if server.elo_ind and gametype_elo_eligible(game_type_cd):
1092 ep = EloProcessor(session, game, pgstats)
1096 log.debug('Success! Stats recorded.')
1098 # ranks are fetched after we've done the "real" processing
1099 ranks = get_ranks(session, player_ids, game_type_cd)
1101 # plain text response
1102 request.response.content_type = 'text/plain'
1105 "now" : calendar.timegm(datetime.datetime.utcnow().timetuple()),
1109 "player_ids" : player_ids,
1110 "hashkeys" : hashkeys,
1115 except Exception as e: