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.elo import process_elos
\r
12 from xonstat.models import *
\r
13 from xonstat.util import strip_colors, qfont_decode, verify_request
\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
29 # we're not in either stanza to start
\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
40 if key not in 'P' 'Q' 'n' 'e' 't' 'i':
\r
41 game_meta[key] = value
\r
43 if key == 'Q' or key == 'P':
\r
44 #log.debug('Found a {0}'.format(key))
\r
45 #log.debug('in_Q: {0}'.format(in_Q))
\r
46 #log.debug('in_P: {0}'.format(in_P))
\r
47 #log.debug('events: {0}'.format(events))
\r
49 # check where we were before and append events accordingly
\r
50 if in_Q and len(events) > 0:
\r
51 #log.debug('creating a team (Q) entry')
\r
52 teams.append(events)
\r
54 elif in_P and len(events) > 0:
\r
55 #log.debug('creating a player (P) entry')
\r
56 players.append(events)
\r
60 #log.debug('key == P')
\r
64 #log.debug('key == Q')
\r
71 (subkey, subvalue) = value.split(' ', 1)
\r
72 events[subkey] = subvalue
\r
78 # no key/value pair - move on to the next line
\r
81 # add the last entity we were working on
\r
82 if in_P and len(events) > 0:
\r
83 players.append(events)
\r
84 elif in_Q and len(events) > 0:
\r
85 teams.append(events)
\r
87 return (game_meta, players, teams)
\r
90 def is_blank_game(gametype, players):
\r
91 """Determine if this is a blank game or not. A blank game is either:
\r
93 1) a match that ended in the warmup stage, where accuracy events are not
\r
94 present (for non-CTS games)
\r
96 2) a match in which no player made a positive or negative score AND was
\r
99 ... or for CTS, which doesn't record accuracy events
\r
101 1) a match in which no player made a fastest lap AND was
\r
104 r = re.compile(r'acc-.*-cnt-fired')
\r
105 flg_nonzero_score = False
\r
106 flg_acc_events = False
\r
107 flg_fastest_lap = False
\r
109 for events in players:
\r
110 if is_real_player(events) and played_in_game(events):
\r
111 for (key,value) in events.items():
\r
112 if key == 'scoreboard-score' and value != 0:
\r
113 flg_nonzero_score = True
\r
115 flg_acc_events = True
\r
116 if key == 'scoreboard-fastest':
\r
117 flg_fastest_lap = True
\r
119 if gametype == 'cts':
\r
120 return not flg_fastest_lap
\r
122 return not (flg_nonzero_score and flg_acc_events)
\r
125 def get_remote_addr(request):
\r
126 """Get the Xonotic server's IP address"""
\r
127 if 'X-Forwarded-For' in request.headers:
\r
128 return request.headers['X-Forwarded-For']
\r
130 return request.remote_addr
\r
133 def is_supported_gametype(gametype, version):
\r
134 """Whether a gametype is supported or not"""
\r
135 is_supported = False
\r
137 # if the type can be supported, but with version constraints, uncomment
\r
138 # here and add the restriction for a specific version below
\r
139 supported_game_types = (
\r
157 if gametype in supported_game_types:
\r
158 is_supported = True
\r
160 is_supported = False
\r
162 # some game types were buggy before revisions, thus this additional filter
\r
163 if gametype == 'ca' and version <= 5:
\r
164 is_supported = False
\r
166 return is_supported
\r
169 def do_precondition_checks(request, game_meta, raw_players):
\r
170 """Precondition checks for ALL gametypes.
\r
171 These do not require a database connection."""
\r
172 if not has_required_metadata(game_meta):
\r
173 log.debug("ERROR: Required game meta missing")
\r
174 raise pyramid.httpexceptions.HTTPUnprocessableEntity("Missing game meta")
\r
177 version = int(game_meta['V'])
\r
179 log.debug("ERROR: Required game meta invalid")
\r
180 raise pyramid.httpexceptions.HTTPUnprocessableEntity("Invalid game meta")
\r
182 if not is_supported_gametype(game_meta['G'], version):
\r
183 log.debug("ERROR: Unsupported gametype")
\r
184 raise pyramid.httpexceptions.HTTPOk("OK")
\r
186 if not has_minimum_real_players(request.registry.settings, raw_players):
\r
187 log.debug("ERROR: Not enough real players")
\r
188 raise pyramid.httpexceptions.HTTPOk("OK")
\r
190 if is_blank_game(game_meta['G'], raw_players):
\r
191 log.debug("ERROR: Blank game")
\r
192 raise pyramid.httpexceptions.HTTPOk("OK")
\r
195 def is_real_player(events):
\r
197 Determines if a given set of events correspond with a non-bot
\r
199 if not events['P'].startswith('bot'):
\r
205 def played_in_game(events):
\r
207 Determines if a given set of player events correspond with a player who
\r
208 played in the game (matches 1 and scoreboardvalid 1)
\r
210 if 'matches' in events and 'scoreboardvalid' in events:
\r
216 def num_real_players(player_events):
\r
218 Returns the number of real players (those who played
\r
219 and are on the scoreboard).
\r
223 for events in player_events:
\r
224 if is_real_player(events) and played_in_game(events):
\r
227 return real_players
\r
230 def has_minimum_real_players(settings, player_events):
\r
232 Determines if the collection of player events has enough "real" players
\r
233 to store in the database. The minimum setting comes from the config file
\r
234 under the setting xonstat.minimum_real_players.
\r
236 flg_has_min_real_players = True
\r
239 minimum_required_players = int(
\r
240 settings['xonstat.minimum_required_players'])
\r
242 minimum_required_players = 2
\r
244 real_players = num_real_players(player_events)
\r
246 if real_players < minimum_required_players:
\r
247 flg_has_min_real_players = False
\r
249 return flg_has_min_real_players
\r
252 def has_required_metadata(metadata):
\r
254 Determines if a give set of metadata has enough data to create a game,
\r
255 server, and map with.
\r
257 flg_has_req_metadata = True
\r
259 if 'T' not in metadata or\
\r
260 'G' not in metadata or\
\r
261 'M' not in metadata or\
\r
262 'I' not in metadata or\
\r
263 'S' not in metadata:
\r
264 flg_has_req_metadata = False
\r
266 return flg_has_req_metadata
\r
269 def should_do_weapon_stats(game_type_cd):
\r
270 """True of the game type should record weapon stats. False otherwise."""
\r
271 if game_type_cd in 'cts':
\r
277 def should_do_elos(game_type_cd):
\r
278 """True of the game type should process Elos. False otherwise."""
\r
279 elo_game_types = ('duel', 'dm', 'ca', 'ctf', 'tdm', 'ka', 'ft')
\r
281 if game_type_cd in elo_game_types:
\r
287 def register_new_nick(session, player, new_nick):
\r
289 Change the player record's nick to the newly found nick. Store the old
\r
290 nick in the player_nicks table for that player.
\r
292 session - SQLAlchemy database session factory
\r
293 player - player record whose nick is changing
\r
294 new_nick - the new nickname
\r
296 # see if that nick already exists
\r
297 stripped_nick = strip_colors(qfont_decode(player.nick))
\r
299 player_nick = session.query(PlayerNick).filter_by(
\r
300 player_id=player.player_id, stripped_nick=stripped_nick).one()
\r
301 except NoResultFound, e:
\r
302 # player_id/stripped_nick not found, create one
\r
303 # but we don't store "Anonymous Player #N"
\r
304 if not re.search('^Anonymous Player #\d+$', player.nick):
\r
305 player_nick = PlayerNick()
\r
306 player_nick.player_id = player.player_id
\r
307 player_nick.stripped_nick = stripped_nick
\r
308 player_nick.nick = player.nick
\r
309 session.add(player_nick)
\r
311 # We change to the new nick regardless
\r
312 player.nick = new_nick
\r
313 player.stripped_nick = strip_colors(qfont_decode(new_nick))
\r
314 session.add(player)
\r
317 def update_fastest_cap(session, player_id, game_id, map_id, captime):
\r
319 Check the fastest cap time for the player and map. If there isn't
\r
320 one, insert one. If there is, check if the passed time is faster.
\r
323 # we don't record fastest cap times for bots or anonymous players
\r
327 # see if a cap entry exists already
\r
328 # then check to see if the new captime is faster
\r
330 cur_fastest_cap = session.query(PlayerCaptime).filter_by(
\r
331 player_id=player_id, map_id=map_id).one()
\r
333 # current captime is faster, so update
\r
334 if captime < cur_fastest_cap.fastest_cap:
\r
335 cur_fastest_cap.fastest_cap = captime
\r
336 cur_fastest_cap.game_id = game_id
\r
337 cur_fastest_cap.create_dt = datetime.datetime.utcnow()
\r
338 session.add(cur_fastest_cap)
\r
340 except NoResultFound, e:
\r
341 # none exists, so insert
\r
342 cur_fastest_cap = PlayerCaptime(player_id, game_id, map_id, captime)
\r
343 session.add(cur_fastest_cap)
\r
347 def get_or_create_server(session, name, hashkey, ip_addr, revision, port):
\r
349 Find a server by name or create one if not found. Parameters:
\r
351 session - SQLAlchemy database session factory
\r
352 name - server name of the server to be found or created
\r
353 hashkey - server hashkey
\r
362 # finding by hashkey is preferred, but if not we will fall
\r
363 # back to using name only, which can result in dupes
\r
364 if hashkey is not None:
\r
365 servers = session.query(Server).\
\r
366 filter_by(hashkey=hashkey).\
\r
367 order_by(expr.desc(Server.create_dt)).limit(1).all()
\r
369 if len(servers) > 0:
\r
370 server = servers[0]
\r
371 log.debug("Found existing server {0} by hashkey ({1})".format(
\r
372 server.server_id, server.hashkey))
\r
374 servers = session.query(Server).\
\r
375 filter_by(name=name).\
\r
376 order_by(expr.desc(Server.create_dt)).limit(1).all()
\r
378 if len(servers) > 0:
\r
379 server = servers[0]
\r
380 log.debug("Found existing server {0} by name".format(server.server_id))
\r
382 # still haven't found a server by hashkey or name, so we need to create one
\r
384 server = Server(name=name, hashkey=hashkey)
\r
385 session.add(server)
\r
387 log.debug("Created server {0} with hashkey {1}".format(
\r
388 server.server_id, server.hashkey))
\r
390 # detect changed fields
\r
391 if server.name != name:
\r
393 session.add(server)
\r
395 if server.hashkey != hashkey:
\r
396 server.hashkey = hashkey
\r
397 session.add(server)
\r
399 if server.ip_addr != ip_addr:
\r
400 server.ip_addr = ip_addr
\r
401 session.add(server)
\r
403 if server.port != port:
\r
405 session.add(server)
\r
407 if server.revision != revision:
\r
408 server.revision = revision
\r
409 session.add(server)
\r
414 def get_or_create_map(session=None, name=None):
\r
416 Find a map by name or create one if not found. Parameters:
\r
418 session - SQLAlchemy database session factory
\r
419 name - map name of the map to be found or created
\r
422 # find one by the name, if it exists
\r
423 gmap = session.query(Map).filter_by(name=name).one()
\r
424 log.debug("Found map id {0}: {1}".format(gmap.map_id,
\r
426 except NoResultFound, e:
\r
427 gmap = Map(name=name)
\r
430 log.debug("Created map id {0}: {1}".format(gmap.map_id,
\r
432 except MultipleResultsFound, e:
\r
433 # multiple found, so use the first one but warn
\r
435 gmaps = session.query(Map).filter_by(name=name).order_by(
\r
438 log.debug("Found map id {0}: {1} but found \
\r
439 multiple".format(gmap.map_id, gmap.name))
\r
444 def create_game(session, start_dt, game_type_cd, server_id, map_id,
\r
445 match_id, duration, mod, winner=None):
\r
447 Creates a game. Parameters:
\r
449 session - SQLAlchemy database session factory
\r
450 start_dt - when the game started (datetime object)
\r
451 game_type_cd - the game type of the game being played
\r
452 server_id - server identifier of the server hosting the game
\r
453 map_id - map on which the game was played
\r
454 winner - the team id of the team that won
\r
455 duration - how long the game lasted
\r
456 mod - mods in use during the game
\r
458 seq = Sequence('games_game_id_seq')
\r
459 game_id = session.execute(seq)
\r
460 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
\r
461 server_id=server_id, map_id=map_id, winner=winner)
\r
462 game.match_id = match_id
\r
463 game.mod = mod[:64]
\r
466 game.duration = datetime.timedelta(seconds=int(round(float(duration))))
\r
471 session.query(Game).filter(Game.server_id==server_id).\
\r
472 filter(Game.match_id==match_id).one()
\r
474 log.debug("Error: game with same server and match_id found! Ignoring.")
\r
476 # if a game under the same server and match_id found,
\r
477 # this is a duplicate game and can be ignored
\r
478 raise pyramid.httpexceptions.HTTPOk('OK')
\r
479 except NoResultFound, e:
\r
480 # server_id/match_id combination not found. game is ok to insert
\r
483 log.debug("Created game id {0} on server {1}, map {2} at \
\r
484 {3}".format(game.game_id,
\r
485 server_id, map_id, start_dt))
\r
490 def get_or_create_player(session=None, hashkey=None, nick=None):
\r
492 Finds a player by hashkey or creates a new one (along with a
\r
493 corresponding hashkey entry. Parameters:
\r
495 session - SQLAlchemy database session factory
\r
496 hashkey - hashkey of the player to be found or created
\r
497 nick - nick of the player (in case of a first time create)
\r
500 if re.search('^bot#\d+$', hashkey) or re.search('^bot#\d+#', hashkey):
\r
501 player = session.query(Player).filter_by(player_id=1).one()
\r
502 # if we have an untracked player
\r
503 elif re.search('^player#\d+$', hashkey):
\r
504 player = session.query(Player).filter_by(player_id=2).one()
\r
505 # else it is a tracked player
\r
507 # see if the player is already in the database
\r
508 # if not, create one and the hashkey along with it
\r
510 hk = session.query(Hashkey).filter_by(
\r
511 hashkey=hashkey).one()
\r
512 player = session.query(Player).filter_by(
\r
513 player_id=hk.player_id).one()
\r
514 log.debug("Found existing player {0} with hashkey {1}".format(
\r
515 player.player_id, hashkey))
\r
518 session.add(player)
\r
521 # if nick is given to us, use it. If not, use "Anonymous Player"
\r
522 # with a suffix added for uniqueness.
\r
524 player.nick = nick[:128]
\r
525 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
\r
527 player.nick = "Anonymous Player #{0}".format(player.player_id)
\r
528 player.stripped_nick = player.nick
\r
530 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
\r
532 log.debug("Created player {0} ({2}) with hashkey {1}".format(
\r
533 player.player_id, hashkey, player.nick.encode('utf-8')))
\r
538 def create_default_game_stat(session, game_type_cd):
\r
539 """Creates a blanked-out pgstat record for the given game type"""
\r
541 # this is what we have to do to get partitioned records in - grab the
\r
542 # sequence value first, then insert using the explicit ID (vs autogenerate)
\r
543 seq = Sequence('player_game_stats_player_game_stat_id_seq')
\r
544 pgstat_id = session.execute(seq)
\r
545 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
\r
546 create_dt=datetime.datetime.utcnow())
\r
548 if game_type_cd == 'as':
\r
549 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
\r
551 if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
\r
552 pgstat.kills = pgstat.deaths = pgstat.suicides = 0
\r
554 if game_type_cd == 'cq':
\r
555 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
\r
558 if game_type_cd == 'ctf':
\r
559 pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
\r
560 pgstat.returns = pgstat.carrier_frags = 0
\r
562 if game_type_cd == 'cts':
\r
565 if game_type_cd == 'dom':
\r
566 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
569 if game_type_cd == 'ft':
\r
570 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
\r
572 if game_type_cd == 'ka':
\r
573 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
574 pgstat.carrier_frags = 0
\r
575 pgstat.time = datetime.timedelta(seconds=0)
\r
577 if game_type_cd == 'kh':
\r
578 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
579 pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
\r
580 pgstat.carrier_frags = 0
\r
582 if game_type_cd == 'lms':
\r
583 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
\r
585 if game_type_cd == 'nb':
\r
586 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
\r
589 if game_type_cd == 'rc':
\r
590 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
\r
595 def create_game_stat(session, game_meta, game, server, gmap, player, events):
\r
596 """Game stats handler for all game types"""
\r
598 game_type_cd = game.game_type_cd
\r
600 pgstat = create_default_game_stat(session, game_type_cd)
\r
602 # these fields should be on every pgstat record
\r
603 pgstat.game_id = game.game_id
\r
604 pgstat.player_id = player.player_id
\r
605 pgstat.nick = events.get('n', 'Anonymous Player')[:128]
\r
606 pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
\r
607 pgstat.score = int(round(float(events.get('scoreboard-score', 0))))
\r
608 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
\r
609 pgstat.rank = int(events.get('rank', None))
\r
610 pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
\r
612 if pgstat.nick != player.nick \
\r
613 and player.player_id > 2 \
\r
614 and pgstat.nick != 'Anonymous Player':
\r
615 register_new_nick(session, player, pgstat.nick)
\r
619 # gametype-specific stuff is handled here. if passed to us, we store it
\r
620 for (key,value) in events.items():
\r
621 if key == 'wins': wins = True
\r
622 if key == 't': pgstat.team = int(value)
\r
624 if key == 'scoreboard-drops': pgstat.drops = int(value)
\r
625 if key == 'scoreboard-returns': pgstat.returns = int(value)
\r
626 if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
\r
627 if key == 'scoreboard-pickups': pgstat.pickups = int(value)
\r
628 if key == 'scoreboard-caps': pgstat.captures = int(value)
\r
629 if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
\r
630 if key == 'scoreboard-deaths': pgstat.deaths = int(value)
\r
631 if key == 'scoreboard-kills': pgstat.kills = int(value)
\r
632 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
\r
633 if key == 'scoreboard-objectives': pgstat.collects = int(value)
\r
634 if key == 'scoreboard-captured': pgstat.captures = int(value)
\r
635 if key == 'scoreboard-released': pgstat.drops = int(value)
\r
636 if key == 'scoreboard-fastest':
\r
637 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
\r
638 if key == 'scoreboard-takes': pgstat.pickups = int(value)
\r
639 if key == 'scoreboard-ticks': pgstat.drops = int(value)
\r
640 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
\r
641 if key == 'scoreboard-bctime':
\r
642 pgstat.time = datetime.timedelta(seconds=int(value))
\r
643 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
\r
644 if key == 'scoreboard-losses': pgstat.drops = int(value)
\r
645 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
\r
646 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
\r
647 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
\r
648 if key == 'scoreboard-lives': pgstat.lives = int(value)
\r
649 if key == 'scoreboard-goals': pgstat.captures = int(value)
\r
650 if key == 'scoreboard-faults': pgstat.drops = int(value)
\r
651 if key == 'scoreboard-laps': pgstat.laps = int(value)
\r
653 if key == 'avglatency': pgstat.avg_latency = float(value)
\r
654 if key == 'scoreboard-captime':
\r
655 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
\r
656 if game.game_type_cd == 'ctf':
\r
657 update_fastest_cap(session, player.player_id, game.game_id,
\r
658 gmap.map_id, pgstat.fastest)
\r
660 # there is no "winning team" field, so we have to derive it
\r
661 if wins and pgstat.team is not None and game.winner is None:
\r
662 game.winner = pgstat.team
\r
665 session.add(pgstat)
\r
670 def create_default_team_stat(session, game_type_cd):
\r
671 """Creates a blanked-out teamstat record for the given game type"""
\r
673 # this is what we have to do to get partitioned records in - grab the
\r
674 # sequence value first, then insert using the explicit ID (vs autogenerate)
\r
675 seq = Sequence('team_game_stats_team_game_stat_id_seq')
\r
676 teamstat_id = session.execute(seq)
\r
677 teamstat = TeamGameStat(team_game_stat_id=teamstat_id,
\r
678 create_dt=datetime.datetime.utcnow())
\r
680 # all team game modes have a score, so we'll zero that out always
\r
683 if game_type_cd in 'ca' 'ft' 'lms' 'ka':
\r
684 teamstat.rounds = 0
\r
686 if game_type_cd == 'ctf':
\r
692 def create_team_stat(session, game, events):
\r
693 """Team stats handler for all game types"""
\r
696 teamstat = create_default_team_stat(session, game.game_type_cd)
\r
697 teamstat.game_id = game.game_id
\r
699 # we should have a team ID if we have a 'Q' event
\r
700 if re.match(r'^team#\d+$', events.get('Q', '')):
\r
701 team = int(events.get('Q').replace('team#', ''))
\r
702 teamstat.team = team
\r
704 # gametype-specific stuff is handled here. if passed to us, we store it
\r
705 for (key,value) in events.items():
\r
706 if key == 'scoreboard-score': teamstat.score = int(round(float(value)))
\r
707 if key == 'scoreboard-caps': teamstat.caps = int(value)
\r
708 if key == 'scoreboard-rounds': teamstat.rounds = int(value)
\r
710 session.add(teamstat)
\r
711 except Exception as e:
\r
717 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
\r
718 """Weapon stats handler for all game types"""
\r
721 # Version 1 of stats submissions doubled the data sent.
\r
722 # To counteract this we divide the data by 2 only for
\r
723 # POSTs coming from version 1.
\r
725 version = int(game_meta['V'])
\r
728 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
\r
734 for (key,value) in events.items():
\r
735 matched = re.search("acc-(.*?)-cnt-fired", key)
\r
737 weapon_cd = matched.group(1)
\r
738 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
\r
739 pwstat_id = session.execute(seq)
\r
740 pwstat = PlayerWeaponStat()
\r
741 pwstat.player_weapon_stats_id = pwstat_id
\r
742 pwstat.player_id = player.player_id
\r
743 pwstat.game_id = game.game_id
\r
744 pwstat.player_game_stat_id = pgstat.player_game_stat_id
\r
745 pwstat.weapon_cd = weapon_cd
\r
748 pwstat.nick = events['n']
\r
750 pwstat.nick = events['P']
\r
752 if 'acc-' + weapon_cd + '-cnt-fired' in events:
\r
753 pwstat.fired = int(round(float(
\r
754 events['acc-' + weapon_cd + '-cnt-fired'])))
\r
755 if 'acc-' + weapon_cd + '-fired' in events:
\r
756 pwstat.max = int(round(float(
\r
757 events['acc-' + weapon_cd + '-fired'])))
\r
758 if 'acc-' + weapon_cd + '-cnt-hit' in events:
\r
759 pwstat.hit = int(round(float(
\r
760 events['acc-' + weapon_cd + '-cnt-hit'])))
\r
761 if 'acc-' + weapon_cd + '-hit' in events:
\r
762 pwstat.actual = int(round(float(
\r
763 events['acc-' + weapon_cd + '-hit'])))
\r
764 if 'acc-' + weapon_cd + '-frags' in events:
\r
765 pwstat.frags = int(round(float(
\r
766 events['acc-' + weapon_cd + '-frags'])))
\r
769 pwstat.fired = pwstat.fired/2
\r
770 pwstat.max = pwstat.max/2
\r
771 pwstat.hit = pwstat.hit/2
\r
772 pwstat.actual = pwstat.actual/2
\r
773 pwstat.frags = pwstat.frags/2
\r
775 session.add(pwstat)
\r
776 pwstats.append(pwstat)
\r
781 def create_elos(session, game):
\r
782 """Elo handler for all game types."""
\r
784 process_elos(game, session)
\r
785 except Exception as e:
\r
786 log.debug('Error (non-fatal): elo processing failed.')
\r
789 def submit_stats(request):
\r
791 Entry handler for POST stats submissions.
\r
794 # placeholder for the actual session
\r
797 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
\r
798 "----- END REQUEST BODY -----\n\n")
\r
800 (idfp, status) = verify_request(request)
\r
801 (game_meta, raw_players, raw_teams) = parse_stats_submission(request.body)
\r
802 revision = game_meta.get('R', 'unknown')
\r
803 duration = game_meta.get('D', None)
\r
805 # only players present at the end of the match are eligible for stats
\r
806 raw_players = filter(played_in_game, raw_players)
\r
808 do_precondition_checks(request, game_meta, raw_players)
\r
810 # the "duel" gametype is fake
\r
811 if len(raw_players) == 2 \
\r
812 and num_real_players(raw_players) == 2 \
\r
813 and game_meta['G'] == 'dm':
\r
814 game_meta['G'] = 'duel'
\r
816 #----------------------------------------------------------------------
\r
817 # Actual setup (inserts/updates) below here
\r
818 #----------------------------------------------------------------------
\r
819 session = DBSession()
\r
821 game_type_cd = game_meta['G']
\r
823 # All game types create Game, Server, Map, and Player records
\r
825 server = get_or_create_server(
\r
828 name = game_meta['S'],
\r
829 revision = revision,
\r
830 ip_addr = get_remote_addr(request),
\r
831 port = game_meta.get('U', None))
\r
833 gmap = get_or_create_map(
\r
835 name = game_meta['M'])
\r
837 game = create_game(
\r
839 start_dt = datetime.datetime.utcnow(),
\r
840 server_id = server.server_id,
\r
841 game_type_cd = game_type_cd,
\r
842 map_id = gmap.map_id,
\r
843 match_id = game_meta['I'],
\r
844 duration = duration,
\r
845 mod = game_meta.get('O', None))
\r
847 for events in raw_players:
\r
848 player = get_or_create_player(
\r
850 hashkey = events['P'],
\r
851 nick = events.get('n', None))
\r
853 pgstat = create_game_stat(session, game_meta, game, server,
\r
854 gmap, player, events)
\r
856 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
\r
857 pwstats = create_weapon_stats(session, game_meta, game, player,
\r
860 for events in raw_teams:
\r
862 teamstat = create_team_stat(session, game, events)
\r
863 except Exception as e:
\r
866 if should_do_elos(game_type_cd):
\r
867 create_elos(session, game)
\r
870 log.debug('Success! Stats recorded.')
\r
871 return Response('200 OK')
\r
872 except Exception as e:
\r