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':
\r
637 if game_type_cd != 'cts':
\r
638 pgstat.kills = int(value)
\r
639 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
\r
640 if key == 'scoreboard-objectives': pgstat.collects = int(value)
\r
641 if key == 'scoreboard-captured': pgstat.captures = int(value)
\r
642 if key == 'scoreboard-released': pgstat.drops = int(value)
\r
643 if key == 'scoreboard-fastest':
\r
644 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
\r
645 if key == 'scoreboard-takes': pgstat.pickups = int(value)
\r
646 if key == 'scoreboard-ticks': pgstat.drops = int(value)
\r
647 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
\r
648 if key == 'scoreboard-bctime':
\r
649 pgstat.time = datetime.timedelta(seconds=int(value))
\r
650 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
\r
651 if key == 'scoreboard-losses': pgstat.drops = int(value)
\r
652 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
\r
653 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
\r
654 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
\r
655 if key == 'scoreboard-lives': pgstat.lives = int(value)
\r
656 if key == 'scoreboard-goals': pgstat.captures = int(value)
\r
657 if key == 'scoreboard-faults': pgstat.drops = int(value)
\r
658 if key == 'scoreboard-laps': pgstat.laps = int(value)
\r
660 if key == 'avglatency': pgstat.avg_latency = float(value)
\r
661 if key == 'scoreboard-captime':
\r
662 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
\r
663 if game.game_type_cd == 'ctf':
\r
664 update_fastest_cap(session, player.player_id, game.game_id,
\r
665 gmap.map_id, pgstat.fastest)
\r
667 # there is no "winning team" field, so we have to derive it
\r
668 if wins and pgstat.team is not None and game.winner is None:
\r
669 game.winner = pgstat.team
\r
672 session.add(pgstat)
\r
677 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
\r
678 """Weapon stats handler for all game types"""
\r
681 # Version 1 of stats submissions doubled the data sent.
\r
682 # To counteract this we divide the data by 2 only for
\r
683 # POSTs coming from version 1.
\r
685 version = int(game_meta['V'])
\r
688 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
\r
694 for (key,value) in events.items():
\r
695 matched = re.search("acc-(.*?)-cnt-fired", key)
\r
697 weapon_cd = matched.group(1)
\r
698 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
\r
699 pwstat_id = session.execute(seq)
\r
700 pwstat = PlayerWeaponStat()
\r
701 pwstat.player_weapon_stats_id = pwstat_id
\r
702 pwstat.player_id = player.player_id
\r
703 pwstat.game_id = game.game_id
\r
704 pwstat.player_game_stat_id = pgstat.player_game_stat_id
\r
705 pwstat.weapon_cd = weapon_cd
\r
708 pwstat.nick = events['n']
\r
710 pwstat.nick = events['P']
\r
712 if 'acc-' + weapon_cd + '-cnt-fired' in events:
\r
713 pwstat.fired = int(round(float(
\r
714 events['acc-' + weapon_cd + '-cnt-fired'])))
\r
715 if 'acc-' + weapon_cd + '-fired' in events:
\r
716 pwstat.max = int(round(float(
\r
717 events['acc-' + weapon_cd + '-fired'])))
\r
718 if 'acc-' + weapon_cd + '-cnt-hit' in events:
\r
719 pwstat.hit = int(round(float(
\r
720 events['acc-' + weapon_cd + '-cnt-hit'])))
\r
721 if 'acc-' + weapon_cd + '-hit' in events:
\r
722 pwstat.actual = int(round(float(
\r
723 events['acc-' + weapon_cd + '-hit'])))
\r
724 if 'acc-' + weapon_cd + '-frags' in events:
\r
725 pwstat.frags = int(round(float(
\r
726 events['acc-' + weapon_cd + '-frags'])))
\r
729 pwstat.fired = pwstat.fired/2
\r
730 pwstat.max = pwstat.max/2
\r
731 pwstat.hit = pwstat.hit/2
\r
732 pwstat.actual = pwstat.actual/2
\r
733 pwstat.frags = pwstat.frags/2
\r
735 session.add(pwstat)
\r
736 pwstats.append(pwstat)
\r
741 def create_elos(session, game):
\r
742 """Elo handler for all game types."""
\r
744 process_elos(game, session)
\r
745 except Exception as e:
\r
746 log.debug('Error (non-fatal): elo processing failed.')
\r
749 def submit_stats(request):
\r
751 Entry handler for POST stats submissions.
\r
754 # placeholder for the actual session
\r
757 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
\r
758 "----- END REQUEST BODY -----\n\n")
\r
760 (idfp, status) = verify_request(request)
\r
761 (game_meta, raw_players) = parse_stats_submission(request.body)
\r
762 revision = game_meta.get('R', 'unknown')
\r
763 duration = game_meta.get('D', None)
\r
765 # only players present at the end of the match are eligible for stats
\r
766 raw_players = filter(played_in_game, raw_players)
\r
768 do_precondition_checks(request, game_meta, raw_players)
\r
770 # the "duel" gametype is fake
\r
771 if len(raw_players) == 2 \
\r
772 and num_real_players(raw_players) == 2 \
\r
773 and game_meta['G'] == 'dm':
\r
774 game_meta['G'] = 'duel'
\r
776 #----------------------------------------------------------------------
\r
777 # Actual setup (inserts/updates) below here
\r
778 #----------------------------------------------------------------------
\r
779 session = DBSession()
\r
781 game_type_cd = game_meta['G']
\r
783 # All game types create Game, Server, Map, and Player records
\r
785 server = get_or_create_server(
\r
788 name = game_meta['S'],
\r
789 revision = revision,
\r
790 ip_addr = get_remote_addr(request),
\r
791 port = game_meta.get('U', None))
\r
793 gmap = get_or_create_map(
\r
795 name = game_meta['M'])
\r
797 game = create_game(
\r
799 start_dt = datetime.datetime.utcnow(),
\r
800 server_id = server.server_id,
\r
801 game_type_cd = game_type_cd,
\r
802 map_id = gmap.map_id,
\r
803 match_id = game_meta['I'],
\r
804 duration = duration,
\r
805 mod = game_meta.get('O', None))
\r
807 for events in raw_players:
\r
808 player = get_or_create_player(
\r
810 hashkey = events['P'],
\r
811 nick = events.get('n', None))
\r
813 pgstat = create_game_stat(session, game_meta, game, server,
\r
814 gmap, player, events)
\r
816 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
\r
817 pwstats = create_weapon_stats(session, game_meta, game, player,
\r
820 if should_do_elos(game_type_cd):
\r
821 create_elos(session, game)
\r
824 log.debug('Success! Stats recorded.')
\r
825 return Response('200 OK')
\r
826 except Exception as e:
\r