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', 'ka', 'ft')
\r
285 if game_type_cd in elo_game_types:
\r
291 def register_new_nick(session, player, new_nick):
\r
293 Change the player record's nick to the newly found nick. Store the old
\r
294 nick in the player_nicks table for that player.
\r
296 session - SQLAlchemy database session factory
\r
297 player - player record whose nick is changing
\r
298 new_nick - the new nickname
\r
300 # see if that nick already exists
\r
301 stripped_nick = strip_colors(qfont_decode(player.nick))
\r
303 player_nick = session.query(PlayerNick).filter_by(
\r
304 player_id=player.player_id, stripped_nick=stripped_nick).one()
\r
305 except NoResultFound, e:
\r
306 # player_id/stripped_nick not found, create one
\r
307 # but we don't store "Anonymous Player #N"
\r
308 if not re.search('^Anonymous Player #\d+$', player.nick):
\r
309 player_nick = PlayerNick()
\r
310 player_nick.player_id = player.player_id
\r
311 player_nick.stripped_nick = stripped_nick
\r
312 player_nick.nick = player.nick
\r
313 session.add(player_nick)
\r
315 # We change to the new nick regardless
\r
316 player.nick = new_nick
\r
317 player.stripped_nick = strip_colors(qfont_decode(new_nick))
\r
318 session.add(player)
\r
321 def update_fastest_cap(session, player_id, game_id, map_id, captime):
\r
323 Check the fastest cap time for the player and map. If there isn't
\r
324 one, insert one. If there is, check if the passed time is faster.
\r
327 # we don't record fastest cap times for bots or anonymous players
\r
331 # see if a cap entry exists already
\r
332 # then check to see if the new captime is faster
\r
334 cur_fastest_cap = session.query(PlayerCaptime).filter_by(
\r
335 player_id=player_id, map_id=map_id).one()
\r
337 # current captime is faster, so update
\r
338 if captime < cur_fastest_cap.fastest_cap:
\r
339 cur_fastest_cap.fastest_cap = captime
\r
340 cur_fastest_cap.game_id = game_id
\r
341 cur_fastest_cap.create_dt = datetime.datetime.utcnow()
\r
342 session.add(cur_fastest_cap)
\r
344 except NoResultFound, e:
\r
345 # none exists, so insert
\r
346 cur_fastest_cap = PlayerCaptime(player_id, game_id, map_id, captime)
\r
347 session.add(cur_fastest_cap)
\r
351 def get_or_create_server(session=None, name=None, hashkey=None, ip_addr=None,
\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
361 # find one by that name, if it exists
\r
362 server = session.query(Server).filter_by(name=name).one()
\r
364 # store new hashkey
\r
365 if server.hashkey != hashkey:
\r
366 server.hashkey = hashkey
\r
367 session.add(server)
\r
369 # store new IP address
\r
370 if server.ip_addr != ip_addr:
\r
371 server.ip_addr = ip_addr
\r
372 session.add(server)
\r
374 # store new revision
\r
375 if server.revision != revision:
\r
376 server.revision = revision
\r
377 session.add(server)
\r
379 log.debug("Found existing server {0}".format(server.server_id))
\r
381 except MultipleResultsFound, e:
\r
382 # multiple found, so also filter by hashkey
\r
383 server = session.query(Server).filter_by(name=name).\
\r
384 filter_by(hashkey=hashkey).one()
\r
385 log.debug("Found existing server {0}".format(server.server_id))
\r
387 except NoResultFound, e:
\r
388 # not found, 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
398 def get_or_create_map(session=None, name=None):
\r
400 Find a map by name or create one if not found. Parameters:
\r
402 session - SQLAlchemy database session factory
\r
403 name - map name of the map to be found or created
\r
406 # find one by the name, if it exists
\r
407 gmap = session.query(Map).filter_by(name=name).one()
\r
408 log.debug("Found map id {0}: {1}".format(gmap.map_id,
\r
410 except NoResultFound, e:
\r
411 gmap = Map(name=name)
\r
414 log.debug("Created map id {0}: {1}".format(gmap.map_id,
\r
416 except MultipleResultsFound, e:
\r
417 # multiple found, so use the first one but warn
\r
419 gmaps = session.query(Map).filter_by(name=name).order_by(
\r
422 log.debug("Found map id {0}: {1} but found \
\r
423 multiple".format(gmap.map_id, gmap.name))
\r
428 def create_game(session=None, start_dt=None, game_type_cd=None,
\r
429 server_id=None, map_id=None, winner=None, match_id=None,
\r
432 Creates a game. Parameters:
\r
434 session - SQLAlchemy database session factory
\r
435 start_dt - when the game started (datetime object)
\r
436 game_type_cd - the game type of the game being played
\r
437 server_id - server identifier of the server hosting the game
\r
438 map_id - map on which the game was played
\r
439 winner - the team id of the team that won
\r
441 seq = Sequence('games_game_id_seq')
\r
442 game_id = session.execute(seq)
\r
443 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
\r
444 server_id=server_id, map_id=map_id, winner=winner)
\r
445 game.match_id = match_id
\r
448 game.duration = datetime.timedelta(seconds=int(round(float(duration))))
\r
453 session.query(Game).filter(Game.server_id==server_id).\
\r
454 filter(Game.match_id==match_id).one()
\r
456 log.debug("Error: game with same server and match_id found! Ignoring.")
\r
458 # if a game under the same server and match_id found,
\r
459 # this is a duplicate game and can be ignored
\r
460 raise pyramid.httpexceptions.HTTPOk('OK')
\r
461 except NoResultFound, e:
\r
462 # server_id/match_id combination not found. game is ok to insert
\r
465 log.debug("Created game id {0} on server {1}, map {2} at \
\r
466 {3}".format(game.game_id,
\r
467 server_id, map_id, start_dt))
\r
472 def get_or_create_player(session=None, hashkey=None, nick=None):
\r
474 Finds a player by hashkey or creates a new one (along with a
\r
475 corresponding hashkey entry. Parameters:
\r
477 session - SQLAlchemy database session factory
\r
478 hashkey - hashkey of the player to be found or created
\r
479 nick - nick of the player (in case of a first time create)
\r
482 if re.search('^bot#\d+$', hashkey) or re.search('^bot#\d+#', hashkey):
\r
483 player = session.query(Player).filter_by(player_id=1).one()
\r
484 # if we have an untracked player
\r
485 elif re.search('^player#\d+$', hashkey):
\r
486 player = session.query(Player).filter_by(player_id=2).one()
\r
487 # else it is a tracked player
\r
489 # see if the player is already in the database
\r
490 # if not, create one and the hashkey along with it
\r
492 hk = session.query(Hashkey).filter_by(
\r
493 hashkey=hashkey).one()
\r
494 player = session.query(Player).filter_by(
\r
495 player_id=hk.player_id).one()
\r
496 log.debug("Found existing player {0} with hashkey {1}".format(
\r
497 player.player_id, hashkey))
\r
500 session.add(player)
\r
503 # if nick is given to us, use it. If not, use "Anonymous Player"
\r
504 # with a suffix added for uniqueness.
\r
506 player.nick = nick[:128]
\r
507 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
\r
509 player.nick = "Anonymous Player #{0}".format(player.player_id)
\r
510 player.stripped_nick = player.nick
\r
512 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
\r
514 log.debug("Created player {0} ({2}) with hashkey {1}".format(
\r
515 player.player_id, hashkey, player.nick.encode('utf-8')))
\r
520 def create_default_game_stat(session, game_type_cd):
\r
521 """Creates a blanked-out pgstat record for the given game type"""
\r
523 # this is what we have to do to get partitioned records in - grab the
\r
524 # sequence value first, then insert using the explicit ID (vs autogenerate)
\r
525 seq = Sequence('player_game_stats_player_game_stat_id_seq')
\r
526 pgstat_id = session.execute(seq)
\r
527 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
\r
528 create_dt=datetime.datetime.utcnow())
\r
530 if game_type_cd == 'as':
\r
531 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
\r
533 if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
\r
534 pgstat.kills = pgstat.deaths = pgstat.suicides = 0
\r
536 if game_type_cd == 'cq':
\r
537 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
\r
540 if game_type_cd == 'ctf':
\r
541 pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
\r
542 pgstat.returns = pgstat.carrier_frags = 0
\r
544 if game_type_cd == 'cts':
\r
547 if game_type_cd == 'dom':
\r
548 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
551 if game_type_cd == 'ft':
\r
552 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
\r
554 if game_type_cd == 'ka':
\r
555 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
556 pgstat.carrier_frags = 0
\r
557 pgstat.time = datetime.timedelta(seconds=0)
\r
559 if game_type_cd == 'kh':
\r
560 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
561 pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
\r
562 pgstat.carrier_frags = 0
\r
564 if game_type_cd == 'lms':
\r
565 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
\r
567 if game_type_cd == 'nb':
\r
568 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
\r
571 if game_type_cd == 'rc':
\r
572 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
\r
577 def create_game_stat(session, game_meta, game, server, gmap, player, events):
\r
578 """Game stats handler for all game types"""
\r
580 pgstat = create_default_game_stat(session, game.game_type_cd)
\r
582 # these fields should be on every pgstat record
\r
583 pgstat.game_id = game.game_id
\r
584 pgstat.player_id = player.player_id
\r
585 pgstat.nick = events.get('n', 'Anonymous Player')[:128]
\r
586 pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
\r
587 pgstat.score = int(round(float(events.get('scoreboard-score', 0))))
\r
588 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
\r
589 pgstat.rank = int(events.get('rank', None))
\r
590 pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
\r
592 if pgstat.nick != player.nick \
\r
593 and player.player_id > 2 \
\r
594 and pgstat.nick != 'Anonymous Player':
\r
595 register_new_nick(session, player, pgstat.nick)
\r
599 # gametype-specific stuff is handled here. if passed to us, we store it
\r
600 for (key,value) in events.items():
\r
601 if key == 'wins': wins = True
\r
602 if key == 't': pgstat.team = int(value)
\r
604 if key == 'scoreboard-drops': pgstat.drops = int(value)
\r
605 if key == 'scoreboard-returns': pgstat.returns = int(value)
\r
606 if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
\r
607 if key == 'scoreboard-pickups': pgstat.pickups = int(value)
\r
608 if key == 'scoreboard-caps': pgstat.captures = int(value)
\r
609 if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
\r
610 if key == 'scoreboard-deaths': pgstat.deaths = int(value)
\r
611 if key == 'scoreboard-kills': pgstat.kills = int(value)
\r
612 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
\r
613 if key == 'scoreboard-objectives': pgstat.collects = int(value)
\r
614 if key == 'scoreboard-captured': pgstat.captures = int(value)
\r
615 if key == 'scoreboard-released': pgstat.drops = int(value)
\r
616 if key == 'scoreboard-fastest':
\r
617 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
\r
618 if key == 'scoreboard-takes': pgstat.pickups = int(value)
\r
619 if key == 'scoreboard-ticks': pgstat.drops = int(value)
\r
620 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
\r
621 if key == 'scoreboard-bctime':
\r
622 pgstat.time = datetime.timedelta(seconds=int(value))
\r
623 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
\r
624 if key == 'scoreboard-losses': pgstat.drops = int(value)
\r
625 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
\r
626 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
\r
627 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
\r
628 if key == 'scoreboard-lives': pgstat.lives = int(value)
\r
629 if key == 'scoreboard-goals': pgstat.captures = int(value)
\r
630 if key == 'scoreboard-faults': pgstat.drops = int(value)
\r
631 if key == 'scoreboard-laps': pgstat.laps = int(value)
\r
633 if key == 'avglatency': pgstat.avg_latency = float(value)
\r
634 if key == 'scoreboard-captime':
\r
635 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
\r
636 if game.game_type_cd == 'ctf':
\r
637 update_fastest_cap(session, player.player_id, game.game_id,
\r
638 gmap.map_id, pgstat.fastest)
\r
640 # there is no "winning team" field, so we have to derive it
\r
641 if wins and pgstat.team is not None and game.winner is None:
\r
642 game.winner = pgstat.team
\r
645 session.add(pgstat)
\r
650 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
\r
651 """Weapon stats handler for all game types"""
\r
654 # Version 1 of stats submissions doubled the data sent.
\r
655 # To counteract this we divide the data by 2 only for
\r
656 # POSTs coming from version 1.
\r
658 version = int(game_meta['V'])
\r
661 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
\r
667 for (key,value) in events.items():
\r
668 matched = re.search("acc-(.*?)-cnt-fired", key)
\r
670 weapon_cd = matched.group(1)
\r
671 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
\r
672 pwstat_id = session.execute(seq)
\r
673 pwstat = PlayerWeaponStat()
\r
674 pwstat.player_weapon_stats_id = pwstat_id
\r
675 pwstat.player_id = player.player_id
\r
676 pwstat.game_id = game.game_id
\r
677 pwstat.player_game_stat_id = pgstat.player_game_stat_id
\r
678 pwstat.weapon_cd = weapon_cd
\r
681 pwstat.nick = events['n']
\r
683 pwstat.nick = events['P']
\r
685 if 'acc-' + weapon_cd + '-cnt-fired' in events:
\r
686 pwstat.fired = int(round(float(
\r
687 events['acc-' + weapon_cd + '-cnt-fired'])))
\r
688 if 'acc-' + weapon_cd + '-fired' in events:
\r
689 pwstat.max = int(round(float(
\r
690 events['acc-' + weapon_cd + '-fired'])))
\r
691 if 'acc-' + weapon_cd + '-cnt-hit' in events:
\r
692 pwstat.hit = int(round(float(
\r
693 events['acc-' + weapon_cd + '-cnt-hit'])))
\r
694 if 'acc-' + weapon_cd + '-hit' in events:
\r
695 pwstat.actual = int(round(float(
\r
696 events['acc-' + weapon_cd + '-hit'])))
\r
697 if 'acc-' + weapon_cd + '-frags' in events:
\r
698 pwstat.frags = int(round(float(
\r
699 events['acc-' + weapon_cd + '-frags'])))
\r
702 pwstat.fired = pwstat.fired/2
\r
703 pwstat.max = pwstat.max/2
\r
704 pwstat.hit = pwstat.hit/2
\r
705 pwstat.actual = pwstat.actual/2
\r
706 pwstat.frags = pwstat.frags/2
\r
708 session.add(pwstat)
\r
709 pwstats.append(pwstat)
\r
714 def create_elos(session, game):
\r
715 """Elo handler for all game types."""
\r
717 process_elos(game, session)
\r
718 except Exception as e:
\r
719 log.debug('Error (non-fatal): elo processing failed.')
\r
722 def submit_stats(request):
\r
724 Entry handler for POST stats submissions.
\r
727 # placeholder for the actual session
\r
730 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
\r
731 "----- END REQUEST BODY -----\n\n")
\r
733 (idfp, status) = verify_request(request)
\r
734 (game_meta, raw_players) = parse_stats_submission(request.body)
\r
735 revision = game_meta.get('R', 'unknown')
\r
736 duration = game_meta.get('D', None)
\r
738 # only players present at the end of the match are eligible for stats
\r
739 raw_players = filter(played_in_game, raw_players)
\r
741 do_precondition_checks(request, game_meta, raw_players)
\r
743 # the "duel" gametype is fake
\r
744 if len(raw_players) == 2 \
\r
745 and num_real_players(raw_players) == 2 \
\r
746 and game_meta['G'] == 'dm':
\r
747 game_meta['G'] = 'duel'
\r
749 #----------------------------------------------------------------------
\r
750 # Actual setup (inserts/updates) below here
\r
751 #----------------------------------------------------------------------
\r
752 session = DBSession()
\r
754 game_type_cd = game_meta['G']
\r
756 # All game types create Game, Server, Map, and Player records
\r
758 server = get_or_create_server(
\r
761 name = game_meta['S'],
\r
762 revision = revision,
\r
763 ip_addr = get_remote_addr(request))
\r
765 gmap = get_or_create_map(
\r
767 name = game_meta['M'])
\r
769 game = create_game(
\r
771 start_dt = datetime.datetime.utcnow(),
\r
772 server_id = server.server_id,
\r
773 game_type_cd = game_type_cd,
\r
774 map_id = gmap.map_id,
\r
775 match_id = game_meta['I'],
\r
776 duration = duration)
\r
778 for events in raw_players:
\r
779 player = get_or_create_player(
\r
781 hashkey = events['P'],
\r
782 nick = events.get('n', None))
\r
784 pgstat = create_game_stat(session, game_meta, game, server,
\r
785 gmap, player, events)
\r
787 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
\r
788 pwstats = create_weapon_stats(session, game_meta, game, player,
\r
791 if should_do_elos(game_type_cd):
\r
792 create_elos(session, game)
\r
795 log.debug('Success! Stats recorded.')
\r
796 return Response('200 OK')
\r
797 except Exception as e:
\r