]> git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/views/submission.py
71b86cabcb8f54c3dac0b77fc3eafea1b5c90783
[xonotic/xonstat.git] / xonstat / views / submission.py
1 import datetime\r
2 import logging\r
3 import os\r
4 import pyramid.httpexceptions\r
5 import re\r
6 import time\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
14 \r
15 \r
16 log = logging.getLogger(__name__)\r
17 \r
18 \r
19 def parse_stats_submission(body):\r
20     """\r
21     Parses the POST request body for a stats submission\r
22     """\r
23     # storage vars for the request body\r
24     game_meta = {}\r
25     events = {}\r
26     players = []\r
27 \r
28     for line in body.split('\n'):\r
29         try:\r
30             (key, value) = line.strip().split(' ', 1)\r
31 \r
32             # Server (S) and Nick (n) fields can have international characters.\r
33             if key in 'S' 'n':\r
34                 value = unicode(value, 'utf-8')\r
35 \r
36             if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W' 'I' 'D' 'O':\r
37                 game_meta[key] = value\r
38 \r
39             if key == 'P':\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
42                 if len(events) > 0:\r
43                     players.append(events)\r
44                     events = {}\r
45 \r
46                 events[key] = value\r
47 \r
48             if key == 'e':\r
49                 (subkey, subvalue) = value.split(' ', 1)\r
50                 events[subkey] = subvalue\r
51             if key == 'n':\r
52                 events[key] = value\r
53             if key == 't':\r
54                 events[key] = value\r
55         except:\r
56             # no key/value pair - move on to the next line\r
57             pass\r
58 \r
59     # add the last player we were working on\r
60     if len(events) > 0:\r
61         players.append(events)\r
62 \r
63     return (game_meta, players)\r
64 \r
65 \r
66 def is_blank_game(players):\r
67     """Determine if this is a blank game or not. A blank game is either:\r
68 \r
69     1) a match that ended in the warmup stage, where accuracy events are not\r
70     present\r
71 \r
72     2) a match in which no player made a positive or negative score AND was\r
73     on the scoreboard\r
74     """\r
75     r = re.compile(r'acc-.*-cnt-fired')\r
76     flg_nonzero_score = False\r
77     flg_acc_events = False\r
78 \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
84                 if r.search(key):\r
85                     flg_acc_events = True\r
86 \r
87     return not (flg_nonzero_score and flg_acc_events)\r
88 \r
89 \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
94     else:\r
95         return request.remote_addr\r
96 \r
97 \r
98 def is_supported_gametype(gametype):\r
99     """Whether a gametype is supported or not"""\r
100     supported_game_types = ('duel', 'dm', 'ctf', 'tdm', 'kh',\r
101             'ka', 'ft', 'freezetag', 'nb', 'nexball')\r
102 \r
103     if gametype in supported_game_types:\r
104         return True\r
105     else:\r
106         return False\r
107 \r
108 \r
109 def verify_request(request):\r
110     """Verify requests using the d0_blind_id library"""\r
111 \r
112     # first determine if we should be verifying or not\r
113     val_verify_requests = request.registry.settings.get('xonstat.verify_requests', 'true')\r
114     if val_verify_requests == "true":\r
115         flg_verify_requests = True\r
116     else:\r
117         flg_verify_requests = False\r
118 \r
119     try:\r
120         (idfp, status) = d0_blind_id_verify(\r
121                 sig=request.headers['X-D0-Blind-Id-Detached-Signature'],\r
122                 querystring='',\r
123                 postdata=request.body)\r
124 \r
125         log.debug('\nidfp: {0}\nstatus: {1}'.format(idfp, status))\r
126     except:\r
127         idfp = None\r
128         status = None\r
129 \r
130     if flg_verify_requests and not idfp:\r
131         log.debug("ERROR: Unverified request")\r
132         raise pyramid.httpexceptions.HTTPUnauthorized("Unverified request")\r
133 \r
134     return (idfp, status)\r
135 \r
136 \r
137 def do_precondition_checks(request, game_meta, raw_players):\r
138     """Precondition checks for ALL gametypes.\r
139        These do not require a database connection."""\r
140     if not is_supported_gametype(game_meta['G']):\r
141         log.debug("ERROR: Unsupported gametype")\r
142         raise pyramid.httpexceptions.HTTPOk("OK")\r
143 \r
144     if not has_required_metadata(game_meta):\r
145         log.debug("ERROR: Required game meta missing")\r
146         raise pyramid.httpexceptions.HTTPUnprocessableEntity("Missing game meta")\r
147 \r
148     if not has_minimum_real_players(request.registry.settings, raw_players):\r
149         log.debug("ERROR: Not enough real players")\r
150         raise pyramid.httpexceptions.HTTPOk("OK")\r
151 \r
152     if is_blank_game(raw_players):\r
153         log.debug("ERROR: Blank game")\r
154         raise pyramid.httpexceptions.HTTPOk("OK")\r
155 \r
156 \r
157 def is_real_player(events):\r
158     """\r
159     Determines if a given set of events correspond with a non-bot\r
160     """\r
161     if not events['P'].startswith('bot'):\r
162         return True\r
163     else:\r
164         return False\r
165 \r
166 \r
167 def played_in_game(events):\r
168     """\r
169     Determines if a given set of player events correspond with a player who\r
170     played in the game (matches 1 and scoreboardvalid 1)\r
171     """\r
172     if 'matches' in events and 'scoreboardvalid' in events:\r
173         return True\r
174     else:\r
175         return False\r
176 \r
177 \r
178 def num_real_players(player_events):\r
179     """\r
180     Returns the number of real players (those who played \r
181     and are on the scoreboard).\r
182     """\r
183     real_players = 0\r
184 \r
185     for events in player_events:\r
186         if is_real_player(events) and played_in_game(events):\r
187             real_players += 1\r
188 \r
189     return real_players\r
190 \r
191 \r
192 def has_minimum_real_players(settings, player_events):\r
193     """\r
194     Determines if the collection of player events has enough "real" players\r
195     to store in the database. The minimum setting comes from the config file\r
196     under the setting xonstat.minimum_real_players.\r
197     """\r
198     flg_has_min_real_players = True\r
199 \r
200     try:\r
201         minimum_required_players = int(\r
202                 settings['xonstat.minimum_required_players'])\r
203     except:\r
204         minimum_required_players = 2\r
205 \r
206     real_players = num_real_players(player_events)\r
207 \r
208     if real_players < minimum_required_players:\r
209         flg_has_min_real_players = False\r
210 \r
211     return flg_has_min_real_players\r
212 \r
213 \r
214 def has_required_metadata(metadata):\r
215     """\r
216     Determines if a give set of metadata has enough data to create a game,\r
217     server, and map with.\r
218     """\r
219     flg_has_req_metadata = True\r
220 \r
221     if 'T' not in metadata or\\r
222         'G' not in metadata or\\r
223         'M' not in metadata or\\r
224         'I' not in metadata or\\r
225         'S' not in metadata:\r
226             flg_has_req_metadata = False\r
227 \r
228     return flg_has_req_metadata\r
229 \r
230 \r
231 def register_new_nick(session, player, new_nick):\r
232     """\r
233     Change the player record's nick to the newly found nick. Store the old\r
234     nick in the player_nicks table for that player.\r
235 \r
236     session - SQLAlchemy database session factory\r
237     player - player record whose nick is changing\r
238     new_nick - the new nickname\r
239     """\r
240     # see if that nick already exists\r
241     stripped_nick = strip_colors(qfont_decode(player.nick))\r
242     try:\r
243         player_nick = session.query(PlayerNick).filter_by(\r
244             player_id=player.player_id, stripped_nick=stripped_nick).one()\r
245     except NoResultFound, e:\r
246         # player_id/stripped_nick not found, create one\r
247         # but we don't store "Anonymous Player #N"\r
248         if not re.search('^Anonymous Player #\d+$', player.nick):\r
249             player_nick = PlayerNick()\r
250             player_nick.player_id = player.player_id\r
251             player_nick.stripped_nick = stripped_nick\r
252             player_nick.nick = player.nick\r
253             session.add(player_nick)\r
254 \r
255     # We change to the new nick regardless\r
256     player.nick = new_nick\r
257     player.stripped_nick = strip_colors(qfont_decode(new_nick))\r
258     session.add(player)\r
259 \r
260 \r
261 def update_fastest_cap(session, player_id, game_id,  map_id, captime):\r
262     """\r
263     Check the fastest cap time for the player and map. If there isn't\r
264     one, insert one. If there is, check if the passed time is faster.\r
265     If so, update!\r
266     """\r
267     # we don't record fastest cap times for bots or anonymous players\r
268     if player_id <= 2:\r
269         return\r
270 \r
271     # see if a cap entry exists already\r
272     # then check to see if the new captime is faster\r
273     try:\r
274         cur_fastest_cap = session.query(PlayerCaptime).filter_by(\r
275             player_id=player_id, map_id=map_id).one()\r
276 \r
277         # current captime is faster, so update\r
278         if captime < cur_fastest_cap.fastest_cap:\r
279             cur_fastest_cap.fastest_cap = captime\r
280             cur_fastest_cap.game_id = game_id\r
281             cur_fastest_cap.create_dt = datetime.datetime.utcnow()\r
282             session.add(cur_fastest_cap)\r
283 \r
284     except NoResultFound, e:\r
285         # none exists, so insert\r
286         cur_fastest_cap = PlayerCaptime(player_id, game_id, map_id, captime)\r
287         session.add(cur_fastest_cap)\r
288         session.flush()\r
289 \r
290 \r
291 def get_or_create_server(session=None, name=None, hashkey=None, ip_addr=None,\r
292         revision=None):\r
293     """\r
294     Find a server by name or create one if not found. Parameters:\r
295 \r
296     session - SQLAlchemy database session factory\r
297     name - server name of the server to be found or created\r
298     hashkey - server hashkey\r
299     """\r
300     try:\r
301         # find one by that name, if it exists\r
302         server = session.query(Server).filter_by(name=name).one()\r
303 \r
304         # store new hashkey\r
305         if server.hashkey != hashkey:\r
306             server.hashkey = hashkey\r
307             session.add(server)\r
308 \r
309         # store new IP address\r
310         if server.ip_addr != ip_addr:\r
311             server.ip_addr = ip_addr\r
312             session.add(server)\r
313 \r
314         # store new revision\r
315         if server.revision != revision:\r
316             server.revision = revision\r
317             session.add(server)\r
318 \r
319         log.debug("Found existing server {0}".format(server.server_id))\r
320 \r
321     except MultipleResultsFound, e:\r
322         # multiple found, so also filter by hashkey\r
323         server = session.query(Server).filter_by(name=name).\\r
324                 filter_by(hashkey=hashkey).one()\r
325         log.debug("Found existing server {0}".format(server.server_id))\r
326 \r
327     except NoResultFound, e:\r
328         # not found, create one\r
329         server = Server(name=name, hashkey=hashkey)\r
330         session.add(server)\r
331         session.flush()\r
332         log.debug("Created server {0} with hashkey {1}".format(\r
333             server.server_id, server.hashkey))\r
334 \r
335     return server\r
336 \r
337 \r
338 def get_or_create_map(session=None, name=None):\r
339     """\r
340     Find a map by name or create one if not found. Parameters:\r
341 \r
342     session - SQLAlchemy database session factory\r
343     name - map name of the map to be found or created\r
344     """\r
345     try:\r
346         # find one by the name, if it exists\r
347         gmap = session.query(Map).filter_by(name=name).one()\r
348         log.debug("Found map id {0}: {1}".format(gmap.map_id, \r
349             gmap.name))\r
350     except NoResultFound, e:\r
351         gmap = Map(name=name)\r
352         session.add(gmap)\r
353         session.flush()\r
354         log.debug("Created map id {0}: {1}".format(gmap.map_id,\r
355             gmap.name))\r
356     except MultipleResultsFound, e:\r
357         # multiple found, so use the first one but warn\r
358         log.debug(e)\r
359         gmaps = session.query(Map).filter_by(name=name).order_by(\r
360                 Map.map_id).all()\r
361         gmap = gmaps[0]\r
362         log.debug("Found map id {0}: {1} but found \\r
363                 multiple".format(gmap.map_id, gmap.name))\r
364 \r
365     return gmap\r
366 \r
367 \r
368 def create_game(session=None, start_dt=None, game_type_cd=None, \r
369         server_id=None, map_id=None, winner=None, match_id=None,\r
370         duration=None):\r
371     """\r
372     Creates a game. Parameters:\r
373 \r
374     session - SQLAlchemy database session factory\r
375     start_dt - when the game started (datetime object)\r
376     game_type_cd - the game type of the game being played\r
377     server_id - server identifier of the server hosting the game\r
378     map_id - map on which the game was played\r
379     winner - the team id of the team that won\r
380     """\r
381     seq = Sequence('games_game_id_seq')\r
382     game_id = session.execute(seq)\r
383     game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,\r
384                 server_id=server_id, map_id=map_id, winner=winner)\r
385     game.match_id = match_id\r
386 \r
387     try:\r
388         game.duration = datetime.timedelta(seconds=int(round(float(duration))))\r
389     except:\r
390         pass\r
391 \r
392     try:\r
393         session.query(Game).filter(Game.server_id==server_id).\\r
394                 filter(Game.match_id==match_id).one()\r
395 \r
396         log.debug("Error: game with same server and match_id found! Ignoring.")\r
397 \r
398         # if a game under the same server and match_id found, \r
399         # this is a duplicate game and can be ignored\r
400         raise pyramid.httpexceptions.HTTPOk('OK')\r
401     except NoResultFound, e:\r
402         # server_id/match_id combination not found. game is ok to insert\r
403         session.add(game)\r
404         session.flush()\r
405         log.debug("Created game id {0} on server {1}, map {2} at \\r
406                 {3}".format(game.game_id, \r
407                     server_id, map_id, start_dt))\r
408 \r
409     return game\r
410 \r
411 \r
412 def get_or_create_player(session=None, hashkey=None, nick=None):\r
413     """\r
414     Finds a player by hashkey or creates a new one (along with a\r
415     corresponding hashkey entry. Parameters:\r
416 \r
417     session - SQLAlchemy database session factory\r
418     hashkey - hashkey of the player to be found or created\r
419     nick - nick of the player (in case of a first time create)\r
420     """\r
421     # if we have a bot\r
422     if re.search('^bot#\d+$', hashkey) or re.search('^bot#\d+#', hashkey):\r
423         player = session.query(Player).filter_by(player_id=1).one()\r
424     # if we have an untracked player\r
425     elif re.search('^player#\d+$', hashkey):\r
426         player = session.query(Player).filter_by(player_id=2).one()\r
427     # else it is a tracked player\r
428     else:\r
429         # see if the player is already in the database\r
430         # if not, create one and the hashkey along with it\r
431         try:\r
432             hk = session.query(Hashkey).filter_by(\r
433                     hashkey=hashkey).one()\r
434             player = session.query(Player).filter_by(\r
435                     player_id=hk.player_id).one()\r
436             log.debug("Found existing player {0} with hashkey {1}".format(\r
437                 player.player_id, hashkey))\r
438         except:\r
439             player = Player()\r
440             session.add(player)\r
441             session.flush()\r
442 \r
443             # if nick is given to us, use it. If not, use "Anonymous Player"\r
444             # with a suffix added for uniqueness.\r
445             if nick:\r
446                 player.nick = nick[:128]\r
447                 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))\r
448             else:\r
449                 player.nick = "Anonymous Player #{0}".format(player.player_id)\r
450                 player.stripped_nick = player.nick\r
451 \r
452             hk = Hashkey(player_id=player.player_id, hashkey=hashkey)\r
453             session.add(hk)\r
454             log.debug("Created player {0} ({2}) with hashkey {1}".format(\r
455                 player.player_id, hashkey, player.nick.encode('utf-8')))\r
456 \r
457     return player\r
458 \r
459 \r
460 def create_game_stat(session, game_meta, game, server, gmap, player, events):\r
461     """Game stats handler for all game types"""\r
462 \r
463     # this is what we have to do to get partitioned records in - grab the\r
464     # sequence value first, then insert using the explicit ID (vs autogenerate)\r
465     seq = Sequence('player_game_stats_player_game_stat_id_seq')\r
466     pgstat_id = session.execute(seq)\r
467     pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,\r
468             create_dt=datetime.datetime.utcnow())\r
469 \r
470     # these fields should be on every pgstat record\r
471     pgstat.game_id       = game.game_id\r
472     pgstat.player_id     = player.player_id\r
473     pgstat.nick          = events.get('n', 'Anonymous Player')[:128]\r
474     pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))\r
475     pgstat.score         = int(events.get('scoreboard-score', 0))\r
476     pgstat.alivetime     = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))\r
477     pgstat.rank          = int(events.get('rank', None))\r
478     pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))\r
479 \r
480     # defaults for common game types only\r
481     if game.game_type_cd == 'dm' or game.game_type_cd == 'tdm' or game.game_type_cd == 'duel':\r
482         pgstat.kills = 0\r
483         pgstat.deaths = 0\r
484         pgstat.suicides = 0\r
485     elif game.game_type_cd == 'ctf':\r
486         pgstat.kills = 0\r
487         pgstat.captures = 0\r
488         pgstat.pickups = 0\r
489         pgstat.drops = 0\r
490         pgstat.returns = 0\r
491         pgstat.carrier_frags = 0\r
492 \r
493     if pgstat.nick != player.nick \\r
494             and player.player_id > 2 \\r
495             and pgstat.nick != 'Anonymous Player':\r
496         register_new_nick(session, player, pgstat.nick)\r
497 \r
498     wins = False\r
499 \r
500     # gametype-specific stuff is handled here. if passed to us, we store it\r
501     for (key,value) in events.items():\r
502         if key == 'wins': wins = True\r
503         if key == 't': pgstat.team = int(value)\r
504         if key == 'scoreboard-drops': pgstat.drops = int(value)\r
505         if key == 'scoreboard-returns': pgstat.returns = int(value)\r
506         if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)\r
507         if key == 'scoreboard-pickups': pgstat.pickups = int(value)\r
508         if key == 'scoreboard-caps': pgstat.captures = int(value)\r
509         if key == 'scoreboard-score': pgstat.score = int(value)\r
510         if key == 'scoreboard-deaths': pgstat.deaths = int(value)\r
511         if key == 'scoreboard-kills': pgstat.kills = int(value)\r
512         if key == 'scoreboard-suicides': pgstat.suicides = int(value)\r
513         if key == 'avglatency': pgstat.avg_latency = float(value)\r
514 \r
515         if key == 'scoreboard-captime':\r
516             pgstat.fastest_cap = datetime.timedelta(seconds=float(value)/100)\r
517             if game.game_type_cd == 'ctf':\r
518                 update_fastest_cap(session, player.player_id, game.game_id,\r
519                         gmap.map_id, pgstat.fastest_cap)\r
520 \r
521     # there is no "winning team" field, so we have to derive it\r
522     if wins and pgstat.team is not None and game.winner is None:\r
523         game.winner = pgstat.team\r
524         session.add(game)\r
525 \r
526     session.add(pgstat)\r
527 \r
528     return pgstat\r
529 \r
530 \r
531 def create_weapon_stats(session, game_meta, game, player, pgstat, events):\r
532     """Weapon stats handler for all game types"""\r
533     if game.game_type_cd in 'cts' or player.player_id == 1:\r
534         return []\r
535 \r
536     pwstats = []\r
537 \r
538     # Version 1 of stats submissions doubled the data sent.\r
539     # To counteract this we divide the data by 2 only for\r
540     # POSTs coming from version 1.\r
541     try:\r
542         version = int(game_meta['V'])\r
543         if version == 1:\r
544             is_doubled = True\r
545             log.debug('NOTICE: found a version 1 request, halving the weapon stats...')\r
546         else:\r
547             is_doubled = False\r
548     except:\r
549         is_doubled = False\r
550 \r
551     for (key,value) in events.items():\r
552         matched = re.search("acc-(.*?)-cnt-fired", key)\r
553         if matched:\r
554             weapon_cd = matched.group(1)\r
555             seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')\r
556             pwstat_id = session.execute(seq)\r
557             pwstat = PlayerWeaponStat()\r
558             pwstat.player_weapon_stats_id = pwstat_id\r
559             pwstat.player_id = player.player_id\r
560             pwstat.game_id = game.game_id\r
561             pwstat.player_game_stat_id = pgstat.player_game_stat_id\r
562             pwstat.weapon_cd = weapon_cd\r
563 \r
564             if 'n' in events:\r
565                 pwstat.nick = events['n']\r
566             else:\r
567                 pwstat.nick = events['P']\r
568 \r
569             if 'acc-' + weapon_cd + '-cnt-fired' in events:\r
570                 pwstat.fired = int(round(float(\r
571                         events['acc-' + weapon_cd + '-cnt-fired'])))\r
572             if 'acc-' + weapon_cd + '-fired' in events:\r
573                 pwstat.max = int(round(float(\r
574                         events['acc-' + weapon_cd + '-fired'])))\r
575             if 'acc-' + weapon_cd + '-cnt-hit' in events:\r
576                 pwstat.hit = int(round(float(\r
577                         events['acc-' + weapon_cd + '-cnt-hit'])))\r
578             if 'acc-' + weapon_cd + '-hit' in events:\r
579                 pwstat.actual = int(round(float(\r
580                         events['acc-' + weapon_cd + '-hit'])))\r
581             if 'acc-' + weapon_cd + '-frags' in events:\r
582                 pwstat.frags = int(round(float(\r
583                         events['acc-' + weapon_cd + '-frags'])))\r
584 \r
585             if is_doubled:\r
586                 pwstat.fired = pwstat.fired/2\r
587                 pwstat.max = pwstat.max/2\r
588                 pwstat.hit = pwstat.hit/2\r
589                 pwstat.actual = pwstat.actual/2\r
590                 pwstat.frags = pwstat.frags/2\r
591 \r
592             session.add(pwstat)\r
593             pwstats.append(pwstat)\r
594 \r
595     return pwstats\r
596 \r
597 \r
598 def create_elos(session, game):\r
599     """Elo handler for all game types."""\r
600 \r
601     # the following game types do not record elo\r
602     if game.game_type_cd in 'cts':\r
603         return None\r
604 \r
605     try:\r
606         process_elos(game, session)\r
607     except Exception as e:\r
608         log.debug('Error (non-fatal): elo processing failed.')\r
609 \r
610 \r
611 def submit_stats(request):\r
612     """\r
613     Entry handler for POST stats submissions.\r
614     """\r
615     try:\r
616         # placeholder for the actual session\r
617         session = None\r
618 \r
619         log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +\r
620                 "----- END REQUEST BODY -----\n\n")\r
621 \r
622         (idfp, status) = verify_request(request)\r
623         (game_meta, raw_players) = parse_stats_submission(request.body)\r
624         revision = game_meta.get('R', 'unknown')\r
625         duration = game_meta.get('D', None)\r
626 \r
627         # only players present at the end of the match are eligible for stats\r
628         raw_players = filter(played_in_game, raw_players)\r
629 \r
630         do_precondition_checks(request, game_meta, raw_players)\r
631 \r
632         # the "duel" gametype is fake\r
633         if num_real_players(raw_players) == 2 and game_meta['G'] == 'dm':\r
634             game_meta['G'] = 'duel'\r
635 \r
636         #----------------------------------------------------------------------\r
637         # Actual setup (inserts/updates) below here\r
638         #----------------------------------------------------------------------\r
639         session = DBSession()\r
640 \r
641         game_type_cd = game_meta['G']\r
642 \r
643         # All game types create Game, Server, Map, and Player records\r
644         # the same way.\r
645         server = get_or_create_server(\r
646                 session  = session,\r
647                 hashkey  = idfp,\r
648                 name     = game_meta['S'],\r
649                 revision = revision,\r
650                 ip_addr  = get_remote_addr(request))\r
651 \r
652         gmap = get_or_create_map(\r
653                 session = session,\r
654                 name    = game_meta['M'])\r
655 \r
656         game = create_game(\r
657                 session      = session,\r
658                 start_dt     = datetime.datetime.utcnow(),\r
659                 server_id    = server.server_id,\r
660                 game_type_cd = game_type_cd,\r
661                 map_id       = gmap.map_id,\r
662                 match_id     = game_meta['I'],\r
663                 duration     = duration)\r
664 \r
665         for events in raw_players:\r
666             player = get_or_create_player(\r
667                 session = session,\r
668                 hashkey = events['P'],\r
669                 nick    = events.get('n', None))\r
670 \r
671             pgstat = create_game_stat(session, game_meta, game, server,\r
672                     gmap, player, events)\r
673 \r
674             pwstats = create_weapon_stats(session, game_meta, game, player,\r
675                     pgstat, events)\r
676 \r
677         create_elos(session, game)\r
678 \r
679         session.commit()\r
680         log.debug('Success! Stats recorded.')\r
681         return Response('200 OK')\r
682     except Exception as e:\r
683         if session:\r
684             session.rollback()\r
685         return e\r