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, port):
\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
367 # finding by hashkey is preferred, but if not we will fall
\r
368 # back to using name only, which can result in dupes
\r
369 if hashkey is not None:
\r
370 servers = session.query(Server).\
\r
371 filter_by(hashkey=hashkey).\
\r
372 order_by(expr.desc(Server.create_dt)).limit(1).all()
\r
374 if len(servers) > 0:
\r
375 server = servers[0]
\r
376 log.debug("Found existing server {0} by hashkey ({1})".format(
\r
377 server.server_id, server.hashkey))
\r
379 servers = session.query(Server).\
\r
380 filter_by(name=name).\
\r
381 order_by(expr.desc(Server.create_dt)).limit(1).all()
\r
383 if len(servers) > 0:
\r
384 server = servers[0]
\r
385 log.debug("Found existing server {0} by name".format(server.server_id))
\r
387 # still haven't found a server by hashkey or name, so we need to create one
\r
389 server = Server(name=name, hashkey=hashkey)
\r
390 session.add(server)
\r
392 log.debug("Created server {0} with hashkey {1}".format(
\r
393 server.server_id, server.hashkey))
\r
395 # detect changed fields
\r
396 if server.name != name:
\r
398 session.add(server)
\r
400 if server.hashkey != hashkey:
\r
401 server.hashkey = hashkey
\r
402 session.add(server)
\r
404 if server.ip_addr != ip_addr:
\r
405 server.ip_addr = ip_addr
\r
406 session.add(server)
\r
408 if server.port != port:
\r
410 session.add(server)
\r
412 if server.revision != revision:
\r
413 server.revision = revision
\r
414 session.add(server)
\r
419 def get_or_create_map(session=None, name=None):
\r
421 Find a map by name or create one if not found. Parameters:
\r
423 session - SQLAlchemy database session factory
\r
424 name - map name of the map to be found or created
\r
427 # find one by the name, if it exists
\r
428 gmap = session.query(Map).filter_by(name=name).one()
\r
429 log.debug("Found map id {0}: {1}".format(gmap.map_id,
\r
431 except NoResultFound, e:
\r
432 gmap = Map(name=name)
\r
435 log.debug("Created map id {0}: {1}".format(gmap.map_id,
\r
437 except MultipleResultsFound, e:
\r
438 # multiple found, so use the first one but warn
\r
440 gmaps = session.query(Map).filter_by(name=name).order_by(
\r
443 log.debug("Found map id {0}: {1} but found \
\r
444 multiple".format(gmap.map_id, gmap.name))
\r
449 def create_game(session=None, start_dt=None, game_type_cd=None,
\r
450 server_id=None, map_id=None, winner=None, match_id=None,
\r
453 Creates a game. Parameters:
\r
455 session - SQLAlchemy database session factory
\r
456 start_dt - when the game started (datetime object)
\r
457 game_type_cd - the game type of the game being played
\r
458 server_id - server identifier of the server hosting the game
\r
459 map_id - map on which the game was played
\r
460 winner - the team id of the team that won
\r
462 seq = Sequence('games_game_id_seq')
\r
463 game_id = session.execute(seq)
\r
464 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
\r
465 server_id=server_id, map_id=map_id, winner=winner)
\r
466 game.match_id = match_id
\r
469 game.duration = datetime.timedelta(seconds=int(round(float(duration))))
\r
474 session.query(Game).filter(Game.server_id==server_id).\
\r
475 filter(Game.match_id==match_id).one()
\r
477 log.debug("Error: game with same server and match_id found! Ignoring.")
\r
479 # if a game under the same server and match_id found,
\r
480 # this is a duplicate game and can be ignored
\r
481 raise pyramid.httpexceptions.HTTPOk('OK')
\r
482 except NoResultFound, e:
\r
483 # server_id/match_id combination not found. game is ok to insert
\r
486 log.debug("Created game id {0} on server {1}, map {2} at \
\r
487 {3}".format(game.game_id,
\r
488 server_id, map_id, start_dt))
\r
493 def get_or_create_player(session=None, hashkey=None, nick=None):
\r
495 Finds a player by hashkey or creates a new one (along with a
\r
496 corresponding hashkey entry. Parameters:
\r
498 session - SQLAlchemy database session factory
\r
499 hashkey - hashkey of the player to be found or created
\r
500 nick - nick of the player (in case of a first time create)
\r
503 if re.search('^bot#\d+$', hashkey) or re.search('^bot#\d+#', hashkey):
\r
504 player = session.query(Player).filter_by(player_id=1).one()
\r
505 # if we have an untracked player
\r
506 elif re.search('^player#\d+$', hashkey):
\r
507 player = session.query(Player).filter_by(player_id=2).one()
\r
508 # else it is a tracked player
\r
510 # see if the player is already in the database
\r
511 # if not, create one and the hashkey along with it
\r
513 hk = session.query(Hashkey).filter_by(
\r
514 hashkey=hashkey).one()
\r
515 player = session.query(Player).filter_by(
\r
516 player_id=hk.player_id).one()
\r
517 log.debug("Found existing player {0} with hashkey {1}".format(
\r
518 player.player_id, hashkey))
\r
521 session.add(player)
\r
524 # if nick is given to us, use it. If not, use "Anonymous Player"
\r
525 # with a suffix added for uniqueness.
\r
527 player.nick = nick[:128]
\r
528 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
\r
530 player.nick = "Anonymous Player #{0}".format(player.player_id)
\r
531 player.stripped_nick = player.nick
\r
533 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
\r
535 log.debug("Created player {0} ({2}) with hashkey {1}".format(
\r
536 player.player_id, hashkey, player.nick.encode('utf-8')))
\r
541 def create_default_game_stat(session, game_type_cd):
\r
542 """Creates a blanked-out pgstat record for the given game type"""
\r
544 # this is what we have to do to get partitioned records in - grab the
\r
545 # sequence value first, then insert using the explicit ID (vs autogenerate)
\r
546 seq = Sequence('player_game_stats_player_game_stat_id_seq')
\r
547 pgstat_id = session.execute(seq)
\r
548 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
\r
549 create_dt=datetime.datetime.utcnow())
\r
551 if game_type_cd == 'as':
\r
552 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
\r
554 if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
\r
555 pgstat.kills = pgstat.deaths = pgstat.suicides = 0
\r
557 if game_type_cd == 'cq':
\r
558 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
\r
561 if game_type_cd == 'ctf':
\r
562 pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
\r
563 pgstat.returns = pgstat.carrier_frags = 0
\r
565 if game_type_cd == 'cts':
\r
568 if game_type_cd == 'dom':
\r
569 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
572 if game_type_cd == 'ft':
\r
573 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
\r
575 if game_type_cd == 'ka':
\r
576 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
577 pgstat.carrier_frags = 0
\r
578 pgstat.time = datetime.timedelta(seconds=0)
\r
580 if game_type_cd == 'kh':
\r
581 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
582 pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
\r
583 pgstat.carrier_frags = 0
\r
585 if game_type_cd == 'lms':
\r
586 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
\r
588 if game_type_cd == 'nb':
\r
589 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
\r
592 if game_type_cd == 'rc':
\r
593 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
\r
598 def create_game_stat(session, game_meta, game, server, gmap, player, events):
\r
599 """Game stats handler for all game types"""
\r
601 pgstat = create_default_game_stat(session, game.game_type_cd)
\r
603 # these fields should be on every pgstat record
\r
604 pgstat.game_id = game.game_id
\r
605 pgstat.player_id = player.player_id
\r
606 pgstat.nick = events.get('n', 'Anonymous Player')[:128]
\r
607 pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
\r
608 pgstat.score = int(round(float(events.get('scoreboard-score', 0))))
\r
609 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
\r
610 pgstat.rank = int(events.get('rank', None))
\r
611 pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
\r
613 if pgstat.nick != player.nick \
\r
614 and player.player_id > 2 \
\r
615 and pgstat.nick != 'Anonymous Player':
\r
616 register_new_nick(session, player, pgstat.nick)
\r
620 # gametype-specific stuff is handled here. if passed to us, we store it
\r
621 for (key,value) in events.items():
\r
622 if key == 'wins': wins = True
\r
623 if key == 't': pgstat.team = int(value)
\r
625 if key == 'scoreboard-drops': pgstat.drops = int(value)
\r
626 if key == 'scoreboard-returns': pgstat.returns = int(value)
\r
627 if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
\r
628 if key == 'scoreboard-pickups': pgstat.pickups = int(value)
\r
629 if key == 'scoreboard-caps': pgstat.captures = int(value)
\r
630 if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
\r
631 if key == 'scoreboard-deaths': pgstat.deaths = int(value)
\r
632 if key == 'scoreboard-kills': pgstat.kills = int(value)
\r
633 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
\r
634 if key == 'scoreboard-objectives': pgstat.collects = int(value)
\r
635 if key == 'scoreboard-captured': pgstat.captures = int(value)
\r
636 if key == 'scoreboard-released': pgstat.drops = int(value)
\r
637 if key == 'scoreboard-fastest':
\r
638 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
\r
639 if key == 'scoreboard-takes': pgstat.pickups = int(value)
\r
640 if key == 'scoreboard-ticks': pgstat.drops = int(value)
\r
641 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
\r
642 if key == 'scoreboard-bctime':
\r
643 pgstat.time = datetime.timedelta(seconds=int(value))
\r
644 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
\r
645 if key == 'scoreboard-losses': pgstat.drops = int(value)
\r
646 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
\r
647 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
\r
648 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
\r
649 if key == 'scoreboard-lives': pgstat.lives = int(value)
\r
650 if key == 'scoreboard-goals': pgstat.captures = int(value)
\r
651 if key == 'scoreboard-faults': pgstat.drops = int(value)
\r
652 if key == 'scoreboard-laps': pgstat.laps = int(value)
\r
654 if key == 'avglatency': pgstat.avg_latency = float(value)
\r
655 if key == 'scoreboard-captime':
\r
656 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
\r
657 if game.game_type_cd == 'ctf':
\r
658 update_fastest_cap(session, player.player_id, game.game_id,
\r
659 gmap.map_id, pgstat.fastest)
\r
661 # there is no "winning team" field, so we have to derive it
\r
662 if wins and pgstat.team is not None and game.winner is None:
\r
663 game.winner = pgstat.team
\r
666 session.add(pgstat)
\r
671 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
\r
672 """Weapon stats handler for all game types"""
\r
675 # Version 1 of stats submissions doubled the data sent.
\r
676 # To counteract this we divide the data by 2 only for
\r
677 # POSTs coming from version 1.
\r
679 version = int(game_meta['V'])
\r
682 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
\r
688 for (key,value) in events.items():
\r
689 matched = re.search("acc-(.*?)-cnt-fired", key)
\r
691 weapon_cd = matched.group(1)
\r
692 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
\r
693 pwstat_id = session.execute(seq)
\r
694 pwstat = PlayerWeaponStat()
\r
695 pwstat.player_weapon_stats_id = pwstat_id
\r
696 pwstat.player_id = player.player_id
\r
697 pwstat.game_id = game.game_id
\r
698 pwstat.player_game_stat_id = pgstat.player_game_stat_id
\r
699 pwstat.weapon_cd = weapon_cd
\r
702 pwstat.nick = events['n']
\r
704 pwstat.nick = events['P']
\r
706 if 'acc-' + weapon_cd + '-cnt-fired' in events:
\r
707 pwstat.fired = int(round(float(
\r
708 events['acc-' + weapon_cd + '-cnt-fired'])))
\r
709 if 'acc-' + weapon_cd + '-fired' in events:
\r
710 pwstat.max = int(round(float(
\r
711 events['acc-' + weapon_cd + '-fired'])))
\r
712 if 'acc-' + weapon_cd + '-cnt-hit' in events:
\r
713 pwstat.hit = int(round(float(
\r
714 events['acc-' + weapon_cd + '-cnt-hit'])))
\r
715 if 'acc-' + weapon_cd + '-hit' in events:
\r
716 pwstat.actual = int(round(float(
\r
717 events['acc-' + weapon_cd + '-hit'])))
\r
718 if 'acc-' + weapon_cd + '-frags' in events:
\r
719 pwstat.frags = int(round(float(
\r
720 events['acc-' + weapon_cd + '-frags'])))
\r
723 pwstat.fired = pwstat.fired/2
\r
724 pwstat.max = pwstat.max/2
\r
725 pwstat.hit = pwstat.hit/2
\r
726 pwstat.actual = pwstat.actual/2
\r
727 pwstat.frags = pwstat.frags/2
\r
729 session.add(pwstat)
\r
730 pwstats.append(pwstat)
\r
735 def create_elos(session, game):
\r
736 """Elo handler for all game types."""
\r
738 process_elos(game, session)
\r
739 except Exception as e:
\r
740 log.debug('Error (non-fatal): elo processing failed.')
\r
743 def submit_stats(request):
\r
745 Entry handler for POST stats submissions.
\r
748 # placeholder for the actual session
\r
751 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
\r
752 "----- END REQUEST BODY -----\n\n")
\r
754 (idfp, status) = verify_request(request)
\r
755 (game_meta, raw_players) = parse_stats_submission(request.body)
\r
756 revision = game_meta.get('R', 'unknown')
\r
757 duration = game_meta.get('D', None)
\r
759 # only players present at the end of the match are eligible for stats
\r
760 raw_players = filter(played_in_game, raw_players)
\r
762 do_precondition_checks(request, game_meta, raw_players)
\r
764 # the "duel" gametype is fake
\r
765 if len(raw_players) == 2 \
\r
766 and num_real_players(raw_players) == 2 \
\r
767 and game_meta['G'] == 'dm':
\r
768 game_meta['G'] = 'duel'
\r
770 #----------------------------------------------------------------------
\r
771 # Actual setup (inserts/updates) below here
\r
772 #----------------------------------------------------------------------
\r
773 session = DBSession()
\r
775 game_type_cd = game_meta['G']
\r
777 # All game types create Game, Server, Map, and Player records
\r
779 server = get_or_create_server(
\r
782 name = game_meta['S'],
\r
783 revision = revision,
\r
784 ip_addr = get_remote_addr(request),
\r
785 port = game_meta.get('U', None))
\r
787 gmap = get_or_create_map(
\r
789 name = game_meta['M'])
\r
791 game = create_game(
\r
793 start_dt = datetime.datetime.utcnow(),
\r
794 server_id = server.server_id,
\r
795 game_type_cd = game_type_cd,
\r
796 map_id = gmap.map_id,
\r
797 match_id = game_meta['I'],
\r
798 duration = duration)
\r
800 for events in raw_players:
\r
801 player = get_or_create_player(
\r
803 hashkey = events['P'],
\r
804 nick = events.get('n', None))
\r
806 pgstat = create_game_stat(session, game_meta, game, server,
\r
807 gmap, player, events)
\r
809 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
\r
810 pwstats = create_weapon_stats(session, game_meta, game, player,
\r
813 if should_do_elos(game_type_cd):
\r
814 create_elos(session, game)
\r
817 log.debug('Success! Stats recorded.')
\r
818 return Response('200 OK')
\r
819 except Exception as e:
\r