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, start_dt, game_type_cd, server_id, map_id,
\r
450 match_id, duration, mod, winner=None):
\r
452 Creates a game. Parameters:
\r
454 session - SQLAlchemy database session factory
\r
455 start_dt - when the game started (datetime object)
\r
456 game_type_cd - the game type of the game being played
\r
457 server_id - server identifier of the server hosting the game
\r
458 map_id - map on which the game was played
\r
459 winner - the team id of the team that won
\r
460 duration - how long the game lasted
\r
461 mod - mods in use during the game
\r
463 seq = Sequence('games_game_id_seq')
\r
464 game_id = session.execute(seq)
\r
465 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
\r
466 server_id=server_id, map_id=map_id, winner=winner)
\r
467 game.match_id = match_id
\r
468 game.mod = mod[:64]
\r
471 game.duration = datetime.timedelta(seconds=int(round(float(duration))))
\r
476 session.query(Game).filter(Game.server_id==server_id).\
\r
477 filter(Game.match_id==match_id).one()
\r
479 log.debug("Error: game with same server and match_id found! Ignoring.")
\r
481 # if a game under the same server and match_id found,
\r
482 # this is a duplicate game and can be ignored
\r
483 raise pyramid.httpexceptions.HTTPOk('OK')
\r
484 except NoResultFound, e:
\r
485 # server_id/match_id combination not found. game is ok to insert
\r
488 log.debug("Created game id {0} on server {1}, map {2} at \
\r
489 {3}".format(game.game_id,
\r
490 server_id, map_id, start_dt))
\r
495 def get_or_create_player(session=None, hashkey=None, nick=None):
\r
497 Finds a player by hashkey or creates a new one (along with a
\r
498 corresponding hashkey entry. Parameters:
\r
500 session - SQLAlchemy database session factory
\r
501 hashkey - hashkey of the player to be found or created
\r
502 nick - nick of the player (in case of a first time create)
\r
505 if re.search('^bot#\d+$', hashkey) or re.search('^bot#\d+#', hashkey):
\r
506 player = session.query(Player).filter_by(player_id=1).one()
\r
507 # if we have an untracked player
\r
508 elif re.search('^player#\d+$', hashkey):
\r
509 player = session.query(Player).filter_by(player_id=2).one()
\r
510 # else it is a tracked player
\r
512 # see if the player is already in the database
\r
513 # if not, create one and the hashkey along with it
\r
515 hk = session.query(Hashkey).filter_by(
\r
516 hashkey=hashkey).one()
\r
517 player = session.query(Player).filter_by(
\r
518 player_id=hk.player_id).one()
\r
519 log.debug("Found existing player {0} with hashkey {1}".format(
\r
520 player.player_id, hashkey))
\r
523 session.add(player)
\r
526 # if nick is given to us, use it. If not, use "Anonymous Player"
\r
527 # with a suffix added for uniqueness.
\r
529 player.nick = nick[:128]
\r
530 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
\r
532 player.nick = "Anonymous Player #{0}".format(player.player_id)
\r
533 player.stripped_nick = player.nick
\r
535 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
\r
537 log.debug("Created player {0} ({2}) with hashkey {1}".format(
\r
538 player.player_id, hashkey, player.nick.encode('utf-8')))
\r
543 def create_default_game_stat(session, game_type_cd):
\r
544 """Creates a blanked-out pgstat record for the given game type"""
\r
546 # this is what we have to do to get partitioned records in - grab the
\r
547 # sequence value first, then insert using the explicit ID (vs autogenerate)
\r
548 seq = Sequence('player_game_stats_player_game_stat_id_seq')
\r
549 pgstat_id = session.execute(seq)
\r
550 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
\r
551 create_dt=datetime.datetime.utcnow())
\r
553 if game_type_cd == 'as':
\r
554 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
\r
556 if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
\r
557 pgstat.kills = pgstat.deaths = pgstat.suicides = 0
\r
559 if game_type_cd == 'cq':
\r
560 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
\r
563 if game_type_cd == 'ctf':
\r
564 pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
\r
565 pgstat.returns = pgstat.carrier_frags = 0
\r
567 if game_type_cd == 'cts':
\r
570 if game_type_cd == 'dom':
\r
571 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
574 if game_type_cd == 'ft':
\r
575 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
\r
577 if game_type_cd == 'ka':
\r
578 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
579 pgstat.carrier_frags = 0
\r
580 pgstat.time = datetime.timedelta(seconds=0)
\r
582 if game_type_cd == 'kh':
\r
583 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
584 pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
\r
585 pgstat.carrier_frags = 0
\r
587 if game_type_cd == 'lms':
\r
588 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
\r
590 if game_type_cd == 'nb':
\r
591 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
\r
594 if game_type_cd == 'rc':
\r
595 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
\r
600 def create_game_stat(session, game_meta, game, server, gmap, player, events):
\r
601 """Game stats handler for all game types"""
\r
603 game_type_cd = game.game_type_cd
\r
605 pgstat = create_default_game_stat(session, game_type_cd)
\r
607 # these fields should be on every pgstat record
\r
608 pgstat.game_id = game.game_id
\r
609 pgstat.player_id = player.player_id
\r
610 pgstat.nick = events.get('n', 'Anonymous Player')[:128]
\r
611 pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
\r
612 pgstat.score = int(round(float(events.get('scoreboard-score', 0))))
\r
613 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
\r
614 pgstat.rank = int(events.get('rank', None))
\r
615 pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
\r
617 if pgstat.nick != player.nick \
\r
618 and player.player_id > 2 \
\r
619 and pgstat.nick != 'Anonymous Player':
\r
620 register_new_nick(session, player, pgstat.nick)
\r
624 # gametype-specific stuff is handled here. if passed to us, we store it
\r
625 for (key,value) in events.items():
\r
626 if key == 'wins': wins = True
\r
627 if key == 't': pgstat.team = int(value)
\r
629 if key == 'scoreboard-drops': pgstat.drops = int(value)
\r
630 if key == 'scoreboard-returns': pgstat.returns = int(value)
\r
631 if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
\r
632 if key == 'scoreboard-pickups': pgstat.pickups = int(value)
\r
633 if key == 'scoreboard-caps': pgstat.captures = int(value)
\r
634 if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
\r
635 if key == 'scoreboard-deaths': pgstat.deaths = int(value)
\r
636 if key == 'scoreboard-kills': pgstat.kills = int(value)
\r
637 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
\r
638 if key == 'scoreboard-objectives': pgstat.collects = int(value)
\r
639 if key == 'scoreboard-captured': pgstat.captures = int(value)
\r
640 if key == 'scoreboard-released': pgstat.drops = int(value)
\r
641 if key == 'scoreboard-fastest':
\r
642 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
\r
643 if key == 'scoreboard-takes': pgstat.pickups = int(value)
\r
644 if key == 'scoreboard-ticks': pgstat.drops = int(value)
\r
645 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
\r
646 if key == 'scoreboard-bctime':
\r
647 pgstat.time = datetime.timedelta(seconds=int(value))
\r
648 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
\r
649 if key == 'scoreboard-losses': pgstat.drops = int(value)
\r
650 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
\r
651 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
\r
652 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
\r
653 if key == 'scoreboard-lives': pgstat.lives = int(value)
\r
654 if key == 'scoreboard-goals': pgstat.captures = int(value)
\r
655 if key == 'scoreboard-faults': pgstat.drops = int(value)
\r
656 if key == 'scoreboard-laps': pgstat.laps = int(value)
\r
658 if key == 'avglatency': pgstat.avg_latency = float(value)
\r
659 if key == 'scoreboard-captime':
\r
660 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
\r
661 if game.game_type_cd == 'ctf':
\r
662 update_fastest_cap(session, player.player_id, game.game_id,
\r
663 gmap.map_id, pgstat.fastest)
\r
665 # there is no "winning team" field, so we have to derive it
\r
666 if wins and pgstat.team is not None and game.winner is None:
\r
667 game.winner = pgstat.team
\r
670 session.add(pgstat)
\r
675 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
\r
676 """Weapon stats handler for all game types"""
\r
679 # Version 1 of stats submissions doubled the data sent.
\r
680 # To counteract this we divide the data by 2 only for
\r
681 # POSTs coming from version 1.
\r
683 version = int(game_meta['V'])
\r
686 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
\r
692 for (key,value) in events.items():
\r
693 matched = re.search("acc-(.*?)-cnt-fired", key)
\r
695 weapon_cd = matched.group(1)
\r
696 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
\r
697 pwstat_id = session.execute(seq)
\r
698 pwstat = PlayerWeaponStat()
\r
699 pwstat.player_weapon_stats_id = pwstat_id
\r
700 pwstat.player_id = player.player_id
\r
701 pwstat.game_id = game.game_id
\r
702 pwstat.player_game_stat_id = pgstat.player_game_stat_id
\r
703 pwstat.weapon_cd = weapon_cd
\r
706 pwstat.nick = events['n']
\r
708 pwstat.nick = events['P']
\r
710 if 'acc-' + weapon_cd + '-cnt-fired' in events:
\r
711 pwstat.fired = int(round(float(
\r
712 events['acc-' + weapon_cd + '-cnt-fired'])))
\r
713 if 'acc-' + weapon_cd + '-fired' in events:
\r
714 pwstat.max = int(round(float(
\r
715 events['acc-' + weapon_cd + '-fired'])))
\r
716 if 'acc-' + weapon_cd + '-cnt-hit' in events:
\r
717 pwstat.hit = int(round(float(
\r
718 events['acc-' + weapon_cd + '-cnt-hit'])))
\r
719 if 'acc-' + weapon_cd + '-hit' in events:
\r
720 pwstat.actual = int(round(float(
\r
721 events['acc-' + weapon_cd + '-hit'])))
\r
722 if 'acc-' + weapon_cd + '-frags' in events:
\r
723 pwstat.frags = int(round(float(
\r
724 events['acc-' + weapon_cd + '-frags'])))
\r
727 pwstat.fired = pwstat.fired/2
\r
728 pwstat.max = pwstat.max/2
\r
729 pwstat.hit = pwstat.hit/2
\r
730 pwstat.actual = pwstat.actual/2
\r
731 pwstat.frags = pwstat.frags/2
\r
733 session.add(pwstat)
\r
734 pwstats.append(pwstat)
\r
739 def create_elos(session, game):
\r
740 """Elo handler for all game types."""
\r
742 process_elos(game, session)
\r
743 except Exception as e:
\r
744 log.debug('Error (non-fatal): elo processing failed.')
\r
747 def submit_stats(request):
\r
749 Entry handler for POST stats submissions.
\r
752 # placeholder for the actual session
\r
755 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
\r
756 "----- END REQUEST BODY -----\n\n")
\r
758 (idfp, status) = verify_request(request)
\r
759 (game_meta, raw_players) = parse_stats_submission(request.body)
\r
760 revision = game_meta.get('R', 'unknown')
\r
761 duration = game_meta.get('D', None)
\r
763 # only players present at the end of the match are eligible for stats
\r
764 raw_players = filter(played_in_game, raw_players)
\r
766 do_precondition_checks(request, game_meta, raw_players)
\r
768 # the "duel" gametype is fake
\r
769 if len(raw_players) == 2 \
\r
770 and num_real_players(raw_players) == 2 \
\r
771 and game_meta['G'] == 'dm':
\r
772 game_meta['G'] = 'duel'
\r
774 #----------------------------------------------------------------------
\r
775 # Actual setup (inserts/updates) below here
\r
776 #----------------------------------------------------------------------
\r
777 session = DBSession()
\r
779 game_type_cd = game_meta['G']
\r
781 # All game types create Game, Server, Map, and Player records
\r
783 server = get_or_create_server(
\r
786 name = game_meta['S'],
\r
787 revision = revision,
\r
788 ip_addr = get_remote_addr(request),
\r
789 port = game_meta.get('U', None))
\r
791 gmap = get_or_create_map(
\r
793 name = game_meta['M'])
\r
795 game = create_game(
\r
797 start_dt = datetime.datetime.utcnow(),
\r
798 server_id = server.server_id,
\r
799 game_type_cd = game_type_cd,
\r
800 map_id = gmap.map_id,
\r
801 match_id = game_meta['I'],
\r
802 duration = duration,
\r
803 mod = game_meta.get('O', None))
\r
805 for events in raw_players:
\r
806 player = get_or_create_player(
\r
808 hashkey = events['P'],
\r
809 nick = events.get('n', None))
\r
811 pgstat = create_game_stat(session, game_meta, game, server,
\r
812 gmap, player, events)
\r
814 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
\r
815 pwstats = create_weapon_stats(session, game_meta, game, player,
\r
818 if should_do_elos(game_type_cd):
\r
819 create_elos(session, game)
\r
822 log.debug('Success! Stats recorded.')
\r
823 return Response('200 OK')
\r
824 except Exception as e:
\r