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 ... or for NB, in which not all maps have weapons
\r
106 1) a match in which no player made a positive or negative score
\r
108 r = re.compile(r'acc-.*-cnt-fired')
\r
109 flg_nonzero_score = False
\r
110 flg_acc_events = False
\r
111 flg_fastest_lap = False
\r
113 for events in players:
\r
114 if is_real_player(events) and played_in_game(events):
\r
115 for (key,value) in events.items():
\r
116 if key == 'scoreboard-score' and value != 0:
\r
117 flg_nonzero_score = True
\r
119 flg_acc_events = True
\r
120 if key == 'scoreboard-fastest':
\r
121 flg_fastest_lap = True
\r
123 if gametype == 'cts':
\r
124 return not flg_fastest_lap
\r
125 elif gametype == 'nb':
\r
126 return not flg_nonzero_score
\r
128 return not (flg_nonzero_score and flg_acc_events)
\r
131 def get_remote_addr(request):
\r
132 """Get the Xonotic server's IP address"""
\r
133 if 'X-Forwarded-For' in request.headers:
\r
134 return request.headers['X-Forwarded-For']
\r
136 return request.remote_addr
\r
139 def is_supported_gametype(gametype, version):
\r
140 """Whether a gametype is supported or not"""
\r
141 is_supported = False
\r
143 # if the type can be supported, but with version constraints, uncomment
\r
144 # here and add the restriction for a specific version below
\r
145 supported_game_types = (
\r
163 if gametype in supported_game_types:
\r
164 is_supported = True
\r
166 is_supported = False
\r
168 # some game types were buggy before revisions, thus this additional filter
\r
169 if gametype == 'ca' and version <= 5:
\r
170 is_supported = False
\r
172 return is_supported
\r
175 def do_precondition_checks(request, game_meta, raw_players):
\r
176 """Precondition checks for ALL gametypes.
\r
177 These do not require a database connection."""
\r
178 if not has_required_metadata(game_meta):
\r
179 log.debug("ERROR: Required game meta missing")
\r
180 raise pyramid.httpexceptions.HTTPUnprocessableEntity("Missing game meta")
\r
183 version = int(game_meta['V'])
\r
185 log.debug("ERROR: Required game meta invalid")
\r
186 raise pyramid.httpexceptions.HTTPUnprocessableEntity("Invalid game meta")
\r
188 if not is_supported_gametype(game_meta['G'], version):
\r
189 log.debug("ERROR: Unsupported gametype")
\r
190 raise pyramid.httpexceptions.HTTPOk("OK")
\r
192 if not has_minimum_real_players(request.registry.settings, raw_players):
\r
193 log.debug("ERROR: Not enough real players")
\r
194 raise pyramid.httpexceptions.HTTPOk("OK")
\r
196 if is_blank_game(game_meta['G'], raw_players):
\r
197 log.debug("ERROR: Blank game")
\r
198 raise pyramid.httpexceptions.HTTPOk("OK")
\r
201 def is_real_player(events):
\r
203 Determines if a given set of events correspond with a non-bot
\r
205 if not events['P'].startswith('bot'):
\r
211 def played_in_game(events):
\r
213 Determines if a given set of player events correspond with a player who
\r
214 played in the game (matches 1 and scoreboardvalid 1)
\r
216 if 'matches' in events and 'scoreboardvalid' in events:
\r
222 def num_real_players(player_events):
\r
224 Returns the number of real players (those who played
\r
225 and are on the scoreboard).
\r
229 for events in player_events:
\r
230 if is_real_player(events) and played_in_game(events):
\r
233 return real_players
\r
236 def has_minimum_real_players(settings, player_events):
\r
238 Determines if the collection of player events has enough "real" players
\r
239 to store in the database. The minimum setting comes from the config file
\r
240 under the setting xonstat.minimum_real_players.
\r
242 flg_has_min_real_players = True
\r
245 minimum_required_players = int(
\r
246 settings['xonstat.minimum_required_players'])
\r
248 minimum_required_players = 2
\r
250 real_players = num_real_players(player_events)
\r
252 if real_players < minimum_required_players:
\r
253 flg_has_min_real_players = False
\r
255 return flg_has_min_real_players
\r
258 def has_required_metadata(metadata):
\r
260 Determines if a give set of metadata has enough data to create a game,
\r
261 server, and map with.
\r
263 flg_has_req_metadata = True
\r
265 if 'G' not in metadata or\
\r
266 'M' not in metadata or\
\r
267 'I' not in metadata or\
\r
268 'S' not in metadata:
\r
269 flg_has_req_metadata = False
\r
271 return flg_has_req_metadata
\r
274 def should_do_weapon_stats(game_type_cd):
\r
275 """True of the game type should record weapon stats. False otherwise."""
\r
276 if game_type_cd in 'cts':
\r
282 def should_do_elos(game_type_cd):
\r
283 """True of the game type should process Elos. False otherwise."""
\r
284 elo_game_types = ('duel', 'dm', 'ca', 'ctf', 'tdm', 'ka', 'ft')
\r
286 if game_type_cd in elo_game_types:
\r
292 def register_new_nick(session, player, new_nick):
\r
294 Change the player record's nick to the newly found nick. Store the old
\r
295 nick in the player_nicks table for that player.
\r
297 session - SQLAlchemy database session factory
\r
298 player - player record whose nick is changing
\r
299 new_nick - the new nickname
\r
301 # see if that nick already exists
\r
302 stripped_nick = strip_colors(qfont_decode(player.nick))
\r
304 player_nick = session.query(PlayerNick).filter_by(
\r
305 player_id=player.player_id, stripped_nick=stripped_nick).one()
\r
306 except NoResultFound, e:
\r
307 # player_id/stripped_nick not found, create one
\r
308 # but we don't store "Anonymous Player #N"
\r
309 if not re.search('^Anonymous Player #\d+$', player.nick):
\r
310 player_nick = PlayerNick()
\r
311 player_nick.player_id = player.player_id
\r
312 player_nick.stripped_nick = stripped_nick
\r
313 player_nick.nick = player.nick
\r
314 session.add(player_nick)
\r
316 # We change to the new nick regardless
\r
317 player.nick = new_nick
\r
318 player.stripped_nick = strip_colors(qfont_decode(new_nick))
\r
319 session.add(player)
\r
322 def update_fastest_cap(session, player_id, game_id, map_id, captime):
\r
324 Check the fastest cap time for the player and map. If there isn't
\r
325 one, insert one. If there is, check if the passed time is faster.
\r
328 # we don't record fastest cap times for bots or anonymous players
\r
332 # see if a cap entry exists already
\r
333 # then check to see if the new captime is faster
\r
335 cur_fastest_cap = session.query(PlayerCaptime).filter_by(
\r
336 player_id=player_id, map_id=map_id).one()
\r
338 # current captime is faster, so update
\r
339 if captime < cur_fastest_cap.fastest_cap:
\r
340 cur_fastest_cap.fastest_cap = captime
\r
341 cur_fastest_cap.game_id = game_id
\r
342 cur_fastest_cap.create_dt = datetime.datetime.utcnow()
\r
343 session.add(cur_fastest_cap)
\r
345 except NoResultFound, e:
\r
346 # none exists, so insert
\r
347 cur_fastest_cap = PlayerCaptime(player_id, game_id, map_id, captime)
\r
348 session.add(cur_fastest_cap)
\r
352 def get_or_create_server(session, name, hashkey, ip_addr, revision, port):
\r
354 Find a server by name or create one if not found. Parameters:
\r
356 session - SQLAlchemy database session factory
\r
357 name - server name of the server to be found or created
\r
358 hashkey - server hashkey
\r
367 # finding by hashkey is preferred, but if not we will fall
\r
368 # back to using name only, which can result in dupes
\r
369 if hashkey is not None:
\r
370 servers = session.query(Server).\
\r
371 filter_by(hashkey=hashkey).\
\r
372 order_by(expr.desc(Server.create_dt)).limit(1).all()
\r
374 if len(servers) > 0:
\r
375 server = servers[0]
\r
376 log.debug("Found existing server {0} by hashkey ({1})".format(
\r
377 server.server_id, server.hashkey))
\r
379 servers = session.query(Server).\
\r
380 filter_by(name=name).\
\r
381 order_by(expr.desc(Server.create_dt)).limit(1).all()
\r
383 if len(servers) > 0:
\r
384 server = servers[0]
\r
385 log.debug("Found existing server {0} by name".format(server.server_id))
\r
387 # still haven't found a server by hashkey or name, so we need to create one
\r
389 server = Server(name=name, hashkey=hashkey)
\r
390 session.add(server)
\r
392 log.debug("Created server {0} with hashkey {1}".format(
\r
393 server.server_id, server.hashkey))
\r
395 # detect changed fields
\r
396 if server.name != name:
\r
398 session.add(server)
\r
400 if server.hashkey != hashkey:
\r
401 server.hashkey = hashkey
\r
402 session.add(server)
\r
404 if server.ip_addr != ip_addr:
\r
405 server.ip_addr = ip_addr
\r
406 session.add(server)
\r
408 if server.port != port:
\r
410 session.add(server)
\r
412 if server.revision != revision:
\r
413 server.revision = revision
\r
414 session.add(server)
\r
419 def get_or_create_map(session=None, name=None):
\r
421 Find a map by name or create one if not found. Parameters:
\r
423 session - SQLAlchemy database session factory
\r
424 name - map name of the map to be found or created
\r
427 # find one by the name, if it exists
\r
428 gmap = session.query(Map).filter_by(name=name).one()
\r
429 log.debug("Found map id {0}: {1}".format(gmap.map_id,
\r
431 except NoResultFound, e:
\r
432 gmap = Map(name=name)
\r
435 log.debug("Created map id {0}: {1}".format(gmap.map_id,
\r
437 except MultipleResultsFound, e:
\r
438 # multiple found, so use the first one but warn
\r
440 gmaps = session.query(Map).filter_by(name=name).order_by(
\r
443 log.debug("Found map id {0}: {1} but found \
\r
444 multiple".format(gmap.map_id, gmap.name))
\r
449 def create_game(session, start_dt, game_type_cd, server_id, map_id,
\r
450 match_id, duration, mod, winner=None):
\r
452 Creates a game. Parameters:
\r
454 session - SQLAlchemy database session factory
\r
455 start_dt - when the game started (datetime object)
\r
456 game_type_cd - the game type of the game being played
\r
457 server_id - server identifier of the server hosting the game
\r
458 map_id - map on which the game was played
\r
459 winner - the team id of the team that won
\r
460 duration - how long the game lasted
\r
461 mod - mods in use during the game
\r
463 seq = Sequence('games_game_id_seq')
\r
464 game_id = session.execute(seq)
\r
465 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
\r
466 server_id=server_id, map_id=map_id, winner=winner)
\r
467 game.match_id = match_id
\r
468 game.mod = mod[:64]
\r
471 game.duration = datetime.timedelta(seconds=int(round(float(duration))))
\r
476 session.query(Game).filter(Game.server_id==server_id).\
\r
477 filter(Game.match_id==match_id).one()
\r
479 log.debug("Error: game with same server and match_id found! Ignoring.")
\r
481 # if a game under the same server and match_id found,
\r
482 # this is a duplicate game and can be ignored
\r
483 raise pyramid.httpexceptions.HTTPOk('OK')
\r
484 except NoResultFound, e:
\r
485 # server_id/match_id combination not found. game is ok to insert
\r
488 log.debug("Created game id {0} on server {1}, map {2} at \
\r
489 {3}".format(game.game_id,
\r
490 server_id, map_id, start_dt))
\r
495 def get_or_create_player(session=None, hashkey=None, nick=None):
\r
497 Finds a player by hashkey or creates a new one (along with a
\r
498 corresponding hashkey entry. Parameters:
\r
500 session - SQLAlchemy database session factory
\r
501 hashkey - hashkey of the player to be found or created
\r
502 nick - nick of the player (in case of a first time create)
\r
505 if re.search('^bot#\d+$', hashkey) or re.search('^bot#\d+#', hashkey):
\r
506 player = session.query(Player).filter_by(player_id=1).one()
\r
507 # if we have an untracked player
\r
508 elif re.search('^player#\d+$', hashkey):
\r
509 player = session.query(Player).filter_by(player_id=2).one()
\r
510 # else it is a tracked player
\r
512 # see if the player is already in the database
\r
513 # if not, create one and the hashkey along with it
\r
515 hk = session.query(Hashkey).filter_by(
\r
516 hashkey=hashkey).one()
\r
517 player = session.query(Player).filter_by(
\r
518 player_id=hk.player_id).one()
\r
519 log.debug("Found existing player {0} with hashkey {1}".format(
\r
520 player.player_id, hashkey))
\r
523 session.add(player)
\r
526 # if nick is given to us, use it. If not, use "Anonymous Player"
\r
527 # with a suffix added for uniqueness.
\r
529 player.nick = nick[:128]
\r
530 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
\r
532 player.nick = "Anonymous Player #{0}".format(player.player_id)
\r
533 player.stripped_nick = player.nick
\r
535 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
\r
537 log.debug("Created player {0} ({2}) with hashkey {1}".format(
\r
538 player.player_id, hashkey, player.nick.encode('utf-8')))
\r
543 def create_default_game_stat(session, game_type_cd):
\r
544 """Creates a blanked-out pgstat record for the given game type"""
\r
546 # this is what we have to do to get partitioned records in - grab the
\r
547 # sequence value first, then insert using the explicit ID (vs autogenerate)
\r
548 seq = Sequence('player_game_stats_player_game_stat_id_seq')
\r
549 pgstat_id = session.execute(seq)
\r
550 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
\r
551 create_dt=datetime.datetime.utcnow())
\r
553 if game_type_cd == 'as':
\r
554 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
\r
556 if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
\r
557 pgstat.kills = pgstat.deaths = pgstat.suicides = 0
\r
559 if game_type_cd == 'cq':
\r
560 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
\r
563 if game_type_cd == 'ctf':
\r
564 pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
\r
565 pgstat.returns = pgstat.carrier_frags = 0
\r
567 if game_type_cd == 'cts':
\r
570 if game_type_cd == 'dom':
\r
571 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
574 if game_type_cd == 'ft':
\r
575 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
\r
577 if game_type_cd == 'ka':
\r
578 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
579 pgstat.carrier_frags = 0
\r
580 pgstat.time = datetime.timedelta(seconds=0)
\r
582 if game_type_cd == 'kh':
\r
583 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
584 pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
\r
585 pgstat.carrier_frags = 0
\r
587 if game_type_cd == 'lms':
\r
588 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
\r
590 if game_type_cd == 'nb':
\r
591 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
\r
594 if game_type_cd == 'rc':
\r
595 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
\r
600 def create_game_stat(session, game_meta, game, server, gmap, player, events):
\r
601 """Game stats handler for all game types"""
\r
603 game_type_cd = game.game_type_cd
\r
605 pgstat = create_default_game_stat(session, game_type_cd)
\r
607 # these fields should be on every pgstat record
\r
608 pgstat.game_id = game.game_id
\r
609 pgstat.player_id = player.player_id
\r
610 pgstat.nick = events.get('n', 'Anonymous Player')[:128]
\r
611 pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
\r
612 pgstat.score = int(round(float(events.get('scoreboard-score', 0))))
\r
613 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
\r
614 pgstat.rank = int(events.get('rank', None))
\r
615 pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
\r
617 if pgstat.nick != player.nick \
\r
618 and player.player_id > 2 \
\r
619 and pgstat.nick != 'Anonymous Player':
\r
620 register_new_nick(session, player, pgstat.nick)
\r
624 # gametype-specific stuff is handled here. if passed to us, we store it
\r
625 for (key,value) in events.items():
\r
626 if key == 'wins': wins = True
\r
627 if key == 't': pgstat.team = int(value)
\r
629 if key == 'scoreboard-drops': pgstat.drops = int(value)
\r
630 if key == 'scoreboard-returns': pgstat.returns = int(value)
\r
631 if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
\r
632 if key == 'scoreboard-pickups': pgstat.pickups = int(value)
\r
633 if key == 'scoreboard-caps': pgstat.captures = int(value)
\r
634 if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
\r
635 if key == 'scoreboard-deaths': pgstat.deaths = int(value)
\r
636 if key == 'scoreboard-kills': pgstat.kills = int(value)
\r
637 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
\r
638 if key == 'scoreboard-objectives': pgstat.collects = int(value)
\r
639 if key == 'scoreboard-captured': pgstat.captures = int(value)
\r
640 if key == 'scoreboard-released': pgstat.drops = int(value)
\r
641 if key == 'scoreboard-fastest':
\r
642 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
\r
643 if key == 'scoreboard-takes': pgstat.pickups = int(value)
\r
644 if key == 'scoreboard-ticks': pgstat.drops = int(value)
\r
645 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
\r
646 if key == 'scoreboard-bctime':
\r
647 pgstat.time = datetime.timedelta(seconds=int(value))
\r
648 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
\r
649 if key == 'scoreboard-losses': pgstat.drops = int(value)
\r
650 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
\r
651 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
\r
652 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
\r
653 if key == 'scoreboard-lives': pgstat.lives = int(value)
\r
654 if key == 'scoreboard-goals': pgstat.captures = int(value)
\r
655 if key == 'scoreboard-faults': pgstat.drops = int(value)
\r
656 if key == 'scoreboard-laps': pgstat.laps = int(value)
\r
658 if key == 'avglatency': pgstat.avg_latency = float(value)
\r
659 if key == 'scoreboard-captime':
\r
660 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
\r
661 if game.game_type_cd == 'ctf':
\r
662 update_fastest_cap(session, player.player_id, game.game_id,
\r
663 gmap.map_id, pgstat.fastest)
\r
665 # there is no "winning team" field, so we have to derive it
\r
666 if wins and pgstat.team is not None and game.winner is None:
\r
667 game.winner = pgstat.team
\r
670 session.add(pgstat)
\r
675 def create_anticheats(session, pgstat, game, player, events):
\r
676 """Anticheats handler for all game types"""
\r
680 # all anticheat events are prefixed by "anticheat"
\r
681 for (key,value) in events.items():
\r
682 if key.startswith("anticheat"):
\r
684 ac = PlayerGameAnticheat(
\r
690 anticheats.append(ac)
\r
692 except Exception as e:
\r
693 log.debug("Could not parse value for key %s. Ignoring." % key)
\r
698 def create_default_team_stat(session, game_type_cd):
\r
699 """Creates a blanked-out teamstat record for the given game type"""
\r
701 # this is what we have to do to get partitioned records in - grab the
\r
702 # sequence value first, then insert using the explicit ID (vs autogenerate)
\r
703 seq = Sequence('team_game_stats_team_game_stat_id_seq')
\r
704 teamstat_id = session.execute(seq)
\r
705 teamstat = TeamGameStat(team_game_stat_id=teamstat_id,
\r
706 create_dt=datetime.datetime.utcnow())
\r
708 # all team game modes have a score, so we'll zero that out always
\r
711 if game_type_cd in 'ca' 'ft' 'lms' 'ka':
\r
712 teamstat.rounds = 0
\r
714 if game_type_cd == 'ctf':
\r
720 def create_team_stat(session, game, events):
\r
721 """Team stats handler for all game types"""
\r
724 teamstat = create_default_team_stat(session, game.game_type_cd)
\r
725 teamstat.game_id = game.game_id
\r
727 # we should have a team ID if we have a 'Q' event
\r
728 if re.match(r'^team#\d+$', events.get('Q', '')):
\r
729 team = int(events.get('Q').replace('team#', ''))
\r
730 teamstat.team = team
\r
732 # gametype-specific stuff is handled here. if passed to us, we store it
\r
733 for (key,value) in events.items():
\r
734 if key == 'scoreboard-score': teamstat.score = int(round(float(value)))
\r
735 if key == 'scoreboard-caps': teamstat.caps = int(value)
\r
736 if key == 'scoreboard-rounds': teamstat.rounds = int(value)
\r
738 session.add(teamstat)
\r
739 except Exception as e:
\r
745 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
\r
746 """Weapon stats handler for all game types"""
\r
749 # Version 1 of stats submissions doubled the data sent.
\r
750 # To counteract this we divide the data by 2 only for
\r
751 # POSTs coming from version 1.
\r
753 version = int(game_meta['V'])
\r
756 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
\r
762 for (key,value) in events.items():
\r
763 matched = re.search("acc-(.*?)-cnt-fired", key)
\r
765 weapon_cd = matched.group(1)
\r
767 # Weapon names changed for 0.8. We'll convert the old
\r
768 # ones to use the new scheme as well.
\r
769 mapped_weapon_cd = weapon_map.get(weapon_cd, weapon_cd)
\r
771 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
\r
772 pwstat_id = session.execute(seq)
\r
773 pwstat = PlayerWeaponStat()
\r
774 pwstat.player_weapon_stats_id = pwstat_id
\r
775 pwstat.player_id = player.player_id
\r
776 pwstat.game_id = game.game_id
\r
777 pwstat.player_game_stat_id = pgstat.player_game_stat_id
\r
778 pwstat.weapon_cd = mapped_weapon_cd
\r
781 pwstat.nick = events['n']
\r
783 pwstat.nick = events['P']
\r
785 if 'acc-' + weapon_cd + '-cnt-fired' in events:
\r
786 pwstat.fired = int(round(float(
\r
787 events['acc-' + weapon_cd + '-cnt-fired'])))
\r
788 if 'acc-' + weapon_cd + '-fired' in events:
\r
789 pwstat.max = int(round(float(
\r
790 events['acc-' + weapon_cd + '-fired'])))
\r
791 if 'acc-' + weapon_cd + '-cnt-hit' in events:
\r
792 pwstat.hit = int(round(float(
\r
793 events['acc-' + weapon_cd + '-cnt-hit'])))
\r
794 if 'acc-' + weapon_cd + '-hit' in events:
\r
795 pwstat.actual = int(round(float(
\r
796 events['acc-' + weapon_cd + '-hit'])))
\r
797 if 'acc-' + weapon_cd + '-frags' in events:
\r
798 pwstat.frags = int(round(float(
\r
799 events['acc-' + weapon_cd + '-frags'])))
\r
802 pwstat.fired = pwstat.fired/2
\r
803 pwstat.max = pwstat.max/2
\r
804 pwstat.hit = pwstat.hit/2
\r
805 pwstat.actual = pwstat.actual/2
\r
806 pwstat.frags = pwstat.frags/2
\r
808 session.add(pwstat)
\r
809 pwstats.append(pwstat)
\r
814 def create_elos(session, game):
\r
815 """Elo handler for all game types."""
\r
817 process_elos(game, session)
\r
818 except Exception as e:
\r
819 log.debug('Error (non-fatal): elo processing failed.')
\r
822 def submit_stats(request):
\r
824 Entry handler for POST stats submissions.
\r
827 # placeholder for the actual session
\r
830 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
\r
831 "----- END REQUEST BODY -----\n\n")
\r
833 (idfp, status) = verify_request(request)
\r
834 (game_meta, raw_players, raw_teams) = parse_stats_submission(request.body)
\r
835 revision = game_meta.get('R', 'unknown')
\r
836 duration = game_meta.get('D', None)
\r
838 # only players present at the end of the match are eligible for stats
\r
839 raw_players = filter(played_in_game, raw_players)
\r
841 do_precondition_checks(request, game_meta, raw_players)
\r
843 # the "duel" gametype is fake
\r
844 if len(raw_players) == 2 \
\r
845 and num_real_players(raw_players) == 2 \
\r
846 and game_meta['G'] == 'dm':
\r
847 game_meta['G'] = 'duel'
\r
849 #----------------------------------------------------------------------
\r
850 # Actual setup (inserts/updates) below here
\r
851 #----------------------------------------------------------------------
\r
852 session = DBSession()
\r
854 game_type_cd = game_meta['G']
\r
856 # All game types create Game, Server, Map, and Player records
\r
858 server = get_or_create_server(
\r
861 name = game_meta['S'],
\r
862 revision = revision,
\r
863 ip_addr = get_remote_addr(request),
\r
864 port = game_meta.get('U', None))
\r
866 gmap = get_or_create_map(
\r
868 name = game_meta['M'])
\r
870 game = create_game(
\r
872 start_dt = datetime.datetime.utcnow(),
\r
873 server_id = server.server_id,
\r
874 game_type_cd = game_type_cd,
\r
875 map_id = gmap.map_id,
\r
876 match_id = game_meta['I'],
\r
877 duration = duration,
\r
878 mod = game_meta.get('O', None))
\r
880 # keep track of the players we've seen
\r
882 for events in raw_players:
\r
883 player = get_or_create_player(
\r
885 hashkey = events['P'],
\r
886 nick = events.get('n', None))
\r
888 pgstat = create_game_stat(session, game_meta, game, server,
\r
889 gmap, player, events)
\r
891 if player.player_id > 1:
\r
892 anticheats = create_anticheats(session, pgstat, game, player,
\r
894 player_ids.append(player.player_id)
\r
896 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
\r
897 pwstats = create_weapon_stats(session, game_meta, game, player,
\r
900 # store them on games for easy access
\r
901 game.players = player_ids
\r
903 for events in raw_teams:
\r
905 teamstat = create_team_stat(session, game, events)
\r
906 except Exception as e:
\r
909 if should_do_elos(game_type_cd):
\r
910 create_elos(session, game)
\r
913 log.debug('Success! Stats recorded.')
\r
914 return Response('200 OK')
\r
915 except Exception as e:
\r