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
29 for line in body.split('\n'):
\r
31 (key, value) = line.strip().split(' ', 1)
\r
33 # Server (S) and Nick (n) fields can have international characters.
\r
35 value = unicode(value, 'utf-8')
\r
37 if key not in 'P' 'n' 'e' 't' 'i':
\r
38 game_meta[key] = value
\r
41 # if we were working on a player record already, append
\r
42 # it and work on a new one (only set team info)
\r
44 players.append(events)
\r
50 (subkey, subvalue) = value.split(' ', 1)
\r
51 events[subkey] = subvalue
\r
57 # no key/value pair - move on to the next line
\r
60 # add the last player we were working on
\r
62 players.append(events)
\r
64 return (game_meta, players)
\r
67 def is_blank_game(gametype, players):
\r
68 """Determine if this is a blank game or not. A blank game is either:
\r
70 1) a match that ended in the warmup stage, where accuracy events are not
\r
71 present (for non-CTS games)
\r
73 2) a match in which no player made a positive or negative score AND was
\r
76 ... or for CTS, which doesn't record accuracy events
\r
78 1) a match in which no player made a fastest lap AND was
\r
81 r = re.compile(r'acc-.*-cnt-fired')
\r
82 flg_nonzero_score = False
\r
83 flg_acc_events = False
\r
84 flg_fastest_lap = False
\r
86 for events in players:
\r
87 if is_real_player(events) and played_in_game(events):
\r
88 for (key,value) in events.items():
\r
89 if key == 'scoreboard-score' and value != 0:
\r
90 flg_nonzero_score = True
\r
92 flg_acc_events = True
\r
93 if key == 'scoreboard-fastest':
\r
94 flg_fastest_lap = True
\r
96 if gametype == 'cts':
\r
97 return not flg_fastest_lap
\r
99 return not (flg_nonzero_score and flg_acc_events)
\r
102 def get_remote_addr(request):
\r
103 """Get the Xonotic server's IP address"""
\r
104 if 'X-Forwarded-For' in request.headers:
\r
105 return request.headers['X-Forwarded-For']
\r
107 return request.remote_addr
\r
110 def is_supported_gametype(gametype, version):
\r
111 """Whether a gametype is supported or not"""
\r
112 is_supported = False
\r
114 # if the type can be supported, but with version constraints, uncomment
\r
115 # here and add the restriction for a specific version below
\r
116 supported_game_types = (
\r
134 if gametype in supported_game_types:
\r
135 is_supported = True
\r
137 is_supported = False
\r
139 # some game types were buggy before revisions, thus this additional filter
\r
140 if gametype == 'ca' and version <= 5:
\r
141 is_supported = False
\r
143 return is_supported
\r
146 def verify_request(request):
\r
147 """Verify requests using the d0_blind_id library"""
\r
149 # first determine if we should be verifying or not
\r
150 val_verify_requests = request.registry.settings.get('xonstat.verify_requests', 'true')
\r
151 if val_verify_requests == "true":
\r
152 flg_verify_requests = True
\r
154 flg_verify_requests = False
\r
157 (idfp, status) = d0_blind_id_verify(
\r
158 sig=request.headers['X-D0-Blind-Id-Detached-Signature'],
\r
160 postdata=request.body)
\r
162 log.debug('\nidfp: {0}\nstatus: {1}'.format(idfp, status))
\r
167 if flg_verify_requests and not idfp:
\r
168 log.debug("ERROR: Unverified request")
\r
169 raise pyramid.httpexceptions.HTTPUnauthorized("Unverified request")
\r
171 return (idfp, status)
\r
174 def do_precondition_checks(request, game_meta, raw_players):
\r
175 """Precondition checks for ALL gametypes.
\r
176 These do not require a database connection."""
\r
177 if not has_required_metadata(game_meta):
\r
178 log.debug("ERROR: Required game meta missing")
\r
179 raise pyramid.httpexceptions.HTTPUnprocessableEntity("Missing game meta")
\r
182 version = int(game_meta['V'])
\r
184 log.debug("ERROR: Required game meta invalid")
\r
185 raise pyramid.httpexceptions.HTTPUnprocessableEntity("Invalid game meta")
\r
187 if not is_supported_gametype(game_meta['G'], version):
\r
188 log.debug("ERROR: Unsupported gametype")
\r
189 raise pyramid.httpexceptions.HTTPOk("OK")
\r
191 if not has_minimum_real_players(request.registry.settings, raw_players):
\r
192 log.debug("ERROR: Not enough real players")
\r
193 raise pyramid.httpexceptions.HTTPOk("OK")
\r
195 if is_blank_game(game_meta['G'], raw_players):
\r
196 log.debug("ERROR: Blank game")
\r
197 raise pyramid.httpexceptions.HTTPOk("OK")
\r
200 def is_real_player(events):
\r
202 Determines if a given set of events correspond with a non-bot
\r
204 if not events['P'].startswith('bot'):
\r
210 def played_in_game(events):
\r
212 Determines if a given set of player events correspond with a player who
\r
213 played in the game (matches 1 and scoreboardvalid 1)
\r
215 if 'matches' in events and 'scoreboardvalid' in events:
\r
221 def num_real_players(player_events):
\r
223 Returns the number of real players (those who played
\r
224 and are on the scoreboard).
\r
228 for events in player_events:
\r
229 if is_real_player(events) and played_in_game(events):
\r
232 return real_players
\r
235 def has_minimum_real_players(settings, player_events):
\r
237 Determines if the collection of player events has enough "real" players
\r
238 to store in the database. The minimum setting comes from the config file
\r
239 under the setting xonstat.minimum_real_players.
\r
241 flg_has_min_real_players = True
\r
244 minimum_required_players = int(
\r
245 settings['xonstat.minimum_required_players'])
\r
247 minimum_required_players = 2
\r
249 real_players = num_real_players(player_events)
\r
251 if real_players < minimum_required_players:
\r
252 flg_has_min_real_players = False
\r
254 return flg_has_min_real_players
\r
257 def has_required_metadata(metadata):
\r
259 Determines if a give set of metadata has enough data to create a game,
\r
260 server, and map with.
\r
262 flg_has_req_metadata = True
\r
264 if 'T' not in metadata or\
\r
265 'G' not in metadata or\
\r
266 'M' not in metadata or\
\r
267 'I' not in metadata or\
\r
268 'S' not in metadata:
\r
269 flg_has_req_metadata = False
\r
271 return flg_has_req_metadata
\r
274 def should_do_weapon_stats(game_type_cd):
\r
275 """True of the game type should record weapon stats. False otherwise."""
\r
276 if game_type_cd in 'cts':
\r
282 def should_do_elos(game_type_cd):
\r
283 """True of the game type should process Elos. False otherwise."""
\r
284 elo_game_types = ('duel', 'dm', 'ca', 'ctf', 'tdm', 'ka', 'ft')
\r
286 if game_type_cd in elo_game_types:
\r
292 def register_new_nick(session, player, new_nick):
\r
294 Change the player record's nick to the newly found nick. Store the old
\r
295 nick in the player_nicks table for that player.
\r
297 session - SQLAlchemy database session factory
\r
298 player - player record whose nick is changing
\r
299 new_nick - the new nickname
\r
301 # see if that nick already exists
\r
302 stripped_nick = strip_colors(qfont_decode(player.nick))
\r
304 player_nick = session.query(PlayerNick).filter_by(
\r
305 player_id=player.player_id, stripped_nick=stripped_nick).one()
\r
306 except NoResultFound, e:
\r
307 # player_id/stripped_nick not found, create one
\r
308 # but we don't store "Anonymous Player #N"
\r
309 if not re.search('^Anonymous Player #\d+$', player.nick):
\r
310 player_nick = PlayerNick()
\r
311 player_nick.player_id = player.player_id
\r
312 player_nick.stripped_nick = stripped_nick
\r
313 player_nick.nick = player.nick
\r
314 session.add(player_nick)
\r
316 # We change to the new nick regardless
\r
317 player.nick = new_nick
\r
318 player.stripped_nick = strip_colors(qfont_decode(new_nick))
\r
319 session.add(player)
\r
322 def update_fastest_cap(session, player_id, game_id, map_id, captime):
\r
324 Check the fastest cap time for the player and map. If there isn't
\r
325 one, insert one. If there is, check if the passed time is faster.
\r
328 # we don't record fastest cap times for bots or anonymous players
\r
332 # see if a cap entry exists already
\r
333 # then check to see if the new captime is faster
\r
335 cur_fastest_cap = session.query(PlayerCaptime).filter_by(
\r
336 player_id=player_id, map_id=map_id).one()
\r
338 # current captime is faster, so update
\r
339 if captime < cur_fastest_cap.fastest_cap:
\r
340 cur_fastest_cap.fastest_cap = captime
\r
341 cur_fastest_cap.game_id = game_id
\r
342 cur_fastest_cap.create_dt = datetime.datetime.utcnow()
\r
343 session.add(cur_fastest_cap)
\r
345 except NoResultFound, e:
\r
346 # none exists, so insert
\r
347 cur_fastest_cap = PlayerCaptime(player_id, game_id, map_id, captime)
\r
348 session.add(cur_fastest_cap)
\r
352 def get_or_create_server(session, name, hashkey, ip_addr, revision):
\r
354 Find a server by name or create one if not found. Parameters:
\r
356 session - SQLAlchemy database session factory
\r
357 name - server name of the server to be found or created
\r
358 hashkey - server hashkey
\r
362 # finding by hashkey is preferred, but if not we will fall
\r
363 # back to using name only, which can result in dupes
\r
364 if hashkey is not None:
\r
365 servers = session.query(Server).\
\r
366 filter_by(hashkey=hashkey).\
\r
367 order_by(expr.desc(Server.create_dt)).limit(1).all()
\r
369 if len(servers) > 0:
\r
370 server = servers[0]
\r
371 log.debug("Found existing server {0} by hashkey ({1})".format(
\r
372 server.server_id, server.hashkey))
\r
374 servers = session.query(Server).\
\r
375 filter_by(name=name).\
\r
376 order_by(expr.desc(Server.create_dt)).limit(1).all()
\r
378 if len(servers) > 0:
\r
379 server = servers[0]
\r
380 log.debug("Found existing server {0} by name".format(server.server_id))
\r
382 # still haven't found a server by hashkey or name, so we need to create one
\r
384 server = Server(name=name, hashkey=hashkey)
\r
385 session.add(server)
\r
387 log.debug("Created server {0} with hashkey {1}".format(
\r
388 server.server_id, server.hashkey))
\r
390 # detect changed fields
\r
391 if server.name != name:
\r
393 session.add(server)
\r
395 if server.hashkey != hashkey:
\r
396 server.hashkey = hashkey
\r
397 session.add(server)
\r
399 if server.ip_addr != ip_addr:
\r
400 server.ip_addr = ip_addr
\r
401 session.add(server)
\r
403 if server.revision != revision:
\r
404 server.revision = revision
\r
405 session.add(server)
\r
410 def get_or_create_map(session=None, name=None):
\r
412 Find a map by name or create one if not found. Parameters:
\r
414 session - SQLAlchemy database session factory
\r
415 name - map name of the map to be found or created
\r
418 # find one by the name, if it exists
\r
419 gmap = session.query(Map).filter_by(name=name).one()
\r
420 log.debug("Found map id {0}: {1}".format(gmap.map_id,
\r
422 except NoResultFound, e:
\r
423 gmap = Map(name=name)
\r
426 log.debug("Created map id {0}: {1}".format(gmap.map_id,
\r
428 except MultipleResultsFound, e:
\r
429 # multiple found, so use the first one but warn
\r
431 gmaps = session.query(Map).filter_by(name=name).order_by(
\r
434 log.debug("Found map id {0}: {1} but found \
\r
435 multiple".format(gmap.map_id, gmap.name))
\r
440 def create_game(session=None, start_dt=None, game_type_cd=None,
\r
441 server_id=None, map_id=None, winner=None, match_id=None,
\r
444 Creates a game. Parameters:
\r
446 session - SQLAlchemy database session factory
\r
447 start_dt - when the game started (datetime object)
\r
448 game_type_cd - the game type of the game being played
\r
449 server_id - server identifier of the server hosting the game
\r
450 map_id - map on which the game was played
\r
451 winner - the team id of the team that won
\r
453 seq = Sequence('games_game_id_seq')
\r
454 game_id = session.execute(seq)
\r
455 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
\r
456 server_id=server_id, map_id=map_id, winner=winner)
\r
457 game.match_id = match_id
\r
460 game.duration = datetime.timedelta(seconds=int(round(float(duration))))
\r
465 session.query(Game).filter(Game.server_id==server_id).\
\r
466 filter(Game.match_id==match_id).one()
\r
468 log.debug("Error: game with same server and match_id found! Ignoring.")
\r
470 # if a game under the same server and match_id found,
\r
471 # this is a duplicate game and can be ignored
\r
472 raise pyramid.httpexceptions.HTTPOk('OK')
\r
473 except NoResultFound, e:
\r
474 # server_id/match_id combination not found. game is ok to insert
\r
477 log.debug("Created game id {0} on server {1}, map {2} at \
\r
478 {3}".format(game.game_id,
\r
479 server_id, map_id, start_dt))
\r
484 def get_or_create_player(session=None, hashkey=None, nick=None):
\r
486 Finds a player by hashkey or creates a new one (along with a
\r
487 corresponding hashkey entry. Parameters:
\r
489 session - SQLAlchemy database session factory
\r
490 hashkey - hashkey of the player to be found or created
\r
491 nick - nick of the player (in case of a first time create)
\r
494 if re.search('^bot#\d+$', hashkey) or re.search('^bot#\d+#', hashkey):
\r
495 player = session.query(Player).filter_by(player_id=1).one()
\r
496 # if we have an untracked player
\r
497 elif re.search('^player#\d+$', hashkey):
\r
498 player = session.query(Player).filter_by(player_id=2).one()
\r
499 # else it is a tracked player
\r
501 # see if the player is already in the database
\r
502 # if not, create one and the hashkey along with it
\r
504 hk = session.query(Hashkey).filter_by(
\r
505 hashkey=hashkey).one()
\r
506 player = session.query(Player).filter_by(
\r
507 player_id=hk.player_id).one()
\r
508 log.debug("Found existing player {0} with hashkey {1}".format(
\r
509 player.player_id, hashkey))
\r
512 session.add(player)
\r
515 # if nick is given to us, use it. If not, use "Anonymous Player"
\r
516 # with a suffix added for uniqueness.
\r
518 player.nick = nick[:128]
\r
519 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
\r
521 player.nick = "Anonymous Player #{0}".format(player.player_id)
\r
522 player.stripped_nick = player.nick
\r
524 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
\r
526 log.debug("Created player {0} ({2}) with hashkey {1}".format(
\r
527 player.player_id, hashkey, player.nick.encode('utf-8')))
\r
532 def create_default_game_stat(session, game_type_cd):
\r
533 """Creates a blanked-out pgstat record for the given game type"""
\r
535 # this is what we have to do to get partitioned records in - grab the
\r
536 # sequence value first, then insert using the explicit ID (vs autogenerate)
\r
537 seq = Sequence('player_game_stats_player_game_stat_id_seq')
\r
538 pgstat_id = session.execute(seq)
\r
539 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
\r
540 create_dt=datetime.datetime.utcnow())
\r
542 if game_type_cd == 'as':
\r
543 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
\r
545 if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
\r
546 pgstat.kills = pgstat.deaths = pgstat.suicides = 0
\r
548 if game_type_cd == 'cq':
\r
549 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
\r
552 if game_type_cd == 'ctf':
\r
553 pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
\r
554 pgstat.returns = pgstat.carrier_frags = 0
\r
556 if game_type_cd == 'cts':
\r
559 if game_type_cd == 'dom':
\r
560 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
563 if game_type_cd == 'ft':
\r
564 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
\r
566 if game_type_cd == 'ka':
\r
567 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
568 pgstat.carrier_frags = 0
\r
569 pgstat.time = datetime.timedelta(seconds=0)
\r
571 if game_type_cd == 'kh':
\r
572 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
573 pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
\r
574 pgstat.carrier_frags = 0
\r
576 if game_type_cd == 'lms':
\r
577 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
\r
579 if game_type_cd == 'nb':
\r
580 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
\r
583 if game_type_cd == 'rc':
\r
584 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
\r
589 def create_game_stat(session, game_meta, game, server, gmap, player, events):
\r
590 """Game stats handler for all game types"""
\r
592 pgstat = create_default_game_stat(session, game.game_type_cd)
\r
594 # these fields should be on every pgstat record
\r
595 pgstat.game_id = game.game_id
\r
596 pgstat.player_id = player.player_id
\r
597 pgstat.nick = events.get('n', 'Anonymous Player')[:128]
\r
598 pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
\r
599 pgstat.score = int(round(float(events.get('scoreboard-score', 0))))
\r
600 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
\r
601 pgstat.rank = int(events.get('rank', None))
\r
602 pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
\r
604 if pgstat.nick != player.nick \
\r
605 and player.player_id > 2 \
\r
606 and pgstat.nick != 'Anonymous Player':
\r
607 register_new_nick(session, player, pgstat.nick)
\r
611 # gametype-specific stuff is handled here. if passed to us, we store it
\r
612 for (key,value) in events.items():
\r
613 if key == 'wins': wins = True
\r
614 if key == 't': pgstat.team = int(value)
\r
616 if key == 'scoreboard-drops': pgstat.drops = int(value)
\r
617 if key == 'scoreboard-returns': pgstat.returns = int(value)
\r
618 if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
\r
619 if key == 'scoreboard-pickups': pgstat.pickups = int(value)
\r
620 if key == 'scoreboard-caps': pgstat.captures = int(value)
\r
621 if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
\r
622 if key == 'scoreboard-deaths': pgstat.deaths = int(value)
\r
623 if key == 'scoreboard-kills': pgstat.kills = int(value)
\r
624 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
\r
625 if key == 'scoreboard-objectives': pgstat.collects = int(value)
\r
626 if key == 'scoreboard-captured': pgstat.captures = int(value)
\r
627 if key == 'scoreboard-released': pgstat.drops = int(value)
\r
628 if key == 'scoreboard-fastest':
\r
629 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
\r
630 if key == 'scoreboard-takes': pgstat.pickups = int(value)
\r
631 if key == 'scoreboard-ticks': pgstat.drops = int(value)
\r
632 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
\r
633 if key == 'scoreboard-bctime':
\r
634 pgstat.time = datetime.timedelta(seconds=int(value))
\r
635 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
\r
636 if key == 'scoreboard-losses': pgstat.drops = int(value)
\r
637 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
\r
638 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
\r
639 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
\r
640 if key == 'scoreboard-lives': pgstat.lives = int(value)
\r
641 if key == 'scoreboard-goals': pgstat.captures = int(value)
\r
642 if key == 'scoreboard-faults': pgstat.drops = int(value)
\r
643 if key == 'scoreboard-laps': pgstat.laps = int(value)
\r
645 if key == 'avglatency': pgstat.avg_latency = float(value)
\r
646 if key == 'scoreboard-captime':
\r
647 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
\r
648 if game.game_type_cd == 'ctf':
\r
649 update_fastest_cap(session, player.player_id, game.game_id,
\r
650 gmap.map_id, pgstat.fastest)
\r
652 # there is no "winning team" field, so we have to derive it
\r
653 if wins and pgstat.team is not None and game.winner is None:
\r
654 game.winner = pgstat.team
\r
657 session.add(pgstat)
\r
662 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
\r
663 """Weapon stats handler for all game types"""
\r
666 # Version 1 of stats submissions doubled the data sent.
\r
667 # To counteract this we divide the data by 2 only for
\r
668 # POSTs coming from version 1.
\r
670 version = int(game_meta['V'])
\r
673 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
\r
679 for (key,value) in events.items():
\r
680 matched = re.search("acc-(.*?)-cnt-fired", key)
\r
682 weapon_cd = matched.group(1)
\r
683 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
\r
684 pwstat_id = session.execute(seq)
\r
685 pwstat = PlayerWeaponStat()
\r
686 pwstat.player_weapon_stats_id = pwstat_id
\r
687 pwstat.player_id = player.player_id
\r
688 pwstat.game_id = game.game_id
\r
689 pwstat.player_game_stat_id = pgstat.player_game_stat_id
\r
690 pwstat.weapon_cd = weapon_cd
\r
693 pwstat.nick = events['n']
\r
695 pwstat.nick = events['P']
\r
697 if 'acc-' + weapon_cd + '-cnt-fired' in events:
\r
698 pwstat.fired = int(round(float(
\r
699 events['acc-' + weapon_cd + '-cnt-fired'])))
\r
700 if 'acc-' + weapon_cd + '-fired' in events:
\r
701 pwstat.max = int(round(float(
\r
702 events['acc-' + weapon_cd + '-fired'])))
\r
703 if 'acc-' + weapon_cd + '-cnt-hit' in events:
\r
704 pwstat.hit = int(round(float(
\r
705 events['acc-' + weapon_cd + '-cnt-hit'])))
\r
706 if 'acc-' + weapon_cd + '-hit' in events:
\r
707 pwstat.actual = int(round(float(
\r
708 events['acc-' + weapon_cd + '-hit'])))
\r
709 if 'acc-' + weapon_cd + '-frags' in events:
\r
710 pwstat.frags = int(round(float(
\r
711 events['acc-' + weapon_cd + '-frags'])))
\r
714 pwstat.fired = pwstat.fired/2
\r
715 pwstat.max = pwstat.max/2
\r
716 pwstat.hit = pwstat.hit/2
\r
717 pwstat.actual = pwstat.actual/2
\r
718 pwstat.frags = pwstat.frags/2
\r
720 session.add(pwstat)
\r
721 pwstats.append(pwstat)
\r
726 def create_elos(session, game):
\r
727 """Elo handler for all game types."""
\r
729 process_elos(game, session)
\r
730 except Exception as e:
\r
731 log.debug('Error (non-fatal): elo processing failed.')
\r
734 def submit_stats(request):
\r
736 Entry handler for POST stats submissions.
\r
739 # placeholder for the actual session
\r
742 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
\r
743 "----- END REQUEST BODY -----\n\n")
\r
745 (idfp, status) = verify_request(request)
\r
746 (game_meta, raw_players) = parse_stats_submission(request.body)
\r
747 revision = game_meta.get('R', 'unknown')
\r
748 duration = game_meta.get('D', None)
\r
750 # only players present at the end of the match are eligible for stats
\r
751 raw_players = filter(played_in_game, raw_players)
\r
753 do_precondition_checks(request, game_meta, raw_players)
\r
755 # the "duel" gametype is fake
\r
756 if len(raw_players) == 2 \
\r
757 and num_real_players(raw_players) == 2 \
\r
758 and game_meta['G'] == 'dm':
\r
759 game_meta['G'] = 'duel'
\r
761 #----------------------------------------------------------------------
\r
762 # Actual setup (inserts/updates) below here
\r
763 #----------------------------------------------------------------------
\r
764 session = DBSession()
\r
766 game_type_cd = game_meta['G']
\r
768 # All game types create Game, Server, Map, and Player records
\r
770 server = get_or_create_server(
\r
773 name = game_meta['S'],
\r
774 revision = revision,
\r
775 ip_addr = get_remote_addr(request))
\r
777 gmap = get_or_create_map(
\r
779 name = game_meta['M'])
\r
781 game = create_game(
\r
783 start_dt = datetime.datetime.utcnow(),
\r
784 server_id = server.server_id,
\r
785 game_type_cd = game_type_cd,
\r
786 map_id = gmap.map_id,
\r
787 match_id = game_meta['I'],
\r
788 duration = duration)
\r
790 for events in raw_players:
\r
791 player = get_or_create_player(
\r
793 hashkey = events['P'],
\r
794 nick = events.get('n', None))
\r
796 pgstat = create_game_stat(session, game_meta, game, server,
\r
797 gmap, player, events)
\r
799 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
\r
800 pwstats = create_weapon_stats(session, game_meta, game, player,
\r
803 if should_do_elos(game_type_cd):
\r
804 create_elos(session, game)
\r
807 log.debug('Success! Stats recorded.')
\r
808 return Response('200 OK')
\r
809 except Exception as e:
\r