4 import pyramid.httpexceptions
\r
7 import sqlalchemy.sql.expression as expr
\r
8 from pyramid.response import Response
\r
9 from sqlalchemy import Sequence
\r
10 from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
\r
11 from xonstat.d0_blind_id import d0_blind_id_verify
\r
12 from xonstat.elo import process_elos
\r
13 from xonstat.models import *
\r
14 from xonstat.util import strip_colors, qfont_decode
\r
17 log = logging.getLogger(__name__)
\r
20 def parse_stats_submission(body):
\r
22 Parses the POST request body for a stats submission
\r
24 # storage vars for the request body
\r
30 # we're not in either stanza to start
\r
33 for line in body.split('\n'):
\r
35 (key, value) = line.strip().split(' ', 1)
\r
37 # Server (S) and Nick (n) fields can have international characters.
\r
39 value = unicode(value, 'utf-8')
\r
41 if key not in 'P' 'Q' 'n' 'e' 't' 'i':
\r
42 game_meta[key] = value
\r
44 if key == 'Q' or key == 'P':
\r
45 #log.debug('Found a {0}'.format(key))
\r
46 #log.debug('in_Q: {0}'.format(in_Q))
\r
47 #log.debug('in_P: {0}'.format(in_P))
\r
48 #log.debug('events: {0}'.format(events))
\r
50 # check where we were before and append events accordingly
\r
51 if in_Q and len(events) > 0:
\r
52 #log.debug('creating a team (Q) entry')
\r
53 teams.append(events)
\r
55 elif in_P and len(events) > 0:
\r
56 #log.debug('creating a player (P) entry')
\r
57 players.append(events)
\r
61 #log.debug('key == P')
\r
65 #log.debug('key == Q')
\r
72 (subkey, subvalue) = value.split(' ', 1)
\r
73 events[subkey] = subvalue
\r
79 # no key/value pair - move on to the next line
\r
82 # add the last entity we were working on
\r
83 if in_P and len(events) > 0:
\r
84 players.append(events)
\r
85 elif in_Q and len(events) > 0:
\r
86 teams.append(events)
\r
88 return (game_meta, players, teams)
\r
91 def is_blank_game(gametype, players):
\r
92 """Determine if this is a blank game or not. A blank game is either:
\r
94 1) a match that ended in the warmup stage, where accuracy events are not
\r
95 present (for non-CTS games)
\r
97 2) a match in which no player made a positive or negative score AND was
\r
100 ... or for CTS, which doesn't record accuracy events
\r
102 1) a match in which no player made a fastest lap AND was
\r
105 r = re.compile(r'acc-.*-cnt-fired')
\r
106 flg_nonzero_score = False
\r
107 flg_acc_events = False
\r
108 flg_fastest_lap = False
\r
110 for events in players:
\r
111 if is_real_player(events) and played_in_game(events):
\r
112 for (key,value) in events.items():
\r
113 if key == 'scoreboard-score' and value != 0:
\r
114 flg_nonzero_score = True
\r
116 flg_acc_events = True
\r
117 if key == 'scoreboard-fastest':
\r
118 flg_fastest_lap = True
\r
120 if gametype == 'cts':
\r
121 return not flg_fastest_lap
\r
123 return not (flg_nonzero_score and flg_acc_events)
\r
126 def get_remote_addr(request):
\r
127 """Get the Xonotic server's IP address"""
\r
128 if 'X-Forwarded-For' in request.headers:
\r
129 return request.headers['X-Forwarded-For']
\r
131 return request.remote_addr
\r
134 def is_supported_gametype(gametype, version):
\r
135 """Whether a gametype is supported or not"""
\r
136 is_supported = False
\r
138 # if the type can be supported, but with version constraints, uncomment
\r
139 # here and add the restriction for a specific version below
\r
140 supported_game_types = (
\r
158 if gametype in supported_game_types:
\r
159 is_supported = True
\r
161 is_supported = False
\r
163 # some game types were buggy before revisions, thus this additional filter
\r
164 if gametype == 'ca' and version <= 5:
\r
165 is_supported = False
\r
167 return is_supported
\r
170 def verify_request(request):
\r
171 """Verify requests using the d0_blind_id library"""
\r
173 # first determine if we should be verifying or not
\r
174 val_verify_requests = request.registry.settings.get('xonstat.verify_requests', 'true')
\r
175 if val_verify_requests == "true":
\r
176 flg_verify_requests = True
\r
178 flg_verify_requests = False
\r
181 (idfp, status) = d0_blind_id_verify(
\r
182 sig=request.headers['X-D0-Blind-Id-Detached-Signature'],
\r
184 postdata=request.body)
\r
186 log.debug('\nidfp: {0}\nstatus: {1}'.format(idfp, status))
\r
191 if flg_verify_requests and not idfp:
\r
192 log.debug("ERROR: Unverified request")
\r
193 raise pyramid.httpexceptions.HTTPUnauthorized("Unverified request")
\r
195 return (idfp, status)
\r
198 def do_precondition_checks(request, game_meta, raw_players):
\r
199 """Precondition checks for ALL gametypes.
\r
200 These do not require a database connection."""
\r
201 if not has_required_metadata(game_meta):
\r
202 log.debug("ERROR: Required game meta missing")
\r
203 raise pyramid.httpexceptions.HTTPUnprocessableEntity("Missing game meta")
\r
206 version = int(game_meta['V'])
\r
208 log.debug("ERROR: Required game meta invalid")
\r
209 raise pyramid.httpexceptions.HTTPUnprocessableEntity("Invalid game meta")
\r
211 if not is_supported_gametype(game_meta['G'], version):
\r
212 log.debug("ERROR: Unsupported gametype")
\r
213 raise pyramid.httpexceptions.HTTPOk("OK")
\r
215 if not has_minimum_real_players(request.registry.settings, raw_players):
\r
216 log.debug("ERROR: Not enough real players")
\r
217 raise pyramid.httpexceptions.HTTPOk("OK")
\r
219 if is_blank_game(game_meta['G'], raw_players):
\r
220 log.debug("ERROR: Blank game")
\r
221 raise pyramid.httpexceptions.HTTPOk("OK")
\r
224 def is_real_player(events):
\r
226 Determines if a given set of events correspond with a non-bot
\r
228 if not events['P'].startswith('bot'):
\r
234 def played_in_game(events):
\r
236 Determines if a given set of player events correspond with a player who
\r
237 played in the game (matches 1 and scoreboardvalid 1)
\r
239 if 'matches' in events and 'scoreboardvalid' in events:
\r
245 def num_real_players(player_events):
\r
247 Returns the number of real players (those who played
\r
248 and are on the scoreboard).
\r
252 for events in player_events:
\r
253 if is_real_player(events) and played_in_game(events):
\r
256 return real_players
\r
259 def has_minimum_real_players(settings, player_events):
\r
261 Determines if the collection of player events has enough "real" players
\r
262 to store in the database. The minimum setting comes from the config file
\r
263 under the setting xonstat.minimum_real_players.
\r
265 flg_has_min_real_players = True
\r
268 minimum_required_players = int(
\r
269 settings['xonstat.minimum_required_players'])
\r
271 minimum_required_players = 2
\r
273 real_players = num_real_players(player_events)
\r
275 if real_players < minimum_required_players:
\r
276 flg_has_min_real_players = False
\r
278 return flg_has_min_real_players
\r
281 def has_required_metadata(metadata):
\r
283 Determines if a give set of metadata has enough data to create a game,
\r
284 server, and map with.
\r
286 flg_has_req_metadata = True
\r
288 if 'T' not in metadata or\
\r
289 'G' not in metadata or\
\r
290 'M' not in metadata or\
\r
291 'I' not in metadata or\
\r
292 'S' not in metadata:
\r
293 flg_has_req_metadata = False
\r
295 return flg_has_req_metadata
\r
298 def should_do_weapon_stats(game_type_cd):
\r
299 """True of the game type should record weapon stats. False otherwise."""
\r
300 if game_type_cd in 'cts':
\r
306 def should_do_elos(game_type_cd):
\r
307 """True of the game type should process Elos. False otherwise."""
\r
308 elo_game_types = ('duel', 'dm', 'ca', 'ctf', 'tdm', 'ka', 'ft')
\r
310 if game_type_cd in elo_game_types:
\r
316 def register_new_nick(session, player, new_nick):
\r
318 Change the player record's nick to the newly found nick. Store the old
\r
319 nick in the player_nicks table for that player.
\r
321 session - SQLAlchemy database session factory
\r
322 player - player record whose nick is changing
\r
323 new_nick - the new nickname
\r
325 # see if that nick already exists
\r
326 stripped_nick = strip_colors(qfont_decode(player.nick))
\r
328 player_nick = session.query(PlayerNick).filter_by(
\r
329 player_id=player.player_id, stripped_nick=stripped_nick).one()
\r
330 except NoResultFound, e:
\r
331 # player_id/stripped_nick not found, create one
\r
332 # but we don't store "Anonymous Player #N"
\r
333 if not re.search('^Anonymous Player #\d+$', player.nick):
\r
334 player_nick = PlayerNick()
\r
335 player_nick.player_id = player.player_id
\r
336 player_nick.stripped_nick = stripped_nick
\r
337 player_nick.nick = player.nick
\r
338 session.add(player_nick)
\r
340 # We change to the new nick regardless
\r
341 player.nick = new_nick
\r
342 player.stripped_nick = strip_colors(qfont_decode(new_nick))
\r
343 session.add(player)
\r
346 def update_fastest_cap(session, player_id, game_id, map_id, captime):
\r
348 Check the fastest cap time for the player and map. If there isn't
\r
349 one, insert one. If there is, check if the passed time is faster.
\r
352 # we don't record fastest cap times for bots or anonymous players
\r
356 # see if a cap entry exists already
\r
357 # then check to see if the new captime is faster
\r
359 cur_fastest_cap = session.query(PlayerCaptime).filter_by(
\r
360 player_id=player_id, map_id=map_id).one()
\r
362 # current captime is faster, so update
\r
363 if captime < cur_fastest_cap.fastest_cap:
\r
364 cur_fastest_cap.fastest_cap = captime
\r
365 cur_fastest_cap.game_id = game_id
\r
366 cur_fastest_cap.create_dt = datetime.datetime.utcnow()
\r
367 session.add(cur_fastest_cap)
\r
369 except NoResultFound, e:
\r
370 # none exists, so insert
\r
371 cur_fastest_cap = PlayerCaptime(player_id, game_id, map_id, captime)
\r
372 session.add(cur_fastest_cap)
\r
376 def get_or_create_server(session, name, hashkey, ip_addr, revision, port):
\r
378 Find a server by name or create one if not found. Parameters:
\r
380 session - SQLAlchemy database session factory
\r
381 name - server name of the server to be found or created
\r
382 hashkey - server hashkey
\r
391 # finding by hashkey is preferred, but if not we will fall
\r
392 # back to using name only, which can result in dupes
\r
393 if hashkey is not None:
\r
394 servers = session.query(Server).\
\r
395 filter_by(hashkey=hashkey).\
\r
396 order_by(expr.desc(Server.create_dt)).limit(1).all()
\r
398 if len(servers) > 0:
\r
399 server = servers[0]
\r
400 log.debug("Found existing server {0} by hashkey ({1})".format(
\r
401 server.server_id, server.hashkey))
\r
403 servers = session.query(Server).\
\r
404 filter_by(name=name).\
\r
405 order_by(expr.desc(Server.create_dt)).limit(1).all()
\r
407 if len(servers) > 0:
\r
408 server = servers[0]
\r
409 log.debug("Found existing server {0} by name".format(server.server_id))
\r
411 # still haven't found a server by hashkey or name, so we need to create one
\r
413 server = Server(name=name, hashkey=hashkey)
\r
414 session.add(server)
\r
416 log.debug("Created server {0} with hashkey {1}".format(
\r
417 server.server_id, server.hashkey))
\r
419 # detect changed fields
\r
420 if server.name != name:
\r
422 session.add(server)
\r
424 if server.hashkey != hashkey:
\r
425 server.hashkey = hashkey
\r
426 session.add(server)
\r
428 if server.ip_addr != ip_addr:
\r
429 server.ip_addr = ip_addr
\r
430 session.add(server)
\r
432 if server.port != port:
\r
434 session.add(server)
\r
436 if server.revision != revision:
\r
437 server.revision = revision
\r
438 session.add(server)
\r
443 def get_or_create_map(session=None, name=None):
\r
445 Find a map by name or create one if not found. Parameters:
\r
447 session - SQLAlchemy database session factory
\r
448 name - map name of the map to be found or created
\r
451 # find one by the name, if it exists
\r
452 gmap = session.query(Map).filter_by(name=name).one()
\r
453 log.debug("Found map id {0}: {1}".format(gmap.map_id,
\r
455 except NoResultFound, e:
\r
456 gmap = Map(name=name)
\r
459 log.debug("Created map id {0}: {1}".format(gmap.map_id,
\r
461 except MultipleResultsFound, e:
\r
462 # multiple found, so use the first one but warn
\r
464 gmaps = session.query(Map).filter_by(name=name).order_by(
\r
467 log.debug("Found map id {0}: {1} but found \
\r
468 multiple".format(gmap.map_id, gmap.name))
\r
473 def create_game(session, start_dt, game_type_cd, server_id, map_id,
\r
474 match_id, duration, mod, winner=None):
\r
476 Creates a game. Parameters:
\r
478 session - SQLAlchemy database session factory
\r
479 start_dt - when the game started (datetime object)
\r
480 game_type_cd - the game type of the game being played
\r
481 server_id - server identifier of the server hosting the game
\r
482 map_id - map on which the game was played
\r
483 winner - the team id of the team that won
\r
484 duration - how long the game lasted
\r
485 mod - mods in use during the game
\r
487 seq = Sequence('games_game_id_seq')
\r
488 game_id = session.execute(seq)
\r
489 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
\r
490 server_id=server_id, map_id=map_id, winner=winner)
\r
491 game.match_id = match_id
\r
492 game.mod = mod[:64]
\r
495 game.duration = datetime.timedelta(seconds=int(round(float(duration))))
\r
500 session.query(Game).filter(Game.server_id==server_id).\
\r
501 filter(Game.match_id==match_id).one()
\r
503 log.debug("Error: game with same server and match_id found! Ignoring.")
\r
505 # if a game under the same server and match_id found,
\r
506 # this is a duplicate game and can be ignored
\r
507 raise pyramid.httpexceptions.HTTPOk('OK')
\r
508 except NoResultFound, e:
\r
509 # server_id/match_id combination not found. game is ok to insert
\r
512 log.debug("Created game id {0} on server {1}, map {2} at \
\r
513 {3}".format(game.game_id,
\r
514 server_id, map_id, start_dt))
\r
519 def get_or_create_player(session=None, hashkey=None, nick=None):
\r
521 Finds a player by hashkey or creates a new one (along with a
\r
522 corresponding hashkey entry. Parameters:
\r
524 session - SQLAlchemy database session factory
\r
525 hashkey - hashkey of the player to be found or created
\r
526 nick - nick of the player (in case of a first time create)
\r
529 if re.search('^bot#\d+$', hashkey) or re.search('^bot#\d+#', hashkey):
\r
530 player = session.query(Player).filter_by(player_id=1).one()
\r
531 # if we have an untracked player
\r
532 elif re.search('^player#\d+$', hashkey):
\r
533 player = session.query(Player).filter_by(player_id=2).one()
\r
534 # else it is a tracked player
\r
536 # see if the player is already in the database
\r
537 # if not, create one and the hashkey along with it
\r
539 hk = session.query(Hashkey).filter_by(
\r
540 hashkey=hashkey).one()
\r
541 player = session.query(Player).filter_by(
\r
542 player_id=hk.player_id).one()
\r
543 log.debug("Found existing player {0} with hashkey {1}".format(
\r
544 player.player_id, hashkey))
\r
547 session.add(player)
\r
550 # if nick is given to us, use it. If not, use "Anonymous Player"
\r
551 # with a suffix added for uniqueness.
\r
553 player.nick = nick[:128]
\r
554 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
\r
556 player.nick = "Anonymous Player #{0}".format(player.player_id)
\r
557 player.stripped_nick = player.nick
\r
559 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
\r
561 log.debug("Created player {0} ({2}) with hashkey {1}".format(
\r
562 player.player_id, hashkey, player.nick.encode('utf-8')))
\r
567 def create_default_game_stat(session, game_type_cd):
\r
568 """Creates a blanked-out pgstat record for the given game type"""
\r
570 # this is what we have to do to get partitioned records in - grab the
\r
571 # sequence value first, then insert using the explicit ID (vs autogenerate)
\r
572 seq = Sequence('player_game_stats_player_game_stat_id_seq')
\r
573 pgstat_id = session.execute(seq)
\r
574 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
\r
575 create_dt=datetime.datetime.utcnow())
\r
577 if game_type_cd == 'as':
\r
578 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
\r
580 if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
\r
581 pgstat.kills = pgstat.deaths = pgstat.suicides = 0
\r
583 if game_type_cd == 'cq':
\r
584 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
\r
587 if game_type_cd == 'ctf':
\r
588 pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
\r
589 pgstat.returns = pgstat.carrier_frags = 0
\r
591 if game_type_cd == 'cts':
\r
594 if game_type_cd == 'dom':
\r
595 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
598 if game_type_cd == 'ft':
\r
599 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
\r
601 if game_type_cd == 'ka':
\r
602 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
603 pgstat.carrier_frags = 0
\r
604 pgstat.time = datetime.timedelta(seconds=0)
\r
606 if game_type_cd == 'kh':
\r
607 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
608 pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
\r
609 pgstat.carrier_frags = 0
\r
611 if game_type_cd == 'lms':
\r
612 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
\r
614 if game_type_cd == 'nb':
\r
615 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
\r
618 if game_type_cd == 'rc':
\r
619 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
\r
624 def create_game_stat(session, game_meta, game, server, gmap, player, events):
\r
625 """Game stats handler for all game types"""
\r
627 game_type_cd = game.game_type_cd
\r
629 pgstat = create_default_game_stat(session, game_type_cd)
\r
631 # these fields should be on every pgstat record
\r
632 pgstat.game_id = game.game_id
\r
633 pgstat.player_id = player.player_id
\r
634 pgstat.nick = events.get('n', 'Anonymous Player')[:128]
\r
635 pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
\r
636 pgstat.score = int(round(float(events.get('scoreboard-score', 0))))
\r
637 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
\r
638 pgstat.rank = int(events.get('rank', None))
\r
639 pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
\r
641 if pgstat.nick != player.nick \
\r
642 and player.player_id > 2 \
\r
643 and pgstat.nick != 'Anonymous Player':
\r
644 register_new_nick(session, player, pgstat.nick)
\r
648 # gametype-specific stuff is handled here. if passed to us, we store it
\r
649 for (key,value) in events.items():
\r
650 if key == 'wins': wins = True
\r
651 if key == 't': pgstat.team = int(value)
\r
653 if key == 'scoreboard-drops': pgstat.drops = int(value)
\r
654 if key == 'scoreboard-returns': pgstat.returns = int(value)
\r
655 if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
\r
656 if key == 'scoreboard-pickups': pgstat.pickups = int(value)
\r
657 if key == 'scoreboard-caps': pgstat.captures = int(value)
\r
658 if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
\r
659 if key == 'scoreboard-deaths': pgstat.deaths = int(value)
\r
660 if key == 'scoreboard-kills': pgstat.kills = int(value)
\r
661 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
\r
662 if key == 'scoreboard-objectives': pgstat.collects = int(value)
\r
663 if key == 'scoreboard-captured': pgstat.captures = int(value)
\r
664 if key == 'scoreboard-released': pgstat.drops = int(value)
\r
665 if key == 'scoreboard-fastest':
\r
666 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
\r
667 if key == 'scoreboard-takes': pgstat.pickups = int(value)
\r
668 if key == 'scoreboard-ticks': pgstat.drops = int(value)
\r
669 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
\r
670 if key == 'scoreboard-bctime':
\r
671 pgstat.time = datetime.timedelta(seconds=int(value))
\r
672 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
\r
673 if key == 'scoreboard-losses': pgstat.drops = int(value)
\r
674 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
\r
675 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
\r
676 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
\r
677 if key == 'scoreboard-lives': pgstat.lives = int(value)
\r
678 if key == 'scoreboard-goals': pgstat.captures = int(value)
\r
679 if key == 'scoreboard-faults': pgstat.drops = int(value)
\r
680 if key == 'scoreboard-laps': pgstat.laps = int(value)
\r
682 if key == 'avglatency': pgstat.avg_latency = float(value)
\r
683 if key == 'scoreboard-captime':
\r
684 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
\r
685 if game.game_type_cd == 'ctf':
\r
686 update_fastest_cap(session, player.player_id, game.game_id,
\r
687 gmap.map_id, pgstat.fastest)
\r
689 # there is no "winning team" field, so we have to derive it
\r
690 if wins and pgstat.team is not None and game.winner is None:
\r
691 game.winner = pgstat.team
\r
694 session.add(pgstat)
\r
699 def create_default_team_stat(session, game_type_cd):
\r
700 """Creates a blanked-out teamstat record for the given game type"""
\r
702 # this is what we have to do to get partitioned records in - grab the
\r
703 # sequence value first, then insert using the explicit ID (vs autogenerate)
\r
704 seq = Sequence('team_game_stats_team_game_stat_id_seq')
\r
705 teamstat_id = session.execute(seq)
\r
706 teamstat = TeamGameStat(team_game_stat_id=teamstat_id,
\r
707 create_dt=datetime.datetime.utcnow())
\r
709 # all team game modes have a score, so we'll zero that out always
\r
712 if game_type_cd in 'ca' 'ft' 'lms' 'ka':
\r
713 teamstat.rounds = 0
\r
715 if game_type_cd == 'ctf':
\r
721 def create_team_stat(session, game, events):
\r
722 """Team stats handler for all game types"""
\r
725 teamstat = create_default_team_stat(session, game.game_type_cd)
\r
726 teamstat.game_id = game.game_id
\r
728 # we should have a team ID if we have a 'Q' event
\r
729 if re.match(r'^team#\d+$', events.get('Q', '')):
\r
730 team = int(events.get('Q').replace('team#', ''))
\r
731 teamstat.team = team
\r
733 # gametype-specific stuff is handled here. if passed to us, we store it
\r
734 for (key,value) in events.items():
\r
735 if key == 'scoreboard-score': teamstat.score = int(round(float(value)))
\r
736 if key == 'scoreboard-caps': teamstat.caps = int(value)
\r
737 if key == 'scoreboard-rounds': teamstat.rounds = int(value)
\r
739 session.add(teamstat)
\r
740 except Exception as e:
\r
746 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
\r
747 """Weapon stats handler for all game types"""
\r
750 # Version 1 of stats submissions doubled the data sent.
\r
751 # To counteract this we divide the data by 2 only for
\r
752 # POSTs coming from version 1.
\r
754 version = int(game_meta['V'])
\r
757 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
\r
763 for (key,value) in events.items():
\r
764 matched = re.search("acc-(.*?)-cnt-fired", key)
\r
766 weapon_cd = matched.group(1)
\r
767 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
\r
768 pwstat_id = session.execute(seq)
\r
769 pwstat = PlayerWeaponStat()
\r
770 pwstat.player_weapon_stats_id = pwstat_id
\r
771 pwstat.player_id = player.player_id
\r
772 pwstat.game_id = game.game_id
\r
773 pwstat.player_game_stat_id = pgstat.player_game_stat_id
\r
774 pwstat.weapon_cd = weapon_cd
\r
777 pwstat.nick = events['n']
\r
779 pwstat.nick = events['P']
\r
781 if 'acc-' + weapon_cd + '-cnt-fired' in events:
\r
782 pwstat.fired = int(round(float(
\r
783 events['acc-' + weapon_cd + '-cnt-fired'])))
\r
784 if 'acc-' + weapon_cd + '-fired' in events:
\r
785 pwstat.max = int(round(float(
\r
786 events['acc-' + weapon_cd + '-fired'])))
\r
787 if 'acc-' + weapon_cd + '-cnt-hit' in events:
\r
788 pwstat.hit = int(round(float(
\r
789 events['acc-' + weapon_cd + '-cnt-hit'])))
\r
790 if 'acc-' + weapon_cd + '-hit' in events:
\r
791 pwstat.actual = int(round(float(
\r
792 events['acc-' + weapon_cd + '-hit'])))
\r
793 if 'acc-' + weapon_cd + '-frags' in events:
\r
794 pwstat.frags = int(round(float(
\r
795 events['acc-' + weapon_cd + '-frags'])))
\r
798 pwstat.fired = pwstat.fired/2
\r
799 pwstat.max = pwstat.max/2
\r
800 pwstat.hit = pwstat.hit/2
\r
801 pwstat.actual = pwstat.actual/2
\r
802 pwstat.frags = pwstat.frags/2
\r
804 session.add(pwstat)
\r
805 pwstats.append(pwstat)
\r
810 def create_elos(session, game):
\r
811 """Elo handler for all game types."""
\r
813 process_elos(game, session)
\r
814 except Exception as e:
\r
815 log.debug('Error (non-fatal): elo processing failed.')
\r
818 def submit_stats(request):
\r
820 Entry handler for POST stats submissions.
\r
823 # placeholder for the actual session
\r
826 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
\r
827 "----- END REQUEST BODY -----\n\n")
\r
829 (idfp, status) = verify_request(request)
\r
830 (game_meta, raw_players, raw_teams) = parse_stats_submission(request.body)
\r
831 revision = game_meta.get('R', 'unknown')
\r
832 duration = game_meta.get('D', None)
\r
834 # only players present at the end of the match are eligible for stats
\r
835 raw_players = filter(played_in_game, raw_players)
\r
837 do_precondition_checks(request, game_meta, raw_players)
\r
839 # the "duel" gametype is fake
\r
840 if len(raw_players) == 2 \
\r
841 and num_real_players(raw_players) == 2 \
\r
842 and game_meta['G'] == 'dm':
\r
843 game_meta['G'] = 'duel'
\r
845 #----------------------------------------------------------------------
\r
846 # Actual setup (inserts/updates) below here
\r
847 #----------------------------------------------------------------------
\r
848 session = DBSession()
\r
850 game_type_cd = game_meta['G']
\r
852 # All game types create Game, Server, Map, and Player records
\r
854 server = get_or_create_server(
\r
857 name = game_meta['S'],
\r
858 revision = revision,
\r
859 ip_addr = get_remote_addr(request),
\r
860 port = game_meta.get('U', None))
\r
862 gmap = get_or_create_map(
\r
864 name = game_meta['M'])
\r
866 game = create_game(
\r
868 start_dt = datetime.datetime.utcnow(),
\r
869 server_id = server.server_id,
\r
870 game_type_cd = game_type_cd,
\r
871 map_id = gmap.map_id,
\r
872 match_id = game_meta['I'],
\r
873 duration = duration,
\r
874 mod = game_meta.get('O', None))
\r
876 for events in raw_players:
\r
877 player = get_or_create_player(
\r
879 hashkey = events['P'],
\r
880 nick = events.get('n', None))
\r
882 pgstat = create_game_stat(session, game_meta, game, server,
\r
883 gmap, player, events)
\r
885 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
\r
886 pwstats = create_weapon_stats(session, game_meta, game, player,
\r
889 for events in raw_teams:
\r
891 teamstat = create_team_stat(session, game, events)
\r
892 except Exception as e:
\r
895 if should_do_elos(game_type_cd):
\r
896 create_elos(session, game)
\r
899 log.debug('Success! Stats recorded.')
\r
900 return Response('200 OK')
\r
901 except Exception as e:
\r