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, weapon_map
\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 'G' not in metadata or\
\r
260 'M' not in metadata or\
\r
261 'I' not in metadata or\
\r
262 'S' not in metadata:
\r
263 flg_has_req_metadata = False
\r
265 return flg_has_req_metadata
\r
268 def should_do_weapon_stats(game_type_cd):
\r
269 """True of the game type should record weapon stats. False otherwise."""
\r
270 if game_type_cd in 'cts':
\r
276 def should_do_elos(game_type_cd):
\r
277 """True of the game type should process Elos. False otherwise."""
\r
278 elo_game_types = ('duel', 'dm', 'ca', 'ctf', 'tdm', 'ka', 'ft')
\r
280 if game_type_cd in elo_game_types:
\r
286 def register_new_nick(session, player, new_nick):
\r
288 Change the player record's nick to the newly found nick. Store the old
\r
289 nick in the player_nicks table for that player.
\r
291 session - SQLAlchemy database session factory
\r
292 player - player record whose nick is changing
\r
293 new_nick - the new nickname
\r
295 # see if that nick already exists
\r
296 stripped_nick = strip_colors(qfont_decode(player.nick))
\r
298 player_nick = session.query(PlayerNick).filter_by(
\r
299 player_id=player.player_id, stripped_nick=stripped_nick).one()
\r
300 except NoResultFound, e:
\r
301 # player_id/stripped_nick not found, create one
\r
302 # but we don't store "Anonymous Player #N"
\r
303 if not re.search('^Anonymous Player #\d+$', player.nick):
\r
304 player_nick = PlayerNick()
\r
305 player_nick.player_id = player.player_id
\r
306 player_nick.stripped_nick = stripped_nick
\r
307 player_nick.nick = player.nick
\r
308 session.add(player_nick)
\r
310 # We change to the new nick regardless
\r
311 player.nick = new_nick
\r
312 player.stripped_nick = strip_colors(qfont_decode(new_nick))
\r
313 session.add(player)
\r
316 def update_fastest_cap(session, player_id, game_id, map_id, captime):
\r
318 Check the fastest cap time for the player and map. If there isn't
\r
319 one, insert one. If there is, check if the passed time is faster.
\r
322 # we don't record fastest cap times for bots or anonymous players
\r
326 # see if a cap entry exists already
\r
327 # then check to see if the new captime is faster
\r
329 cur_fastest_cap = session.query(PlayerCaptime).filter_by(
\r
330 player_id=player_id, map_id=map_id).one()
\r
332 # current captime is faster, so update
\r
333 if captime < cur_fastest_cap.fastest_cap:
\r
334 cur_fastest_cap.fastest_cap = captime
\r
335 cur_fastest_cap.game_id = game_id
\r
336 cur_fastest_cap.create_dt = datetime.datetime.utcnow()
\r
337 session.add(cur_fastest_cap)
\r
339 except NoResultFound, e:
\r
340 # none exists, so insert
\r
341 cur_fastest_cap = PlayerCaptime(player_id, game_id, map_id, captime)
\r
342 session.add(cur_fastest_cap)
\r
346 def get_or_create_server(session, name, hashkey, ip_addr, revision, port):
\r
348 Find a server by name or create one if not found. Parameters:
\r
350 session - SQLAlchemy database session factory
\r
351 name - server name of the server to be found or created
\r
352 hashkey - server hashkey
\r
361 # finding by hashkey is preferred, but if not we will fall
\r
362 # back to using name only, which can result in dupes
\r
363 if hashkey is not None:
\r
364 servers = session.query(Server).\
\r
365 filter_by(hashkey=hashkey).\
\r
366 order_by(expr.desc(Server.create_dt)).limit(1).all()
\r
368 if len(servers) > 0:
\r
369 server = servers[0]
\r
370 log.debug("Found existing server {0} by hashkey ({1})".format(
\r
371 server.server_id, server.hashkey))
\r
373 servers = session.query(Server).\
\r
374 filter_by(name=name).\
\r
375 order_by(expr.desc(Server.create_dt)).limit(1).all()
\r
377 if len(servers) > 0:
\r
378 server = servers[0]
\r
379 log.debug("Found existing server {0} by name".format(server.server_id))
\r
381 # still haven't found a server by hashkey or name, so we need to create one
\r
383 server = Server(name=name, hashkey=hashkey)
\r
384 session.add(server)
\r
386 log.debug("Created server {0} with hashkey {1}".format(
\r
387 server.server_id, server.hashkey))
\r
389 # detect changed fields
\r
390 if server.name != name:
\r
392 session.add(server)
\r
394 if server.hashkey != hashkey:
\r
395 server.hashkey = hashkey
\r
396 session.add(server)
\r
398 if server.ip_addr != ip_addr:
\r
399 server.ip_addr = ip_addr
\r
400 session.add(server)
\r
402 if server.port != port:
\r
404 session.add(server)
\r
406 if server.revision != revision:
\r
407 server.revision = revision
\r
408 session.add(server)
\r
413 def get_or_create_map(session=None, name=None):
\r
415 Find a map by name or create one if not found. Parameters:
\r
417 session - SQLAlchemy database session factory
\r
418 name - map name of the map to be found or created
\r
421 # find one by the name, if it exists
\r
422 gmap = session.query(Map).filter_by(name=name).one()
\r
423 log.debug("Found map id {0}: {1}".format(gmap.map_id,
\r
425 except NoResultFound, e:
\r
426 gmap = Map(name=name)
\r
429 log.debug("Created map id {0}: {1}".format(gmap.map_id,
\r
431 except MultipleResultsFound, e:
\r
432 # multiple found, so use the first one but warn
\r
434 gmaps = session.query(Map).filter_by(name=name).order_by(
\r
437 log.debug("Found map id {0}: {1} but found \
\r
438 multiple".format(gmap.map_id, gmap.name))
\r
443 def create_game(session, start_dt, game_type_cd, server_id, map_id,
\r
444 match_id, duration, mod, winner=None):
\r
446 Creates a game. Parameters:
\r
448 session - SQLAlchemy database session factory
\r
449 start_dt - when the game started (datetime object)
\r
450 game_type_cd - the game type of the game being played
\r
451 server_id - server identifier of the server hosting the game
\r
452 map_id - map on which the game was played
\r
453 winner - the team id of the team that won
\r
454 duration - how long the game lasted
\r
455 mod - mods in use during the game
\r
457 seq = Sequence('games_game_id_seq')
\r
458 game_id = session.execute(seq)
\r
459 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
\r
460 server_id=server_id, map_id=map_id, winner=winner)
\r
461 game.match_id = match_id
\r
462 game.mod = mod[:64]
\r
465 game.duration = datetime.timedelta(seconds=int(round(float(duration))))
\r
470 session.query(Game).filter(Game.server_id==server_id).\
\r
471 filter(Game.match_id==match_id).one()
\r
473 log.debug("Error: game with same server and match_id found! Ignoring.")
\r
475 # if a game under the same server and match_id found,
\r
476 # this is a duplicate game and can be ignored
\r
477 raise pyramid.httpexceptions.HTTPOk('OK')
\r
478 except NoResultFound, e:
\r
479 # server_id/match_id combination not found. game is ok to insert
\r
482 log.debug("Created game id {0} on server {1}, map {2} at \
\r
483 {3}".format(game.game_id,
\r
484 server_id, map_id, start_dt))
\r
489 def get_or_create_player(session=None, hashkey=None, nick=None):
\r
491 Finds a player by hashkey or creates a new one (along with a
\r
492 corresponding hashkey entry. Parameters:
\r
494 session - SQLAlchemy database session factory
\r
495 hashkey - hashkey of the player to be found or created
\r
496 nick - nick of the player (in case of a first time create)
\r
499 if re.search('^bot#\d+$', hashkey) or re.search('^bot#\d+#', hashkey):
\r
500 player = session.query(Player).filter_by(player_id=1).one()
\r
501 # if we have an untracked player
\r
502 elif re.search('^player#\d+$', hashkey):
\r
503 player = session.query(Player).filter_by(player_id=2).one()
\r
504 # else it is a tracked player
\r
506 # see if the player is already in the database
\r
507 # if not, create one and the hashkey along with it
\r
509 hk = session.query(Hashkey).filter_by(
\r
510 hashkey=hashkey).one()
\r
511 player = session.query(Player).filter_by(
\r
512 player_id=hk.player_id).one()
\r
513 log.debug("Found existing player {0} with hashkey {1}".format(
\r
514 player.player_id, hashkey))
\r
517 session.add(player)
\r
520 # if nick is given to us, use it. If not, use "Anonymous Player"
\r
521 # with a suffix added for uniqueness.
\r
523 player.nick = nick[:128]
\r
524 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
\r
526 player.nick = "Anonymous Player #{0}".format(player.player_id)
\r
527 player.stripped_nick = player.nick
\r
529 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
\r
531 log.debug("Created player {0} ({2}) with hashkey {1}".format(
\r
532 player.player_id, hashkey, player.nick.encode('utf-8')))
\r
537 def create_default_game_stat(session, game_type_cd):
\r
538 """Creates a blanked-out pgstat record for the given game type"""
\r
540 # this is what we have to do to get partitioned records in - grab the
\r
541 # sequence value first, then insert using the explicit ID (vs autogenerate)
\r
542 seq = Sequence('player_game_stats_player_game_stat_id_seq')
\r
543 pgstat_id = session.execute(seq)
\r
544 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
\r
545 create_dt=datetime.datetime.utcnow())
\r
547 if game_type_cd == 'as':
\r
548 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
\r
550 if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
\r
551 pgstat.kills = pgstat.deaths = pgstat.suicides = 0
\r
553 if game_type_cd == 'cq':
\r
554 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
\r
557 if game_type_cd == 'ctf':
\r
558 pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
\r
559 pgstat.returns = pgstat.carrier_frags = 0
\r
561 if game_type_cd == 'cts':
\r
564 if game_type_cd == 'dom':
\r
565 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
568 if game_type_cd == 'ft':
\r
569 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
\r
571 if game_type_cd == 'ka':
\r
572 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
573 pgstat.carrier_frags = 0
\r
574 pgstat.time = datetime.timedelta(seconds=0)
\r
576 if game_type_cd == 'kh':
\r
577 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
578 pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
\r
579 pgstat.carrier_frags = 0
\r
581 if game_type_cd == 'lms':
\r
582 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
\r
584 if game_type_cd == 'nb':
\r
585 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
\r
588 if game_type_cd == 'rc':
\r
589 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
\r
594 def create_game_stat(session, game_meta, game, server, gmap, player, events):
\r
595 """Game stats handler for all game types"""
\r
597 game_type_cd = game.game_type_cd
\r
599 pgstat = create_default_game_stat(session, game_type_cd)
\r
601 # these fields should be on every pgstat record
\r
602 pgstat.game_id = game.game_id
\r
603 pgstat.player_id = player.player_id
\r
604 pgstat.nick = events.get('n', 'Anonymous Player')[:128]
\r
605 pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
\r
606 pgstat.score = int(round(float(events.get('scoreboard-score', 0))))
\r
607 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
\r
608 pgstat.rank = int(events.get('rank', None))
\r
609 pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
\r
611 if pgstat.nick != player.nick \
\r
612 and player.player_id > 2 \
\r
613 and pgstat.nick != 'Anonymous Player':
\r
614 register_new_nick(session, player, pgstat.nick)
\r
618 # gametype-specific stuff is handled here. if passed to us, we store it
\r
619 for (key,value) in events.items():
\r
620 if key == 'wins': wins = True
\r
621 if key == 't': pgstat.team = int(value)
\r
623 if key == 'scoreboard-drops': pgstat.drops = int(value)
\r
624 if key == 'scoreboard-returns': pgstat.returns = int(value)
\r
625 if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
\r
626 if key == 'scoreboard-pickups': pgstat.pickups = int(value)
\r
627 if key == 'scoreboard-caps': pgstat.captures = int(value)
\r
628 if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
\r
629 if key == 'scoreboard-deaths': pgstat.deaths = int(value)
\r
630 if key == 'scoreboard-kills': pgstat.kills = int(value)
\r
631 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
\r
632 if key == 'scoreboard-objectives': pgstat.collects = int(value)
\r
633 if key == 'scoreboard-captured': pgstat.captures = int(value)
\r
634 if key == 'scoreboard-released': pgstat.drops = int(value)
\r
635 if key == 'scoreboard-fastest':
\r
636 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
\r
637 if key == 'scoreboard-takes': pgstat.pickups = int(value)
\r
638 if key == 'scoreboard-ticks': pgstat.drops = int(value)
\r
639 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
\r
640 if key == 'scoreboard-bctime':
\r
641 pgstat.time = datetime.timedelta(seconds=int(value))
\r
642 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
\r
643 if key == 'scoreboard-losses': pgstat.drops = int(value)
\r
644 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
\r
645 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
\r
646 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
\r
647 if key == 'scoreboard-lives': pgstat.lives = int(value)
\r
648 if key == 'scoreboard-goals': pgstat.captures = int(value)
\r
649 if key == 'scoreboard-faults': pgstat.drops = int(value)
\r
650 if key == 'scoreboard-laps': pgstat.laps = int(value)
\r
652 if key == 'avglatency': pgstat.avg_latency = float(value)
\r
653 if key == 'scoreboard-captime':
\r
654 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
\r
655 if game.game_type_cd == 'ctf':
\r
656 update_fastest_cap(session, player.player_id, game.game_id,
\r
657 gmap.map_id, pgstat.fastest)
\r
659 # there is no "winning team" field, so we have to derive it
\r
660 if wins and pgstat.team is not None and game.winner is None:
\r
661 game.winner = pgstat.team
\r
664 session.add(pgstat)
\r
669 def create_anticheats(session, pgstat, game, player, events):
\r
670 """Anticheats handler for all game types"""
\r
674 # all anticheat events are prefixed by "anticheat"
\r
675 for (key,value) in events.items():
\r
676 if key.startswith("anticheat"):
\r
678 ac = PlayerGameAnticheat(
\r
684 anticheats.append(ac)
\r
686 except Exception as e:
\r
687 log.debug("Could not parse value for key %s. Ignoring." % key)
\r
692 def create_default_team_stat(session, game_type_cd):
\r
693 """Creates a blanked-out teamstat record for the given game type"""
\r
695 # this is what we have to do to get partitioned records in - grab the
\r
696 # sequence value first, then insert using the explicit ID (vs autogenerate)
\r
697 seq = Sequence('team_game_stats_team_game_stat_id_seq')
\r
698 teamstat_id = session.execute(seq)
\r
699 teamstat = TeamGameStat(team_game_stat_id=teamstat_id,
\r
700 create_dt=datetime.datetime.utcnow())
\r
702 # all team game modes have a score, so we'll zero that out always
\r
705 if game_type_cd in 'ca' 'ft' 'lms' 'ka':
\r
706 teamstat.rounds = 0
\r
708 if game_type_cd == 'ctf':
\r
714 def create_team_stat(session, game, events):
\r
715 """Team stats handler for all game types"""
\r
718 teamstat = create_default_team_stat(session, game.game_type_cd)
\r
719 teamstat.game_id = game.game_id
\r
721 # we should have a team ID if we have a 'Q' event
\r
722 if re.match(r'^team#\d+$', events.get('Q', '')):
\r
723 team = int(events.get('Q').replace('team#', ''))
\r
724 teamstat.team = team
\r
726 # gametype-specific stuff is handled here. if passed to us, we store it
\r
727 for (key,value) in events.items():
\r
728 if key == 'scoreboard-score': teamstat.score = int(round(float(value)))
\r
729 if key == 'scoreboard-caps': teamstat.caps = int(value)
\r
730 if key == 'scoreboard-rounds': teamstat.rounds = int(value)
\r
732 session.add(teamstat)
\r
733 except Exception as e:
\r
739 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
\r
740 """Weapon stats handler for all game types"""
\r
743 # Version 1 of stats submissions doubled the data sent.
\r
744 # To counteract this we divide the data by 2 only for
\r
745 # POSTs coming from version 1.
\r
747 version = int(game_meta['V'])
\r
750 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
\r
756 for (key,value) in events.items():
\r
757 matched = re.search("acc-(.*?)-cnt-fired", key)
\r
759 weapon_cd = matched.group(1)
\r
761 # Weapon names changed for 0.8. We'll convert the old
\r
762 # ones to use the new scheme as well.
\r
763 mapped_weapon_cd = weapon_map.get(weapon_cd, weapon_cd)
\r
765 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
\r
766 pwstat_id = session.execute(seq)
\r
767 pwstat = PlayerWeaponStat()
\r
768 pwstat.player_weapon_stats_id = pwstat_id
\r
769 pwstat.player_id = player.player_id
\r
770 pwstat.game_id = game.game_id
\r
771 pwstat.player_game_stat_id = pgstat.player_game_stat_id
\r
772 pwstat.weapon_cd = mapped_weapon_cd
\r
775 pwstat.nick = events['n']
\r
777 pwstat.nick = events['P']
\r
779 if 'acc-' + weapon_cd + '-cnt-fired' in events:
\r
780 pwstat.fired = int(round(float(
\r
781 events['acc-' + weapon_cd + '-cnt-fired'])))
\r
782 if 'acc-' + weapon_cd + '-fired' in events:
\r
783 pwstat.max = int(round(float(
\r
784 events['acc-' + weapon_cd + '-fired'])))
\r
785 if 'acc-' + weapon_cd + '-cnt-hit' in events:
\r
786 pwstat.hit = int(round(float(
\r
787 events['acc-' + weapon_cd + '-cnt-hit'])))
\r
788 if 'acc-' + weapon_cd + '-hit' in events:
\r
789 pwstat.actual = int(round(float(
\r
790 events['acc-' + weapon_cd + '-hit'])))
\r
791 if 'acc-' + weapon_cd + '-frags' in events:
\r
792 pwstat.frags = int(round(float(
\r
793 events['acc-' + weapon_cd + '-frags'])))
\r
796 pwstat.fired = pwstat.fired/2
\r
797 pwstat.max = pwstat.max/2
\r
798 pwstat.hit = pwstat.hit/2
\r
799 pwstat.actual = pwstat.actual/2
\r
800 pwstat.frags = pwstat.frags/2
\r
802 session.add(pwstat)
\r
803 pwstats.append(pwstat)
\r
808 def create_elos(session, game):
\r
809 """Elo handler for all game types."""
\r
811 process_elos(game, session)
\r
812 except Exception as e:
\r
813 log.debug('Error (non-fatal): elo processing failed.')
\r
816 def submit_stats(request):
\r
818 Entry handler for POST stats submissions.
\r
821 # placeholder for the actual session
\r
824 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
\r
825 "----- END REQUEST BODY -----\n\n")
\r
827 (idfp, status) = verify_request(request)
\r
828 (game_meta, raw_players, raw_teams) = parse_stats_submission(request.body)
\r
829 revision = game_meta.get('R', 'unknown')
\r
830 duration = game_meta.get('D', None)
\r
832 # only players present at the end of the match are eligible for stats
\r
833 raw_players = filter(played_in_game, raw_players)
\r
835 do_precondition_checks(request, game_meta, raw_players)
\r
837 # the "duel" gametype is fake
\r
838 if len(raw_players) == 2 \
\r
839 and num_real_players(raw_players) == 2 \
\r
840 and game_meta['G'] == 'dm':
\r
841 game_meta['G'] = 'duel'
\r
843 #----------------------------------------------------------------------
\r
844 # Actual setup (inserts/updates) below here
\r
845 #----------------------------------------------------------------------
\r
846 session = DBSession()
\r
848 game_type_cd = game_meta['G']
\r
850 # All game types create Game, Server, Map, and Player records
\r
852 server = get_or_create_server(
\r
855 name = game_meta['S'],
\r
856 revision = revision,
\r
857 ip_addr = get_remote_addr(request),
\r
858 port = game_meta.get('U', None))
\r
860 gmap = get_or_create_map(
\r
862 name = game_meta['M'])
\r
864 game = create_game(
\r
866 start_dt = datetime.datetime.utcnow(),
\r
867 server_id = server.server_id,
\r
868 game_type_cd = game_type_cd,
\r
869 map_id = gmap.map_id,
\r
870 match_id = game_meta['I'],
\r
871 duration = duration,
\r
872 mod = game_meta.get('O', None))
\r
874 # keep track of the players we've seen
\r
876 for events in raw_players:
\r
877 player = get_or_create_player(
\r
879 hashkey = events['P'],
\r
880 nick = events.get('n', None))
\r
882 pgstat = create_game_stat(session, game_meta, game, server,
\r
883 gmap, player, events)
\r
885 if player.player_id > 1:
\r
886 anticheats = create_anticheats(session, pgstat, game, player,
\r
888 player_ids.append(player.player_id)
\r
890 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
\r
891 pwstats = create_weapon_stats(session, game_meta, game, player,
\r
894 # store them on games for easy access
\r
895 game.players = player_ids
\r
897 for events in raw_teams:
\r
899 teamstat = create_team_stat(session, game, events)
\r
900 except Exception as e:
\r
903 if should_do_elos(game_type_cd):
\r
904 create_elos(session, game)
\r
907 log.debug('Success! Stats recorded.')
\r
908 return Response('200 OK')
\r
909 except Exception as e:
\r