4 import pyramid.httpexceptions
\r
7 from pyramid.response import Response
\r
8 from sqlalchemy import Sequence
\r
9 from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
\r
10 from xonstat.d0_blind_id import d0_blind_id_verify
\r
11 from xonstat.elo import process_elos
\r
12 from xonstat.models import *
\r
13 from xonstat.util import strip_colors, qfont_decode
\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
28 for line in body.split('\n'):
\r
30 (key, value) = line.strip().split(' ', 1)
\r
32 # Server (S) and Nick (n) fields can have international characters.
\r
34 value = unicode(value, 'utf-8')
\r
36 if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W' 'I' 'D' 'O':
\r
37 game_meta[key] = value
\r
40 # if we were working on a player record already, append
\r
41 # it and work on a new one (only set team info)
\r
43 players.append(events)
\r
49 (subkey, subvalue) = value.split(' ', 1)
\r
50 events[subkey] = subvalue
\r
56 # no key/value pair - move on to the next line
\r
59 # add the last player we were working on
\r
61 players.append(events)
\r
63 return (game_meta, players)
\r
66 def is_blank_game(players):
\r
67 """Determine if this is a blank game or not. A blank game is either:
\r
69 1) a match that ended in the warmup stage, where accuracy events are not
\r
72 2) a match in which no player made a positive or negative score AND was
\r
75 r = re.compile(r'acc-.*-cnt-fired')
\r
76 flg_nonzero_score = False
\r
77 flg_acc_events = False
\r
79 for events in players:
\r
80 if is_real_player(events) and played_in_game(events):
\r
81 for (key,value) in events.items():
\r
82 if key == 'scoreboard-score' and value != 0:
\r
83 flg_nonzero_score = True
\r
85 flg_acc_events = True
\r
87 return not (flg_nonzero_score and flg_acc_events)
\r
90 def get_remote_addr(request):
\r
91 """Get the Xonotic server's IP address"""
\r
92 if 'X-Forwarded-For' in request.headers:
\r
93 return request.headers['X-Forwarded-For']
\r
95 return request.remote_addr
\r
98 def is_supported_gametype(gametype, version):
\r
99 """Whether a gametype is supported or not"""
\r
100 is_supported = False
\r
102 # if the type can be supported, but with version constraints, uncomment
\r
103 # here and add the restriction for a specific version below
\r
104 supported_game_types = (
\r
121 if gametype in supported_game_types:
\r
122 is_supported = True
\r
124 is_supported = False
\r
126 # some game types were buggy before revisions, thus this additional filter
\r
127 if gametype == 'ca' and version <= 5:
\r
128 is_supported = False
\r
130 return is_supported
\r
133 def verify_request(request):
\r
134 """Verify requests using the d0_blind_id library"""
\r
136 # first determine if we should be verifying or not
\r
137 val_verify_requests = request.registry.settings.get('xonstat.verify_requests', 'true')
\r
138 if val_verify_requests == "true":
\r
139 flg_verify_requests = True
\r
141 flg_verify_requests = False
\r
144 (idfp, status) = d0_blind_id_verify(
\r
145 sig=request.headers['X-D0-Blind-Id-Detached-Signature'],
\r
147 postdata=request.body)
\r
149 log.debug('\nidfp: {0}\nstatus: {1}'.format(idfp, status))
\r
154 if flg_verify_requests and not idfp:
\r
155 log.debug("ERROR: Unverified request")
\r
156 raise pyramid.httpexceptions.HTTPUnauthorized("Unverified request")
\r
158 return (idfp, status)
\r
161 def do_precondition_checks(request, game_meta, raw_players):
\r
162 """Precondition checks for ALL gametypes.
\r
163 These do not require a database connection."""
\r
164 if not has_required_metadata(game_meta):
\r
165 log.debug("ERROR: Required game meta missing")
\r
166 raise pyramid.httpexceptions.HTTPUnprocessableEntity("Missing game meta")
\r
169 version = int(game_meta['V'])
\r
171 log.debug("ERROR: Required game meta invalid")
\r
172 raise pyramid.httpexceptions.HTTPUnprocessableEntity("Invalid game meta")
\r
174 if not is_supported_gametype(game_meta['G'], version):
\r
175 log.debug("ERROR: Unsupported gametype")
\r
176 raise pyramid.httpexceptions.HTTPOk("OK")
\r
178 if not has_minimum_real_players(request.registry.settings, raw_players):
\r
179 log.debug("ERROR: Not enough real players")
\r
180 raise pyramid.httpexceptions.HTTPOk("OK")
\r
182 if is_blank_game(raw_players):
\r
183 log.debug("ERROR: Blank game")
\r
184 raise pyramid.httpexceptions.HTTPOk("OK")
\r
187 def is_real_player(events):
\r
189 Determines if a given set of events correspond with a non-bot
\r
191 if not events['P'].startswith('bot'):
\r
197 def played_in_game(events):
\r
199 Determines if a given set of player events correspond with a player who
\r
200 played in the game (matches 1 and scoreboardvalid 1)
\r
202 if 'matches' in events and 'scoreboardvalid' in events:
\r
208 def num_real_players(player_events):
\r
210 Returns the number of real players (those who played
\r
211 and are on the scoreboard).
\r
215 for events in player_events:
\r
216 if is_real_player(events) and played_in_game(events):
\r
219 return real_players
\r
222 def has_minimum_real_players(settings, player_events):
\r
224 Determines if the collection of player events has enough "real" players
\r
225 to store in the database. The minimum setting comes from the config file
\r
226 under the setting xonstat.minimum_real_players.
\r
228 flg_has_min_real_players = True
\r
231 minimum_required_players = int(
\r
232 settings['xonstat.minimum_required_players'])
\r
234 minimum_required_players = 2
\r
236 real_players = num_real_players(player_events)
\r
238 if real_players < minimum_required_players:
\r
239 flg_has_min_real_players = False
\r
241 return flg_has_min_real_players
\r
244 def has_required_metadata(metadata):
\r
246 Determines if a give set of metadata has enough data to create a game,
\r
247 server, and map with.
\r
249 flg_has_req_metadata = True
\r
251 if 'T' not in metadata or\
\r
252 'G' not in metadata or\
\r
253 'M' not in metadata or\
\r
254 'I' not in metadata or\
\r
255 'S' not in metadata:
\r
256 flg_has_req_metadata = False
\r
258 return flg_has_req_metadata
\r
261 def should_do_weapon_stats(game_type_cd):
\r
262 """True of the game type should record weapon stats. False otherwise."""
\r
263 if game_type_cd in 'cts':
\r
269 def should_do_elos(game_type_cd):
\r
270 """True of the game type should process Elos. False otherwise."""
\r
271 elo_game_types = ('duel', 'dm', 'ca', 'ctf', 'tdm', 'kh',
\r
272 'ka', 'ft', 'freezetag')
\r
274 if game_type_cd in elo_game_types:
\r
280 def register_new_nick(session, player, new_nick):
\r
282 Change the player record's nick to the newly found nick. Store the old
\r
283 nick in the player_nicks table for that player.
\r
285 session - SQLAlchemy database session factory
\r
286 player - player record whose nick is changing
\r
287 new_nick - the new nickname
\r
289 # see if that nick already exists
\r
290 stripped_nick = strip_colors(qfont_decode(player.nick))
\r
292 player_nick = session.query(PlayerNick).filter_by(
\r
293 player_id=player.player_id, stripped_nick=stripped_nick).one()
\r
294 except NoResultFound, e:
\r
295 # player_id/stripped_nick not found, create one
\r
296 # but we don't store "Anonymous Player #N"
\r
297 if not re.search('^Anonymous Player #\d+$', player.nick):
\r
298 player_nick = PlayerNick()
\r
299 player_nick.player_id = player.player_id
\r
300 player_nick.stripped_nick = stripped_nick
\r
301 player_nick.nick = player.nick
\r
302 session.add(player_nick)
\r
304 # We change to the new nick regardless
\r
305 player.nick = new_nick
\r
306 player.stripped_nick = strip_colors(qfont_decode(new_nick))
\r
307 session.add(player)
\r
310 def update_fastest_cap(session, player_id, game_id, map_id, captime):
\r
312 Check the fastest cap time for the player and map. If there isn't
\r
313 one, insert one. If there is, check if the passed time is faster.
\r
316 # we don't record fastest cap times for bots or anonymous players
\r
320 # see if a cap entry exists already
\r
321 # then check to see if the new captime is faster
\r
323 cur_fastest_cap = session.query(PlayerCaptime).filter_by(
\r
324 player_id=player_id, map_id=map_id).one()
\r
326 # current captime is faster, so update
\r
327 if captime < cur_fastest_cap.fastest_cap:
\r
328 cur_fastest_cap.fastest_cap = captime
\r
329 cur_fastest_cap.game_id = game_id
\r
330 cur_fastest_cap.create_dt = datetime.datetime.utcnow()
\r
331 session.add(cur_fastest_cap)
\r
333 except NoResultFound, e:
\r
334 # none exists, so insert
\r
335 cur_fastest_cap = PlayerCaptime(player_id, game_id, map_id, captime)
\r
336 session.add(cur_fastest_cap)
\r
340 def get_or_create_server(session=None, name=None, hashkey=None, ip_addr=None,
\r
343 Find a server by name or create one if not found. Parameters:
\r
345 session - SQLAlchemy database session factory
\r
346 name - server name of the server to be found or created
\r
347 hashkey - server hashkey
\r
350 # find one by that name, if it exists
\r
351 server = session.query(Server).filter_by(name=name).one()
\r
353 # store new hashkey
\r
354 if server.hashkey != hashkey:
\r
355 server.hashkey = hashkey
\r
356 session.add(server)
\r
358 # store new IP address
\r
359 if server.ip_addr != ip_addr:
\r
360 server.ip_addr = ip_addr
\r
361 session.add(server)
\r
363 # store new revision
\r
364 if server.revision != revision:
\r
365 server.revision = revision
\r
366 session.add(server)
\r
368 log.debug("Found existing server {0}".format(server.server_id))
\r
370 except MultipleResultsFound, e:
\r
371 # multiple found, so also filter by hashkey
\r
372 server = session.query(Server).filter_by(name=name).\
\r
373 filter_by(hashkey=hashkey).one()
\r
374 log.debug("Found existing server {0}".format(server.server_id))
\r
376 except NoResultFound, e:
\r
377 # not found, create one
\r
378 server = Server(name=name, hashkey=hashkey)
\r
379 session.add(server)
\r
381 log.debug("Created server {0} with hashkey {1}".format(
\r
382 server.server_id, server.hashkey))
\r
387 def get_or_create_map(session=None, name=None):
\r
389 Find a map by name or create one if not found. Parameters:
\r
391 session - SQLAlchemy database session factory
\r
392 name - map name of the map to be found or created
\r
395 # find one by the name, if it exists
\r
396 gmap = session.query(Map).filter_by(name=name).one()
\r
397 log.debug("Found map id {0}: {1}".format(gmap.map_id,
\r
399 except NoResultFound, e:
\r
400 gmap = Map(name=name)
\r
403 log.debug("Created map id {0}: {1}".format(gmap.map_id,
\r
405 except MultipleResultsFound, e:
\r
406 # multiple found, so use the first one but warn
\r
408 gmaps = session.query(Map).filter_by(name=name).order_by(
\r
411 log.debug("Found map id {0}: {1} but found \
\r
412 multiple".format(gmap.map_id, gmap.name))
\r
417 def create_game(session=None, start_dt=None, game_type_cd=None,
\r
418 server_id=None, map_id=None, winner=None, match_id=None,
\r
421 Creates a game. Parameters:
\r
423 session - SQLAlchemy database session factory
\r
424 start_dt - when the game started (datetime object)
\r
425 game_type_cd - the game type of the game being played
\r
426 server_id - server identifier of the server hosting the game
\r
427 map_id - map on which the game was played
\r
428 winner - the team id of the team that won
\r
430 seq = Sequence('games_game_id_seq')
\r
431 game_id = session.execute(seq)
\r
432 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
\r
433 server_id=server_id, map_id=map_id, winner=winner)
\r
434 game.match_id = match_id
\r
437 game.duration = datetime.timedelta(seconds=int(round(float(duration))))
\r
442 session.query(Game).filter(Game.server_id==server_id).\
\r
443 filter(Game.match_id==match_id).one()
\r
445 log.debug("Error: game with same server and match_id found! Ignoring.")
\r
447 # if a game under the same server and match_id found,
\r
448 # this is a duplicate game and can be ignored
\r
449 raise pyramid.httpexceptions.HTTPOk('OK')
\r
450 except NoResultFound, e:
\r
451 # server_id/match_id combination not found. game is ok to insert
\r
454 log.debug("Created game id {0} on server {1}, map {2} at \
\r
455 {3}".format(game.game_id,
\r
456 server_id, map_id, start_dt))
\r
461 def get_or_create_player(session=None, hashkey=None, nick=None):
\r
463 Finds a player by hashkey or creates a new one (along with a
\r
464 corresponding hashkey entry. Parameters:
\r
466 session - SQLAlchemy database session factory
\r
467 hashkey - hashkey of the player to be found or created
\r
468 nick - nick of the player (in case of a first time create)
\r
471 if re.search('^bot#\d+$', hashkey) or re.search('^bot#\d+#', hashkey):
\r
472 player = session.query(Player).filter_by(player_id=1).one()
\r
473 # if we have an untracked player
\r
474 elif re.search('^player#\d+$', hashkey):
\r
475 player = session.query(Player).filter_by(player_id=2).one()
\r
476 # else it is a tracked player
\r
478 # see if the player is already in the database
\r
479 # if not, create one and the hashkey along with it
\r
481 hk = session.query(Hashkey).filter_by(
\r
482 hashkey=hashkey).one()
\r
483 player = session.query(Player).filter_by(
\r
484 player_id=hk.player_id).one()
\r
485 log.debug("Found existing player {0} with hashkey {1}".format(
\r
486 player.player_id, hashkey))
\r
489 session.add(player)
\r
492 # if nick is given to us, use it. If not, use "Anonymous Player"
\r
493 # with a suffix added for uniqueness.
\r
495 player.nick = nick[:128]
\r
496 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
\r
498 player.nick = "Anonymous Player #{0}".format(player.player_id)
\r
499 player.stripped_nick = player.nick
\r
501 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
\r
503 log.debug("Created player {0} ({2}) with hashkey {1}".format(
\r
504 player.player_id, hashkey, player.nick.encode('utf-8')))
\r
509 def create_default_game_stat(session, game_type_cd):
\r
510 """Creates a blanked-out pgstat record for the given game type"""
\r
512 # this is what we have to do to get partitioned records in - grab the
\r
513 # sequence value first, then insert using the explicit ID (vs autogenerate)
\r
514 seq = Sequence('player_game_stats_player_game_stat_id_seq')
\r
515 pgstat_id = session.execute(seq)
\r
516 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
\r
517 create_dt=datetime.datetime.utcnow())
\r
519 if game_type_cd == 'as':
\r
520 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
\r
522 if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
\r
523 pgstat.kills = pgstat.deaths = pgstat.suicides = 0
\r
525 if game_type_cd == 'cq':
\r
526 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
\r
529 if game_type_cd == 'ctf':
\r
530 pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
\r
531 pgstat.returns = pgstat.carrier_frags = 0
\r
533 if game_type_cd == 'cts':
\r
536 if game_type_cd == 'dom':
\r
537 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
540 if game_type_cd == 'ft':
\r
541 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
\r
543 if game_type_cd == 'ka':
\r
544 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
545 pgstat.carrier_frags = 0
\r
546 pgstat.time = datetime.timedelta(seconds=0)
\r
548 if game_type_cd == 'kh':
\r
549 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
\r
550 pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
\r
551 pgstat.carrier_frags = 0
\r
553 if game_type_cd == 'lms':
\r
554 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
\r
556 if game_type_cd == 'nb':
\r
557 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
\r
560 if game_type_cd == 'rc':
\r
561 pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
\r
566 def create_game_stat(session, game_meta, game, server, gmap, player, events):
\r
567 """Game stats handler for all game types"""
\r
569 pgstat = create_default_game_stat(session, game.game_type_cd)
\r
571 # these fields should be on every pgstat record
\r
572 pgstat.game_id = game.game_id
\r
573 pgstat.player_id = player.player_id
\r
574 pgstat.nick = events.get('n', 'Anonymous Player')[:128]
\r
575 pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
\r
576 pgstat.score = int(round(float(events.get('scoreboard-score', 0))))
\r
577 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
\r
578 pgstat.rank = int(events.get('rank', None))
\r
579 pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
\r
581 if pgstat.nick != player.nick \
\r
582 and player.player_id > 2 \
\r
583 and pgstat.nick != 'Anonymous Player':
\r
584 register_new_nick(session, player, pgstat.nick)
\r
588 # gametype-specific stuff is handled here. if passed to us, we store it
\r
589 for (key,value) in events.items():
\r
590 if key == 'wins': wins = True
\r
591 if key == 't': pgstat.team = int(value)
\r
593 if key == 'scoreboard-drops': pgstat.drops = int(value)
\r
594 if key == 'scoreboard-returns': pgstat.returns = int(value)
\r
595 if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
\r
596 if key == 'scoreboard-pickups': pgstat.pickups = int(value)
\r
597 if key == 'scoreboard-caps': pgstat.captures = int(value)
\r
598 if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
\r
599 if key == 'scoreboard-deaths': pgstat.deaths = int(value)
\r
600 if key == 'scoreboard-kills': pgstat.kills = int(value)
\r
601 if key == 'scoreboard-suicides': pgstat.suicides = int(value)
\r
602 if key == 'scoreboard-objectives': pgstat.collects = int(value)
\r
603 if key == 'scoreboard-captured': pgstat.captures = int(value)
\r
604 if key == 'scoreboard-released': pgstat.drops = int(value)
\r
605 if key == 'scoreboard-fastest':
\r
606 pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
\r
607 if key == 'scoreboard-takes': pgstat.pickups = int(value)
\r
608 if key == 'scoreboard-ticks': pgstat.drops = int(value)
\r
609 if key == 'scoreboard-revivals': pgstat.revivals = int(value)
\r
610 if key == 'scoreboard-bctime':
\r
611 pgstat.time = datetime.timedelta(seconds=int(value))
\r
612 if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
\r
613 if key == 'scoreboard-losses': pgstat.drops = int(value)
\r
614 if key == 'scoreboard-pushes': pgstat.pushes = int(value)
\r
615 if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
\r
616 if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
\r
617 if key == 'scoreboard-lives': pgstat.lives = int(value)
\r
618 if key == 'scoreboard-goals': pgstat.captures = int(value)
\r
619 if key == 'scoreboard-faults': pgstat.drops = int(value)
\r
620 if key == 'scoreboard-laps': pgstat.laps = int(value)
\r
622 if key == 'avglatency': pgstat.avg_latency = float(value)
\r
623 if key == 'scoreboard-captime':
\r
624 pgstat.fastest_cap = datetime.timedelta(seconds=float(value)/100)
\r
625 if game.game_type_cd == 'ctf':
\r
626 update_fastest_cap(session, player.player_id, game.game_id,
\r
627 gmap.map_id, pgstat.fastest_cap)
\r
629 # there is no "winning team" field, so we have to derive it
\r
630 if wins and pgstat.team is not None and game.winner is None:
\r
631 game.winner = pgstat.team
\r
634 session.add(pgstat)
\r
639 def create_weapon_stats(session, game_meta, game, player, pgstat, events):
\r
640 """Weapon stats handler for all game types"""
\r
643 # Version 1 of stats submissions doubled the data sent.
\r
644 # To counteract this we divide the data by 2 only for
\r
645 # POSTs coming from version 1.
\r
647 version = int(game_meta['V'])
\r
650 log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
\r
656 for (key,value) in events.items():
\r
657 matched = re.search("acc-(.*?)-cnt-fired", key)
\r
659 weapon_cd = matched.group(1)
\r
660 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
\r
661 pwstat_id = session.execute(seq)
\r
662 pwstat = PlayerWeaponStat()
\r
663 pwstat.player_weapon_stats_id = pwstat_id
\r
664 pwstat.player_id = player.player_id
\r
665 pwstat.game_id = game.game_id
\r
666 pwstat.player_game_stat_id = pgstat.player_game_stat_id
\r
667 pwstat.weapon_cd = weapon_cd
\r
670 pwstat.nick = events['n']
\r
672 pwstat.nick = events['P']
\r
674 if 'acc-' + weapon_cd + '-cnt-fired' in events:
\r
675 pwstat.fired = int(round(float(
\r
676 events['acc-' + weapon_cd + '-cnt-fired'])))
\r
677 if 'acc-' + weapon_cd + '-fired' in events:
\r
678 pwstat.max = int(round(float(
\r
679 events['acc-' + weapon_cd + '-fired'])))
\r
680 if 'acc-' + weapon_cd + '-cnt-hit' in events:
\r
681 pwstat.hit = int(round(float(
\r
682 events['acc-' + weapon_cd + '-cnt-hit'])))
\r
683 if 'acc-' + weapon_cd + '-hit' in events:
\r
684 pwstat.actual = int(round(float(
\r
685 events['acc-' + weapon_cd + '-hit'])))
\r
686 if 'acc-' + weapon_cd + '-frags' in events:
\r
687 pwstat.frags = int(round(float(
\r
688 events['acc-' + weapon_cd + '-frags'])))
\r
691 pwstat.fired = pwstat.fired/2
\r
692 pwstat.max = pwstat.max/2
\r
693 pwstat.hit = pwstat.hit/2
\r
694 pwstat.actual = pwstat.actual/2
\r
695 pwstat.frags = pwstat.frags/2
\r
697 session.add(pwstat)
\r
698 pwstats.append(pwstat)
\r
703 def create_elos(session, game):
\r
704 """Elo handler for all game types."""
\r
706 process_elos(game, session)
\r
707 except Exception as e:
\r
708 log.debug('Error (non-fatal): elo processing failed.')
\r
711 def submit_stats(request):
\r
713 Entry handler for POST stats submissions.
\r
716 # placeholder for the actual session
\r
719 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
\r
720 "----- END REQUEST BODY -----\n\n")
\r
722 (idfp, status) = verify_request(request)
\r
723 (game_meta, raw_players) = parse_stats_submission(request.body)
\r
724 revision = game_meta.get('R', 'unknown')
\r
725 duration = game_meta.get('D', None)
\r
727 # only players present at the end of the match are eligible for stats
\r
728 raw_players = filter(played_in_game, raw_players)
\r
730 do_precondition_checks(request, game_meta, raw_players)
\r
732 # the "duel" gametype is fake
\r
733 if len(raw_players) == 2 \
\r
734 and num_real_players(raw_players) == 2 \
\r
735 and game_meta['G'] == 'dm':
\r
736 game_meta['G'] = 'duel'
\r
738 #----------------------------------------------------------------------
\r
739 # Actual setup (inserts/updates) below here
\r
740 #----------------------------------------------------------------------
\r
741 session = DBSession()
\r
743 game_type_cd = game_meta['G']
\r
745 # All game types create Game, Server, Map, and Player records
\r
747 server = get_or_create_server(
\r
750 name = game_meta['S'],
\r
751 revision = revision,
\r
752 ip_addr = get_remote_addr(request))
\r
754 gmap = get_or_create_map(
\r
756 name = game_meta['M'])
\r
758 game = create_game(
\r
760 start_dt = datetime.datetime.utcnow(),
\r
761 server_id = server.server_id,
\r
762 game_type_cd = game_type_cd,
\r
763 map_id = gmap.map_id,
\r
764 match_id = game_meta['I'],
\r
765 duration = duration)
\r
767 for events in raw_players:
\r
768 player = get_or_create_player(
\r
770 hashkey = events['P'],
\r
771 nick = events.get('n', None))
\r
773 pgstat = create_game_stat(session, game_meta, game, server,
\r
774 gmap, player, events)
\r
776 if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
\r
777 pwstats = create_weapon_stats(session, game_meta, game, player,
\r
780 if should_do_elos(game_type_cd):
\r
781 create_elos(session, game)
\r
784 log.debug('Success! Stats recorded.')
\r
785 return Response('200 OK')
\r
786 except Exception as e:
\r