4 import pyramid.httpexceptions
\r
7 from pyramid.response import Response
\r
8 from sqlalchemy import Sequence
\r
9 from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
\r
10 from xonstat.d0_blind_id import d0_blind_id_verify
\r
11 from xonstat.elo import process_elos
\r
12 from xonstat.models import *
\r
13 from xonstat.util import strip_colors, qfont_decode
\r
16 log = logging.getLogger(__name__)
\r
19 def parse_stats_submission(body):
\r
21 Parses the POST request body for a stats submission
\r
23 # storage vars for the request body
\r
28 for line in body.split('\n'):
\r
30 (key, value) = line.strip().split(' ', 1)
\r
32 # Server (S) and Nick (n) fields can have international characters.
\r
34 value = unicode(value, 'utf-8')
\r
36 if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W' 'I' 'D' 'O':
\r
37 game_meta[key] = value
\r
40 # if we were working on a player record already, append
\r
41 # it and work on a new one (only set team info)
\r
43 players.append(events)
\r
49 (subkey, subvalue) = value.split(' ', 1)
\r
50 events[subkey] = subvalue
\r
56 # no key/value pair - move on to the next line
\r
59 # add the last player we were working on
\r
61 players.append(events)
\r
63 return (game_meta, players)
\r
66 def is_blank_game(gametype, players):
\r
67 """Determine if this is a blank game or not. A blank game is either:
\r
69 1) a match that ended in the warmup stage, where accuracy events are not
\r
70 present (for non-CTS games)
\r
72 2) a match in which no player made a positive or negative score AND was
\r
75 ... or for CTS, which doesn't record accuracy events
\r
77 1) a match in which no player made a fastest lap AND was
\r
80 r = re.compile(r'acc-.*-cnt-fired')
\r
81 flg_nonzero_score = False
\r
82 flg_acc_events = False
\r
83 flg_fastest_lap = False
\r
85 for events in players:
\r
86 if is_real_player(events) and played_in_game(events):
\r
87 for (key,value) in events.items():
\r
88 if key == 'scoreboard-score' and value != 0:
\r
89 flg_nonzero_score = True
\r
91 flg_acc_events = True
\r
92 if key == 'scoreboard-fastest':
\r
93 flg_fastest_lap = True
\r
95 if gametype == 'cts':
\r
96 return not flg_fastest_lap
\r
98 return not (flg_nonzero_score and flg_acc_events)
\r
101 def get_remote_addr(request):
\r
102 """Get the Xonotic server's IP address"""
\r
103 if 'X-Forwarded-For' in request.headers:
\r
104 return request.headers['X-Forwarded-For']
\r
106 return request.remote_addr
\r
109 def is_supported_gametype(gametype, version):
\r
110 """Whether a gametype is supported or not"""
\r
111 is_supported = False
\r
113 # if the type can be supported, but with version constraints, uncomment
\r
114 # here and add the restriction for a specific version below
\r
115 supported_game_types = (
\r
133 if gametype in supported_game_types:
\r
134 is_supported = True
\r
136 is_supported = False
\r
138 # some game types were buggy before revisions, thus this additional filter
\r
139 if gametype == 'ca' and version <= 5:
\r
140 is_supported = False
\r
142 return is_supported
\r
145 def verify_request(request):
\r
146 """Verify requests using the d0_blind_id library"""
\r
148 # first determine if we should be verifying or not
\r
149 val_verify_requests = request.registry.settings.get('xonstat.verify_requests', 'true')
\r
150 if val_verify_requests == "true":
\r
151 flg_verify_requests = True
\r
153 flg_verify_requests = False
\r
156 (idfp, status) = d0_blind_id_verify(
\r
157 sig=request.headers['X-D0-Blind-Id-Detached-Signature'],
\r
159 postdata=request.body)
\r
161 log.debug('\nidfp: {0}\nstatus: {1}'.format(idfp, status))
\r
166 if flg_verify_requests and not idfp:
\r
167 log.debug("ERROR: Unverified request")
\r
168 raise pyramid.httpexceptions.HTTPUnauthorized("Unverified request")
\r
170 return (idfp, status)
\r
173 def do_precondition_checks(request, game_meta, raw_players):
\r
174 """Precondition checks for ALL gametypes.
\r
175 These do not require a database connection."""
\r
176 if not has_required_metadata(game_meta):
\r
177 log.debug("ERROR: Required game meta missing")
\r
178 raise pyramid.httpexceptions.HTTPUnprocessableEntity("Missing game meta")
\r
181 version = int(game_meta['V'])
\r
183 log.debug("ERROR: Required game meta invalid")
\r
184 raise pyramid.httpexceptions.HTTPUnprocessableEntity("Invalid game meta")
\r
186 if not is_supported_gametype(game_meta['G'], version):
\r
187 log.debug("ERROR: Unsupported gametype")
\r
188 raise pyramid.httpexceptions.HTTPOk("OK")
\r
190 if not has_minimum_real_players(request.registry.settings, raw_players):
\r
191 log.debug("ERROR: Not enough real players")
\r
192 raise pyramid.httpexceptions.HTTPOk("OK")
\r
194 if is_blank_game(game_meta['G'], raw_players):
\r
195 log.debug("ERROR: Blank game")
\r
196 raise pyramid.httpexceptions.HTTPOk("OK")
\r
199 def is_real_player(events):
\r
201 Determines if a given set of events correspond with a non-bot
\r
203 if not events['P'].startswith('bot'):
\r
209 def played_in_game(events):
\r
211 Determines if a given set of player events correspond with a player who
\r
212 played in the game (matches 1 and scoreboardvalid 1)
\r
214 if 'matches' in events and 'scoreboardvalid' in events:
\r
220 def num_real_players(player_events):
\r
222 Returns the number of real players (those who played
\r
223 and are on the scoreboard).
\r
227 for events in player_events:
\r
228 if is_real_player(events) and played_in_game(events):
\r
231 return real_players
\r
234 def has_minimum_real_players(settings, player_events):
\r
236 Determines if the collection of player events has enough "real" players
\r
237 to store in the database. The minimum setting comes from the config file
\r
238 under the setting xonstat.minimum_real_players.
\r
240 flg_has_min_real_players = True
\r
243 minimum_required_players = int(
\r
244 settings['xonstat.minimum_required_players'])
\r
246 minimum_required_players = 2
\r
248 real_players = num_real_players(player_events)
\r
250 if real_players < minimum_required_players:
\r
251 flg_has_min_real_players = False
\r
253 return flg_has_min_real_players
\r
256 def has_required_metadata(metadata):
\r
258 Determines if a give set of metadata has enough data to create a game,
\r
259 server, and map with.
\r
261 flg_has_req_metadata = True
\r
263 if 'T' not in metadata or\
\r
264 'G' not in metadata or\
\r
265 'M' not in metadata or\
\r
266 'I' not in metadata or\
\r
267 'S' not in metadata:
\r
268 flg_has_req_metadata = False
\r
270 return flg_has_req_metadata
\r
273 def should_do_weapon_stats(game_type_cd):
\r
274 """True of the game type should record weapon stats. False otherwise."""
\r
275 if game_type_cd in 'cts':
\r
281 def should_do_elos(game_type_cd):
\r
282 """True of the game type should process Elos. False otherwise."""
\r
283 elo_game_types = ('duel', 'dm', 'ca', 'ctf', 'tdm', 'kh',
\r
284 'ka', 'ft', 'freezetag')
\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=None, name=None, hashkey=None, ip_addr=None,
\r
355 Find a server by name or create one if not found. Parameters:
\r
357 session - SQLAlchemy database session factory
\r
358 name - server name of the server to be found or created
\r
359 hashkey - server hashkey
\r
362 # find one by that name, if it exists
\r
363 server = session.query(Server).filter_by(name=name).one()
\r
365 # store new hashkey
\r
366 if server.hashkey != hashkey:
\r
367 server.hashkey = hashkey
\r
368 session.add(server)
\r
370 # store new IP address
\r
371 if server.ip_addr != ip_addr:
\r
372 server.ip_addr = ip_addr
\r
373 session.add(server)
\r
375 # store new revision
\r
376 if server.revision != revision:
\r
377 server.revision = revision
\r
378 session.add(server)
\r
380 log.debug("Found existing server {0}".format(server.server_id))
\r
382 except MultipleResultsFound, e:
\r
383 # multiple found, so also filter by hashkey
\r
384 server = session.query(Server).filter_by(name=name).\
\r
385 filter_by(hashkey=hashkey).one()
\r
386 log.debug("Found existing server {0}".format(server.server_id))
\r
388 except NoResultFound, e:
\r
389 # not found, create one
\r
390 server = Server(name=name, hashkey=hashkey)
\r
391 session.add(server)
\r
393 log.debug("Created server {0} with hashkey {1}".format(
\r
394 server.server_id, server.hashkey))
\r
399 def get_or_create_map(session=None, name=None):
\r
401 Find a map by name or create one if not found. Parameters:
\r
403 session - SQLAlchemy database session factory
\r
404 name - map name of the map to be found or created
\r
407 # find one by the name, if it exists
\r
408 gmap = session.query(Map).filter_by(name=name).one()
\r
409 log.debug("Found map id {0}: {1}".format(gmap.map_id,
\r
411 except NoResultFound, e:
\r
412 gmap = Map(name=name)
\r
415 log.debug("Created map id {0}: {1}".format(gmap.map_id,
\r
417 except MultipleResultsFound, e:
\r
418 # multiple found, so use the first one but warn
\r
420 gmaps = session.query(Map).filter_by(name=name).order_by(
\r
423 log.debug("Found map id {0}: {1} but found \
\r
424 multiple".format(gmap.map_id, gmap.name))
\r
429 def create_game(session=None, start_dt=None, game_type_cd=None,
\r
430 server_id=None, map_id=None, winner=None, match_id=None,
\r
433 Creates a game. Parameters:
\r
435 session - SQLAlchemy database session factory
\r
436 start_dt - when the game started (datetime object)
\r
437 game_type_cd - the game type of the game being played
\r
438 server_id - server identifier of the server hosting the game
\r
439 map_id - map on which the game was played
\r
440 winner - the team id of the team that won
\r
442 seq = Sequence('games_game_id_seq')
\r
443 game_id = session.execute(seq)
\r
444 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
\r
445 server_id=server_id, map_id=map_id, winner=winner)
\r
446 game.match_id = match_id
\r
449 game.duration = datetime.timedelta(seconds=int(round(float(duration))))
\r
454 session.query(Game).filter(Game.server_id==server_id).\
\r
455 filter(Game.match_id==match_id).one()
\r
457 log.debug("Error: game with same server and match_id found! Ignoring.")
\r
459 # if a game under the same server and match_id found,
\r
460 # this is a duplicate game and can be ignored
\r
461 raise pyramid.httpexceptions.HTTPOk('OK')
\r
462 except NoResultFound, e:
\r
463 # server_id/match_id combination not found. game is ok to insert
\r
466 log.debug("Created game id {0} on server {1}, map {2} at \
\r
467 {3}".format(game.game_id,
\r
468 server_id, map_id, start_dt))
\r
473 def get_or_create_player(session=None, hashkey=None, nick=None):
\r
475 Finds a player by hashkey or creates a new one (along with a
\r
476 corresponding hashkey entry. Parameters:
\r
478 session - SQLAlchemy database session factory
\r
479 hashkey - hashkey of the player to be found or created
\r
480 nick - nick of the player (in case of a first time create)
\r
483 if re.search('^bot#\d+$', hashkey) or re.search('^bot#\d+#', hashkey):
\r
484 player = session.query(Player).filter_by(player_id=1).one()
\r
485 # if we have an untracked player
\r
486 elif re.search('^player#\d+$', hashkey):
\r
487 player = session.query(Player).filter_by(player_id=2).one()
\r
488 # else it is a tracked player
\r
490 # see if the player is already in the database
\r
491 # if not, create one and the hashkey along with it
\r
493 hk = session.query(Hashkey).filter_by(
\r
494 hashkey=hashkey).one()
\r
495 player = session.query(Player).filter_by(
\r
496 player_id=hk.player_id).one()
\r
497 log.debug("Found existing player {0} with hashkey {1}".format(
\r
498 player.player_id, hashkey))
\r
501 session.add(player)
\r
504 # if nick is given to us, use it. If not, use "Anonymous Player"
\r
505 # with a suffix added for uniqueness.
\r
507 player.nick = nick[:128]
\r
508 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
\r
510 player.nick = "Anonymous Player #{0}".format(player.player_id)
\r
511 player.stripped_nick = player.nick
\r
513 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
\r
515 log.debug("Created player {0} ({2}) with hashkey {1}".format(
\r
516 player.player_id, hashkey, player.nick.encode('utf-8')))
\r
521 def create_default_game_stat(session, game_type_cd):
\r
522 """Creates a blanked-out pgstat record for the given game type"""
\r
524 # this is what we have to do to get partitioned records in - grab the
\r
525 # sequence value first, then insert using the explicit ID (vs autogenerate)
\r
526 seq = Sequence('player_game_stats_player_game_stat_id_seq')
\r
527 pgstat_id = session.execute(seq)
\r
528 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
\r
529 create_dt=datetime.datetime.utcnow())
\r
531 if game_type_cd == 'as':
\r
532 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
\r
534 if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
\r
535 pgstat.kills = pgstat.deaths = pgstat.suicides = 0
\r
537 if game_type_cd == 'cq':
\r
538 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
\r
541 if game_type_cd == 'ctf':
\r
542 pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
\r
543 pgstat.returns = pgstat.carrier_frags = 0
\r
545 if game_type_cd == 'cts':
\r
548 if game_type_cd == 'dom':
\r
549 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
552 if game_type_cd == 'ft':
\r
553 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
\r
555 if game_type_cd == 'ka':
\r
556 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
557 pgstat.carrier_frags = 0
\r
558 pgstat.time = datetime.timedelta(seconds=0)
\r
560 if game_type_cd == 'kh':
\r
561 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
562 pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
\r
563 pgstat.carrier_frags = 0
\r
565 if game_type_cd == 'lms':
\r
566 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
\r
568 if game_type_cd == 'nb':
\r
569 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
\r
572 if game_type_cd == 'rc':
\r
573 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
\r
578 def create_game_stat(session, game_meta, game, server, gmap, player, events):
\r
579 """Game stats handler for all game types"""
\r
581 pgstat = create_default_game_stat(session, game.game_type_cd)
\r
583 # these fields should be on every pgstat record
\r
584 pgstat.game_id = game.game_id
\r
585 pgstat.player_id = player.player_id
\r
586 pgstat.nick = events.get('n', 'Anonymous Player')[:128]
\r
587 pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
\r
588 pgstat.score = int(round(float(events.get('scoreboard-score', 0))))
\r
589 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
\r
590 pgstat.rank = int(events.get('rank', None))
\r
591 pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
\r
593 if pgstat.nick != player.nick \
\r
594 and player.player_id > 2 \
\r
595 and pgstat.nick != 'Anonymous Player':
\r
596 register_new_nick(session, player, pgstat.nick)
\r
600 # gametype-specific stuff is handled here. if passed to us, we store it
\r
601 for (key,value) in events.items():
\r
602 if key == 'wins': wins = True
\r
603 if key == 't': pgstat.team = int(value)
\r
605 if key == 'scoreboard-drops': pgstat.drops = int(value)
\r
606 if key == 'scoreboard-returns': pgstat.returns = int(value)
\r
607 if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
\r
608 if key == 'scoreboard-pickups': pgstat.pickups = int(value)
\r
609 if key == 'scoreboard-caps': pgstat.captures = int(value)
\r
610 if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
\r
611 if key == 'scoreboard-deaths': pgstat.deaths = int(value)
\r
612 if key == 'scoreboard-kills': pgstat.kills = int(value)
\r
613 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
\r
614 if key == 'scoreboard-objectives': pgstat.collects = int(value)
\r
615 if key == 'scoreboard-captured': pgstat.captures = int(value)
\r
616 if key == 'scoreboard-released': pgstat.drops = int(value)
\r
617 if key == 'scoreboard-fastest':
\r
618 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
\r
619 if key == 'scoreboard-takes': pgstat.pickups = int(value)
\r
620 if key == 'scoreboard-ticks': pgstat.drops = int(value)
\r
621 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
\r
622 if key == 'scoreboard-bctime':
\r
623 pgstat.time = datetime.timedelta(seconds=int(value))
\r
624 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
\r
625 if key == 'scoreboard-losses': pgstat.drops = int(value)
\r
626 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
\r
627 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
\r
628 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
\r
629 if key == 'scoreboard-lives': pgstat.lives = int(value)
\r
630 if key == 'scoreboard-goals': pgstat.captures = int(value)
\r
631 if key == 'scoreboard-faults': pgstat.drops = int(value)
\r
632 if key == 'scoreboard-laps': pgstat.laps = int(value)
\r
634 if key == 'avglatency': pgstat.avg_latency = float(value)
\r
635 if key == 'scoreboard-captime':
\r
636 pgstat.fastest_cap = datetime.timedelta(seconds=float(value)/100)
\r
637 if game.game_type_cd == 'ctf':
\r
638 update_fastest_cap(session, player.player_id, game.game_id,
\r
639 gmap.map_id, pgstat.fastest_cap)
\r
641 # there is no "winning team" field, so we have to derive it
\r
642 if wins and pgstat.team is not None and game.winner is None:
\r
643 game.winner = pgstat.team
\r
646 session.add(pgstat)
\r
651 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
\r
652 """Weapon stats handler for all game types"""
\r
655 # Version 1 of stats submissions doubled the data sent.
\r
656 # To counteract this we divide the data by 2 only for
\r
657 # POSTs coming from version 1.
\r
659 version = int(game_meta['V'])
\r
662 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
\r
668 for (key,value) in events.items():
\r
669 matched = re.search("acc-(.*?)-cnt-fired", key)
\r
671 weapon_cd = matched.group(1)
\r
672 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
\r
673 pwstat_id = session.execute(seq)
\r
674 pwstat = PlayerWeaponStat()
\r
675 pwstat.player_weapon_stats_id = pwstat_id
\r
676 pwstat.player_id = player.player_id
\r
677 pwstat.game_id = game.game_id
\r
678 pwstat.player_game_stat_id = pgstat.player_game_stat_id
\r
679 pwstat.weapon_cd = weapon_cd
\r
682 pwstat.nick = events['n']
\r
684 pwstat.nick = events['P']
\r
686 if 'acc-' + weapon_cd + '-cnt-fired' in events:
\r
687 pwstat.fired = int(round(float(
\r
688 events['acc-' + weapon_cd + '-cnt-fired'])))
\r
689 if 'acc-' + weapon_cd + '-fired' in events:
\r
690 pwstat.max = int(round(float(
\r
691 events['acc-' + weapon_cd + '-fired'])))
\r
692 if 'acc-' + weapon_cd + '-cnt-hit' in events:
\r
693 pwstat.hit = int(round(float(
\r
694 events['acc-' + weapon_cd + '-cnt-hit'])))
\r
695 if 'acc-' + weapon_cd + '-hit' in events:
\r
696 pwstat.actual = int(round(float(
\r
697 events['acc-' + weapon_cd + '-hit'])))
\r
698 if 'acc-' + weapon_cd + '-frags' in events:
\r
699 pwstat.frags = int(round(float(
\r
700 events['acc-' + weapon_cd + '-frags'])))
\r
703 pwstat.fired = pwstat.fired/2
\r
704 pwstat.max = pwstat.max/2
\r
705 pwstat.hit = pwstat.hit/2
\r
706 pwstat.actual = pwstat.actual/2
\r
707 pwstat.frags = pwstat.frags/2
\r
709 session.add(pwstat)
\r
710 pwstats.append(pwstat)
\r
715 def create_elos(session, game):
\r
716 """Elo handler for all game types."""
\r
718 process_elos(game, session)
\r
719 except Exception as e:
\r
720 log.debug('Error (non-fatal): elo processing failed.')
\r
723 def submit_stats(request):
\r
725 Entry handler for POST stats submissions.
\r
728 # placeholder for the actual session
\r
731 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
\r
732 "----- END REQUEST BODY -----\n\n")
\r
734 (idfp, status) = verify_request(request)
\r
735 (game_meta, raw_players) = parse_stats_submission(request.body)
\r
736 revision = game_meta.get('R', 'unknown')
\r
737 duration = game_meta.get('D', None)
\r
739 # only players present at the end of the match are eligible for stats
\r
740 raw_players = filter(played_in_game, raw_players)
\r
742 do_precondition_checks(request, game_meta, raw_players)
\r
744 # the "duel" gametype is fake
\r
745 if len(raw_players) == 2 \
\r
746 and num_real_players(raw_players) == 2 \
\r
747 and game_meta['G'] == 'dm':
\r
748 game_meta['G'] = 'duel'
\r
750 #----------------------------------------------------------------------
\r
751 # Actual setup (inserts/updates) below here
\r
752 #----------------------------------------------------------------------
\r
753 session = DBSession()
\r
755 game_type_cd = game_meta['G']
\r
757 # All game types create Game, Server, Map, and Player records
\r
759 server = get_or_create_server(
\r
762 name = game_meta['S'],
\r
763 revision = revision,
\r
764 ip_addr = get_remote_addr(request))
\r
766 gmap = get_or_create_map(
\r
768 name = game_meta['M'])
\r
770 game = create_game(
\r
772 start_dt = datetime.datetime.utcnow(),
\r
773 server_id = server.server_id,
\r
774 game_type_cd = game_type_cd,
\r
775 map_id = gmap.map_id,
\r
776 match_id = game_meta['I'],
\r
777 duration = duration)
\r
779 for events in raw_players:
\r
780 player = get_or_create_player(
\r
782 hashkey = events['P'],
\r
783 nick = events.get('n', None))
\r
785 pgstat = create_game_stat(session, game_meta, game, server,
\r
786 gmap, player, events)
\r
788 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
\r
789 pwstats = create_weapon_stats(session, game_meta, game, player,
\r
792 if should_do_elos(game_type_cd):
\r
793 create_elos(session, game)
\r
796 log.debug('Success! Stats recorded.')
\r
797 return Response('200 OK')
\r
798 except Exception as e:
\r