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 collections import namedtuple
\r
12 from xonstat.d0_blind_id import d0_blind_id_verify
\r
13 from xonstat.elo import process_elos
\r
14 from xonstat.models import *
\r
15 from xonstat.util import strip_colors, qfont_decode
\r
18 log = logging.getLogger(__name__)
\r
21 def parse_stats_submission(body):
\r
23 Parses the POST request body for a stats submission
\r
25 # storage vars for the request body
\r
32 for line in body.split('\n'):
\r
34 (key, value) = line.strip().split(' ', 1)
\r
36 # Server (S) and Nick (n) fields can have international characters.
\r
38 value = unicode(value, 'utf-8')
\r
41 if key not in 'Q' 'P' 'n' 'e' 't' 'i':
\r
42 game_meta[key] = value
\r
46 # if we were working on a player or team record already,
\r
47 # append it and work on a new one (only set team info)
\r
50 players.append(events)
\r
51 elif last_key == 'Q':
\r
52 teams.append(events)
\r
59 (subkey, subvalue) = value.split(' ', 1)
\r
60 events[subkey] = subvalue
\r
66 # no key/value pair - move on to the next line
\r
69 # add the last player we were working on
\r
72 players.append(events)
\r
73 elif last_key == 'Q':
\r
74 teams.append(events)
\r
76 return (game_meta, players, teams)
\r
79 def is_blank_game(gametype, players):
\r
80 """Determine if this is a blank game or not. A blank game is either:
\r
82 1) a match that ended in the warmup stage, where accuracy events are not
\r
83 present (for non-CTS games)
\r
85 2) a match in which no player made a positive or negative score AND was
\r
88 ... or for CTS, which doesn't record accuracy events
\r
90 1) a match in which no player made a fastest lap AND was
\r
93 r = re.compile(r'acc-.*-cnt-fired')
\r
94 flg_nonzero_score = False
\r
95 flg_acc_events = False
\r
96 flg_fastest_lap = False
\r
98 for events in players:
\r
99 if is_real_player(events) and played_in_game(events):
\r
100 for (key,value) in events.items():
\r
101 if key == 'scoreboard-score' and value != 0:
\r
102 flg_nonzero_score = True
\r
104 flg_acc_events = True
\r
105 if key == 'scoreboard-fastest':
\r
106 flg_fastest_lap = True
\r
108 if gametype == 'cts':
\r
109 return not flg_fastest_lap
\r
111 return not (flg_nonzero_score and flg_acc_events)
\r
114 def get_remote_addr(request):
\r
115 """Get the Xonotic server's IP address"""
\r
116 if 'X-Forwarded-For' in request.headers:
\r
117 return request.headers['X-Forwarded-For']
\r
119 return request.remote_addr
\r
122 def is_supported_gametype(gametype, version):
\r
123 """Whether a gametype is supported or not"""
\r
124 is_supported = False
\r
126 # if the type can be supported, but with version constraints, uncomment
\r
127 # here and add the restriction for a specific version below
\r
128 supported_game_types = (
\r
146 if gametype in supported_game_types:
\r
147 is_supported = True
\r
149 is_supported = False
\r
151 # some game types were buggy before revisions, thus this additional filter
\r
152 if gametype == 'ca' and version <= 5:
\r
153 is_supported = False
\r
155 return is_supported
\r
158 def verify_request(request):
\r
159 """Verify requests using the d0_blind_id library"""
\r
161 # first determine if we should be verifying or not
\r
162 val_verify_requests = request.registry.settings.get('xonstat.verify_requests', 'true')
\r
163 if val_verify_requests == "true":
\r
164 flg_verify_requests = True
\r
166 flg_verify_requests = False
\r
169 (idfp, status) = d0_blind_id_verify(
\r
170 sig=request.headers['X-D0-Blind-Id-Detached-Signature'],
\r
172 postdata=request.body)
\r
174 log.debug('\nidfp: {0}\nstatus: {1}'.format(idfp, status))
\r
179 if flg_verify_requests and not idfp:
\r
180 log.debug("ERROR: Unverified request")
\r
181 raise pyramid.httpexceptions.HTTPUnauthorized("Unverified request")
\r
183 return (idfp, status)
\r
186 def do_precondition_checks(request, game_meta, raw_players):
\r
187 """Precondition checks for ALL gametypes.
\r
188 These do not require a database connection."""
\r
189 if not has_required_metadata(game_meta):
\r
190 log.debug("ERROR: Required game meta missing")
\r
191 raise pyramid.httpexceptions.HTTPUnprocessableEntity("Missing game meta")
\r
194 version = int(game_meta['V'])
\r
196 log.debug("ERROR: Required game meta invalid")
\r
197 raise pyramid.httpexceptions.HTTPUnprocessableEntity("Invalid game meta")
\r
199 if not is_supported_gametype(game_meta['G'], version):
\r
200 log.debug("ERROR: Unsupported gametype")
\r
201 raise pyramid.httpexceptions.HTTPOk("OK")
\r
203 if not has_minimum_real_players(request.registry.settings, raw_players):
\r
204 log.debug("ERROR: Not enough real players")
\r
205 raise pyramid.httpexceptions.HTTPOk("OK")
\r
207 if is_blank_game(game_meta['G'], raw_players):
\r
208 log.debug("ERROR: Blank game")
\r
209 raise pyramid.httpexceptions.HTTPOk("OK")
\r
212 def is_real_player(events):
\r
214 Determines if a given set of events correspond with a non-bot
\r
216 if not events['P'].startswith('bot#'):
\r
222 def played_in_game(events):
\r
224 Determines if a given set of player events correspond with a player who
\r
225 played in the game (matches 1 and scoreboardvalid 1)
\r
227 if 'matches' in events and 'scoreboardvalid' in events:
\r
233 def num_real_players(player_events):
\r
235 Returns the number of real players (those who played
\r
236 and are on the scoreboard).
\r
240 for events in player_events:
\r
241 if is_real_player(events) and played_in_game(events):
\r
244 return real_players
\r
247 def has_minimum_real_players(settings, player_events):
\r
249 Determines if the collection of player events has enough "real" players
\r
250 to store in the database. The minimum setting comes from the config file
\r
251 under the setting xonstat.minimum_real_players.
\r
253 flg_has_min_real_players = True
\r
256 minimum_required_players = int(
\r
257 settings['xonstat.minimum_required_players'])
\r
259 minimum_required_players = 2
\r
261 real_players = num_real_players(player_events)
\r
263 if real_players < minimum_required_players:
\r
264 flg_has_min_real_players = False
\r
266 return flg_has_min_real_players
\r
269 def has_required_metadata(metadata):
\r
271 Determines if a give set of metadata has enough data to create a game,
\r
272 server, and map with.
\r
274 flg_has_req_metadata = True
\r
276 if 'T' not in metadata or\
\r
277 'G' not in metadata or\
\r
278 'M' not in metadata or\
\r
279 'I' not in metadata or\
\r
280 'S' not in metadata:
\r
281 flg_has_req_metadata = False
\r
283 return flg_has_req_metadata
\r
286 def should_do_weapon_stats(game_type_cd):
\r
287 """True of the game type should record weapon stats. False otherwise."""
\r
288 if game_type_cd in 'cts':
\r
294 def should_do_elos(game_type_cd):
\r
295 """True of the game type should process Elos. False otherwise."""
\r
296 elo_game_types = ('duel', 'dm', 'ca', 'ctf', 'tdm', 'ka', 'ft')
\r
298 if game_type_cd in elo_game_types:
\r
304 def register_new_nick(session, player, new_nick):
\r
306 Change the player record's nick to the newly found nick. Store the old
\r
307 nick in the player_nicks table for that player.
\r
309 session - SQLAlchemy database session factory
\r
310 player - player record whose nick is changing
\r
311 new_nick - the new nickname
\r
313 # see if that nick already exists
\r
314 stripped_nick = strip_colors(qfont_decode(player.nick))
\r
316 player_nick = session.query(PlayerNick).filter_by(
\r
317 player_id=player.player_id, stripped_nick=stripped_nick).one()
\r
318 except NoResultFound, e:
\r
319 # player_id/stripped_nick not found, create one
\r
320 # but we don't store "Anonymous Player #N"
\r
321 if not re.search('^Anonymous Player #\d+$', player.nick):
\r
322 player_nick = PlayerNick()
\r
323 player_nick.player_id = player.player_id
\r
324 player_nick.stripped_nick = stripped_nick
\r
325 player_nick.nick = player.nick
\r
326 session.add(player_nick)
\r
328 # We change to the new nick regardless
\r
329 player.nick = new_nick
\r
330 player.stripped_nick = strip_colors(qfont_decode(new_nick))
\r
331 session.add(player)
\r
334 def update_fastest_cap(session, player_id, game_id, map_id, captime):
\r
336 Check the fastest cap time for the player and map. If there isn't
\r
337 one, insert one. If there is, check if the passed time is faster.
\r
340 # we don't record fastest cap times for bots or anonymous players
\r
344 # see if a cap entry exists already
\r
345 # then check to see if the new captime is faster
\r
347 cur_fastest_cap = session.query(PlayerCaptime).filter_by(
\r
348 player_id=player_id, map_id=map_id).one()
\r
350 # current captime is faster, so update
\r
351 if captime < cur_fastest_cap.fastest_cap:
\r
352 cur_fastest_cap.fastest_cap = captime
\r
353 cur_fastest_cap.game_id = game_id
\r
354 cur_fastest_cap.create_dt = datetime.datetime.utcnow()
\r
355 session.add(cur_fastest_cap)
\r
357 except NoResultFound, e:
\r
358 # none exists, so insert
\r
359 cur_fastest_cap = PlayerCaptime(player_id, game_id, map_id, captime)
\r
360 session.add(cur_fastest_cap)
\r
364 def get_or_create_server(session, name, hashkey, ip_addr, revision, port):
\r
366 Find a server by name or create one if not found. Parameters:
\r
368 session - SQLAlchemy database session factory
\r
369 name - server name of the server to be found or created
\r
370 hashkey - server hashkey
\r
379 # finding by hashkey is preferred, but if not we will fall
\r
380 # back to using name only, which can result in dupes
\r
381 if hashkey is not None:
\r
382 servers = session.query(Server).\
\r
383 filter_by(hashkey=hashkey).\
\r
384 order_by(expr.desc(Server.create_dt)).limit(1).all()
\r
386 if len(servers) > 0:
\r
387 server = servers[0]
\r
388 log.debug("Found existing server {0} by hashkey ({1})".format(
\r
389 server.server_id, server.hashkey))
\r
391 servers = session.query(Server).\
\r
392 filter_by(name=name).\
\r
393 order_by(expr.desc(Server.create_dt)).limit(1).all()
\r
395 if len(servers) > 0:
\r
396 server = servers[0]
\r
397 log.debug("Found existing server {0} by name".format(server.server_id))
\r
399 # still haven't found a server by hashkey or name, so we need to create one
\r
401 server = Server(name=name, hashkey=hashkey)
\r
402 session.add(server)
\r
404 log.debug("Created server {0} with hashkey {1}".format(
\r
405 server.server_id, server.hashkey))
\r
407 # detect changed fields
\r
408 if server.name != name:
\r
410 session.add(server)
\r
412 if server.hashkey != hashkey:
\r
413 server.hashkey = hashkey
\r
414 session.add(server)
\r
416 if server.ip_addr != ip_addr:
\r
417 server.ip_addr = ip_addr
\r
418 session.add(server)
\r
420 if server.port != port:
\r
422 session.add(server)
\r
424 if server.revision != revision:
\r
425 server.revision = revision
\r
426 session.add(server)
\r
431 def get_or_create_map(session=None, name=None):
\r
433 Find a map by name or create one if not found. Parameters:
\r
435 session - SQLAlchemy database session factory
\r
436 name - map name of the map to be found or created
\r
439 # find one by the name, if it exists
\r
440 gmap = session.query(Map).filter_by(name=name).one()
\r
441 log.debug("Found map id {0}: {1}".format(gmap.map_id,
\r
443 except NoResultFound, e:
\r
444 gmap = Map(name=name)
\r
447 log.debug("Created map id {0}: {1}".format(gmap.map_id,
\r
449 except MultipleResultsFound, e:
\r
450 # multiple found, so use the first one but warn
\r
452 gmaps = session.query(Map).filter_by(name=name).order_by(
\r
455 log.debug("Found map id {0}: {1} but found \
\r
456 multiple".format(gmap.map_id, gmap.name))
\r
461 def create_game(session, start_dt, game_type_cd, server_id, map_id,
\r
462 match_id, duration, mod, winner=None):
\r
464 Creates a game. Parameters:
\r
466 session - SQLAlchemy database session factory
\r
467 start_dt - when the game started (datetime object)
\r
468 game_type_cd - the game type of the game being played
\r
469 server_id - server identifier of the server hosting the game
\r
470 map_id - map on which the game was played
\r
471 winner - the team id of the team that won
\r
472 duration - how long the game lasted
\r
473 mod - mods in use during the game
\r
475 seq = Sequence('games_game_id_seq')
\r
476 game_id = session.execute(seq)
\r
477 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
\r
478 server_id=server_id, map_id=map_id, winner=winner)
\r
479 game.match_id = match_id
\r
480 game.mod = mod[:64]
\r
483 game.duration = datetime.timedelta(seconds=int(round(float(duration))))
\r
488 session.query(Game).filter(Game.server_id==server_id).\
\r
489 filter(Game.match_id==match_id).one()
\r
491 log.debug("Error: game with same server and match_id found! Ignoring.")
\r
493 # if a game under the same server and match_id found,
\r
494 # this is a duplicate game and can be ignored
\r
495 raise pyramid.httpexceptions.HTTPOk('OK')
\r
496 except NoResultFound, e:
\r
497 # server_id/match_id combination not found. game is ok to insert
\r
500 log.debug("Created game id {0} on server {1}, map {2} at \
\r
501 {3}".format(game.game_id,
\r
502 server_id, map_id, start_dt))
\r
507 def get_or_create_player(session=None, hashkey=None, nick=None):
\r
509 Finds a player by hashkey or creates a new one (along with a
\r
510 corresponding hashkey entry. Parameters:
\r
512 session - SQLAlchemy database session factory
\r
513 hashkey - hashkey of the player to be found or created
\r
514 nick - nick of the player (in case of a first time create)
\r
517 if re.search('^bot#\d+$', hashkey) or re.search('^bot#\d+#', hashkey):
\r
518 player = session.query(Player).filter_by(player_id=1).one()
\r
519 # if we have an untracked player
\r
520 elif re.search('^player#\d+$', hashkey):
\r
521 player = session.query(Player).filter_by(player_id=2).one()
\r
522 # else it is a tracked player
\r
524 # see if the player is already in the database
\r
525 # if not, create one and the hashkey along with it
\r
527 hk = session.query(Hashkey).filter_by(
\r
528 hashkey=hashkey).one()
\r
529 player = session.query(Player).filter_by(
\r
530 player_id=hk.player_id).one()
\r
531 log.debug("Found existing player {0} with hashkey {1}".format(
\r
532 player.player_id, hashkey))
\r
535 session.add(player)
\r
538 # if nick is given to us, use it. If not, use "Anonymous Player"
\r
539 # with a suffix added for uniqueness.
\r
541 player.nick = nick[:128]
\r
542 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
\r
544 player.nick = "Anonymous Player #{0}".format(player.player_id)
\r
545 player.stripped_nick = player.nick
\r
547 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
\r
549 log.debug("Created player {0} ({2}) with hashkey {1}".format(
\r
550 player.player_id, hashkey, player.nick.encode('utf-8')))
\r
555 def create_default_game_stat(session, game_type_cd):
\r
556 """Creates a blanked-out pgstat record for the given game type"""
\r
558 # this is what we have to do to get partitioned records in - grab the
\r
559 # sequence value first, then insert using the explicit ID (vs autogenerate)
\r
560 seq = Sequence('player_game_stats_player_game_stat_id_seq')
\r
561 pgstat_id = session.execute(seq)
\r
562 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
\r
563 create_dt=datetime.datetime.utcnow())
\r
565 if game_type_cd == 'as':
\r
566 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
\r
568 if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
\r
569 pgstat.kills = pgstat.deaths = pgstat.suicides = 0
\r
571 if game_type_cd == 'cq':
\r
572 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
\r
575 if game_type_cd == 'ctf':
\r
576 pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
\r
577 pgstat.returns = pgstat.carrier_frags = 0
\r
579 if game_type_cd == 'cts':
\r
582 if game_type_cd == 'dom':
\r
583 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
586 if game_type_cd == 'ft':
\r
587 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
\r
589 if game_type_cd == 'ka':
\r
590 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
591 pgstat.carrier_frags = 0
\r
592 pgstat.time = datetime.timedelta(seconds=0)
\r
594 if game_type_cd == 'kh':
\r
595 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
596 pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
\r
597 pgstat.carrier_frags = 0
\r
599 if game_type_cd == 'lms':
\r
600 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
\r
602 if game_type_cd == 'nb':
\r
603 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
\r
606 if game_type_cd == 'rc':
\r
607 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
\r
612 def create_game_stat(session, game_meta, game, server, gmap, player, teams, events):
\r
613 """Game stats handler for all game types"""
\r
615 game_type_cd = game.game_type_cd
\r
617 pgstat = create_default_game_stat(session, game_type_cd)
\r
619 # these fields should be on every pgstat record
\r
620 pgstat.game_id = game.game_id
\r
621 pgstat.player_id = player.player_id
\r
622 pgstat.nick = events.get('n', 'Anonymous Player')[:128]
\r
623 pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
\r
624 pgstat.score = int(round(float(events.get('scoreboard-score', 0))))
\r
625 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
\r
626 pgstat.rank = int(events.get('rank', None))
\r
627 pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
\r
629 if pgstat.nick != player.nick \
\r
630 and player.player_id > 2 \
\r
631 and pgstat.nick != 'Anonymous Player':
\r
632 register_new_nick(session, player, pgstat.nick)
\r
636 # gametype-specific stuff is handled here. if passed to us, we store it
\r
637 for (key,value) in events.items():
\r
638 if key == 'wins': wins = True
\r
639 if key == 't': pgstat.team = int(value)
\r
641 if key == 'scoreboard-drops': pgstat.drops = int(value)
\r
642 if key == 'scoreboard-returns': pgstat.returns = int(value)
\r
643 if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
\r
644 if key == 'scoreboard-pickups': pgstat.pickups = int(value)
\r
645 if key == 'scoreboard-caps': pgstat.captures = int(value)
\r
646 if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
\r
647 if key == 'scoreboard-deaths': pgstat.deaths = int(value)
\r
648 if key == 'scoreboard-kills': pgstat.kills = int(value)
\r
649 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
\r
650 if key == 'scoreboard-objectives': pgstat.collects = int(value)
\r
651 if key == 'scoreboard-captured': pgstat.captures = int(value)
\r
652 if key == 'scoreboard-released': pgstat.drops = int(value)
\r
653 if key == 'scoreboard-fastest':
\r
654 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
\r
655 if key == 'scoreboard-takes': pgstat.pickups = int(value)
\r
656 if key == 'scoreboard-ticks': pgstat.drops = int(value)
\r
657 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
\r
658 if key == 'scoreboard-bctime':
\r
659 pgstat.time = datetime.timedelta(seconds=int(value))
\r
660 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
\r
661 if key == 'scoreboard-losses': pgstat.drops = int(value)
\r
662 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
\r
663 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
\r
664 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
\r
665 if key == 'scoreboard-lives': pgstat.lives = int(value)
\r
666 if key == 'scoreboard-goals': pgstat.captures = int(value)
\r
667 if key == 'scoreboard-faults': pgstat.drops = int(value)
\r
668 if key == 'scoreboard-laps': pgstat.laps = int(value)
\r
670 if key == 'avglatency': pgstat.avg_latency = float(value)
\r
671 if key == 'scoreboard-captime':
\r
672 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
\r
673 if game.game_type_cd == 'ctf':
\r
674 update_fastest_cap(session, player.player_id, game.game_id,
\r
675 gmap.map_id, pgstat.fastest)
\r
677 pgstat.teamscore = teams[pgstat.team].score
\r
679 # there is no "winning team" field, so we have to derive it
\r
680 if wins and pgstat.team is not None and game.winner is None:
\r
681 game.winner = pgstat.team
\r
684 session.add(pgstat)
\r
689 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
\r
690 """Weapon stats handler for all game types"""
\r
693 # Version 1 of stats submissions doubled the data sent.
\r
694 # To counteract this we divide the data by 2 only for
\r
695 # POSTs coming from version 1.
\r
697 version = int(game_meta['V'])
\r
700 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
\r
706 for (key,value) in events.items():
\r
707 matched = re.search("acc-(.*?)-cnt-fired", key)
\r
709 weapon_cd = matched.group(1)
\r
710 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
\r
711 pwstat_id = session.execute(seq)
\r
712 pwstat = PlayerWeaponStat()
\r
713 pwstat.player_weapon_stats_id = pwstat_id
\r
714 pwstat.player_id = player.player_id
\r
715 pwstat.game_id = game.game_id
\r
716 pwstat.player_game_stat_id = pgstat.player_game_stat_id
\r
717 pwstat.weapon_cd = weapon_cd
\r
720 pwstat.nick = events['n']
\r
722 pwstat.nick = events['P']
\r
724 if 'acc-' + weapon_cd + '-cnt-fired' in events:
\r
725 pwstat.fired = int(round(float(
\r
726 events['acc-' + weapon_cd + '-cnt-fired'])))
\r
727 if 'acc-' + weapon_cd + '-fired' in events:
\r
728 pwstat.max = int(round(float(
\r
729 events['acc-' + weapon_cd + '-fired'])))
\r
730 if 'acc-' + weapon_cd + '-cnt-hit' in events:
\r
731 pwstat.hit = int(round(float(
\r
732 events['acc-' + weapon_cd + '-cnt-hit'])))
\r
733 if 'acc-' + weapon_cd + '-hit' in events:
\r
734 pwstat.actual = int(round(float(
\r
735 events['acc-' + weapon_cd + '-hit'])))
\r
736 if 'acc-' + weapon_cd + '-frags' in events:
\r
737 pwstat.frags = int(round(float(
\r
738 events['acc-' + weapon_cd + '-frags'])))
\r
741 pwstat.fired = pwstat.fired/2
\r
742 pwstat.max = pwstat.max/2
\r
743 pwstat.hit = pwstat.hit/2
\r
744 pwstat.actual = pwstat.actual/2
\r
745 pwstat.frags = pwstat.frags/2
\r
747 session.add(pwstat)
\r
748 pwstats.append(pwstat)
\r
753 def create_elos(session, game):
\r
754 """Elo handler for all game types."""
\r
756 process_elos(game, session)
\r
757 except Exception as e:
\r
758 log.debug('Error (non-fatal): elo processing failed.')
\r
761 def submit_stats(request):
\r
763 Entry handler for POST stats submissions.
\r
766 # placeholder for the actual session
\r
769 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
\r
770 "----- END REQUEST BODY -----\n\n")
\r
772 (idfp, status) = verify_request(request)
\r
773 (game_meta, raw_players, raw_teams) = parse_stats_submission(request.body)
\r
774 revision = game_meta.get('R', 'unknown')
\r
775 duration = game_meta.get('D', None)
\r
777 # only players present at the end of the match are eligible for stats
\r
778 raw_players = filter(played_in_game, raw_players)
\r
780 do_precondition_checks(request, game_meta, raw_players)
\r
782 # the "duel" gametype is fake
\r
783 if len(raw_players) == 2 \
\r
784 and num_real_players(raw_players) == 2 \
\r
785 and game_meta['G'] == 'dm':
\r
786 game_meta['G'] = 'duel'
\r
788 #----------------------------------------------------------------------
\r
789 # Actual setup (inserts/updates) below here
\r
790 #----------------------------------------------------------------------
\r
791 session = DBSession()
\r
793 game_type_cd = game_meta['G']
\r
795 # All game types create Game, Server, Map, and Player records
\r
797 server = get_or_create_server(
\r
800 name = game_meta['S'],
\r
801 revision = revision,
\r
802 ip_addr = get_remote_addr(request),
\r
803 port = game_meta.get('U', None))
\r
805 gmap = get_or_create_map(
\r
807 name = game_meta['M'])
\r
809 game = create_game(
\r
811 start_dt = datetime.datetime.utcnow(),
\r
812 server_id = server.server_id,
\r
813 game_type_cd = game_type_cd,
\r
814 map_id = gmap.map_id,
\r
815 match_id = game_meta['I'],
\r
816 duration = duration,
\r
817 mod = game_meta.get('O', None))
\r
819 TeamInfo = namedtuple("TeamInfo", ['team','score'])
\r
821 for events in raw_teams:
\r
823 if team.startswith("team#"):
\r
825 for (key,value) in events.items():
\r
826 if key == 'scoreboard-teamscore':
\r
827 teams[t] = TeamInfo(team=t, score=int(value))
\r
829 for events in raw_players:
\r
830 player = get_or_create_player(
\r
832 hashkey = events['P'],
\r
833 nick = events.get('n', None))
\r
835 pgstat = create_game_stat(session, game_meta, game, server,
\r
836 gmap, player, teams, events)
\r
838 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
\r
839 pwstats = create_weapon_stats(session, game_meta, game, player,
\r
842 if should_do_elos(game_type_cd):
\r
843 create_elos(session, game)
\r
846 log.debug('Success! Stats recorded.')
\r
847 return Response('200 OK')
\r
848 except Exception as e:
\r