]> de.git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/views/submission.py
02d883f295196892d39b62ef9ec8acb8636bccfe
[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 should_do_weapon_stats(game_type_cd):\r
232     """True of the game type should record weapon stats. False otherwise."""\r
233     if game_type_cd in 'cts':\r
234         return False\r
235     else:\r
236         return True\r
237 \r
238 \r
239 def should_do_elos(game_type_cd):\r
240     """True of the game type should process Elos. False otherwise."""\r
241     elo_game_types = ('duel', 'dm', 'ctf', 'tdm', 'kh',\r
242             'ka', 'ft', 'freezetag')\r
243 \r
244     if game_type_cd in elo_game_types:\r
245         return True\r
246     else:\r
247         return False\r
248 \r
249 \r
250 def register_new_nick(session, player, new_nick):\r
251     """\r
252     Change the player record's nick to the newly found nick. Store the old\r
253     nick in the player_nicks table for that player.\r
254 \r
255     session - SQLAlchemy database session factory\r
256     player - player record whose nick is changing\r
257     new_nick - the new nickname\r
258     """\r
259     # see if that nick already exists\r
260     stripped_nick = strip_colors(qfont_decode(player.nick))\r
261     try:\r
262         player_nick = session.query(PlayerNick).filter_by(\r
263             player_id=player.player_id, stripped_nick=stripped_nick).one()\r
264     except NoResultFound, e:\r
265         # player_id/stripped_nick not found, create one\r
266         # but we don't store "Anonymous Player #N"\r
267         if not re.search('^Anonymous Player #\d+$', player.nick):\r
268             player_nick = PlayerNick()\r
269             player_nick.player_id = player.player_id\r
270             player_nick.stripped_nick = stripped_nick\r
271             player_nick.nick = player.nick\r
272             session.add(player_nick)\r
273 \r
274     # We change to the new nick regardless\r
275     player.nick = new_nick\r
276     player.stripped_nick = strip_colors(qfont_decode(new_nick))\r
277     session.add(player)\r
278 \r
279 \r
280 def update_fastest_cap(session, player_id, game_id,  map_id, captime):\r
281     """\r
282     Check the fastest cap time for the player and map. If there isn't\r
283     one, insert one. If there is, check if the passed time is faster.\r
284     If so, update!\r
285     """\r
286     # we don't record fastest cap times for bots or anonymous players\r
287     if player_id <= 2:\r
288         return\r
289 \r
290     # see if a cap entry exists already\r
291     # then check to see if the new captime is faster\r
292     try:\r
293         cur_fastest_cap = session.query(PlayerCaptime).filter_by(\r
294             player_id=player_id, map_id=map_id).one()\r
295 \r
296         # current captime is faster, so update\r
297         if captime < cur_fastest_cap.fastest_cap:\r
298             cur_fastest_cap.fastest_cap = captime\r
299             cur_fastest_cap.game_id = game_id\r
300             cur_fastest_cap.create_dt = datetime.datetime.utcnow()\r
301             session.add(cur_fastest_cap)\r
302 \r
303     except NoResultFound, e:\r
304         # none exists, so insert\r
305         cur_fastest_cap = PlayerCaptime(player_id, game_id, map_id, captime)\r
306         session.add(cur_fastest_cap)\r
307         session.flush()\r
308 \r
309 \r
310 def get_or_create_server(session=None, name=None, hashkey=None, ip_addr=None,\r
311         revision=None):\r
312     """\r
313     Find a server by name or create one if not found. Parameters:\r
314 \r
315     session - SQLAlchemy database session factory\r
316     name - server name of the server to be found or created\r
317     hashkey - server hashkey\r
318     """\r
319     try:\r
320         # find one by that name, if it exists\r
321         server = session.query(Server).filter_by(name=name).one()\r
322 \r
323         # store new hashkey\r
324         if server.hashkey != hashkey:\r
325             server.hashkey = hashkey\r
326             session.add(server)\r
327 \r
328         # store new IP address\r
329         if server.ip_addr != ip_addr:\r
330             server.ip_addr = ip_addr\r
331             session.add(server)\r
332 \r
333         # store new revision\r
334         if server.revision != revision:\r
335             server.revision = revision\r
336             session.add(server)\r
337 \r
338         log.debug("Found existing server {0}".format(server.server_id))\r
339 \r
340     except MultipleResultsFound, e:\r
341         # multiple found, so also filter by hashkey\r
342         server = session.query(Server).filter_by(name=name).\\r
343                 filter_by(hashkey=hashkey).one()\r
344         log.debug("Found existing server {0}".format(server.server_id))\r
345 \r
346     except NoResultFound, e:\r
347         # not found, create one\r
348         server = Server(name=name, hashkey=hashkey)\r
349         session.add(server)\r
350         session.flush()\r
351         log.debug("Created server {0} with hashkey {1}".format(\r
352             server.server_id, server.hashkey))\r
353 \r
354     return server\r
355 \r
356 \r
357 def get_or_create_map(session=None, name=None):\r
358     """\r
359     Find a map by name or create one if not found. Parameters:\r
360 \r
361     session - SQLAlchemy database session factory\r
362     name - map name of the map to be found or created\r
363     """\r
364     try:\r
365         # find one by the name, if it exists\r
366         gmap = session.query(Map).filter_by(name=name).one()\r
367         log.debug("Found map id {0}: {1}".format(gmap.map_id, \r
368             gmap.name))\r
369     except NoResultFound, e:\r
370         gmap = Map(name=name)\r
371         session.add(gmap)\r
372         session.flush()\r
373         log.debug("Created map id {0}: {1}".format(gmap.map_id,\r
374             gmap.name))\r
375     except MultipleResultsFound, e:\r
376         # multiple found, so use the first one but warn\r
377         log.debug(e)\r
378         gmaps = session.query(Map).filter_by(name=name).order_by(\r
379                 Map.map_id).all()\r
380         gmap = gmaps[0]\r
381         log.debug("Found map id {0}: {1} but found \\r
382                 multiple".format(gmap.map_id, gmap.name))\r
383 \r
384     return gmap\r
385 \r
386 \r
387 def create_game(session=None, start_dt=None, game_type_cd=None, \r
388         server_id=None, map_id=None, winner=None, match_id=None,\r
389         duration=None):\r
390     """\r
391     Creates a game. Parameters:\r
392 \r
393     session - SQLAlchemy database session factory\r
394     start_dt - when the game started (datetime object)\r
395     game_type_cd - the game type of the game being played\r
396     server_id - server identifier of the server hosting the game\r
397     map_id - map on which the game was played\r
398     winner - the team id of the team that won\r
399     """\r
400     seq = Sequence('games_game_id_seq')\r
401     game_id = session.execute(seq)\r
402     game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,\r
403                 server_id=server_id, map_id=map_id, winner=winner)\r
404     game.match_id = match_id\r
405 \r
406     try:\r
407         game.duration = datetime.timedelta(seconds=int(round(float(duration))))\r
408     except:\r
409         pass\r
410 \r
411     try:\r
412         session.query(Game).filter(Game.server_id==server_id).\\r
413                 filter(Game.match_id==match_id).one()\r
414 \r
415         log.debug("Error: game with same server and match_id found! Ignoring.")\r
416 \r
417         # if a game under the same server and match_id found, \r
418         # this is a duplicate game and can be ignored\r
419         raise pyramid.httpexceptions.HTTPOk('OK')\r
420     except NoResultFound, e:\r
421         # server_id/match_id combination not found. game is ok to insert\r
422         session.add(game)\r
423         session.flush()\r
424         log.debug("Created game id {0} on server {1}, map {2} at \\r
425                 {3}".format(game.game_id, \r
426                     server_id, map_id, start_dt))\r
427 \r
428     return game\r
429 \r
430 \r
431 def get_or_create_player(session=None, hashkey=None, nick=None):\r
432     """\r
433     Finds a player by hashkey or creates a new one (along with a\r
434     corresponding hashkey entry. Parameters:\r
435 \r
436     session - SQLAlchemy database session factory\r
437     hashkey - hashkey of the player to be found or created\r
438     nick - nick of the player (in case of a first time create)\r
439     """\r
440     # if we have a bot\r
441     if re.search('^bot#\d+$', hashkey) or re.search('^bot#\d+#', hashkey):\r
442         player = session.query(Player).filter_by(player_id=1).one()\r
443     # if we have an untracked player\r
444     elif re.search('^player#\d+$', hashkey):\r
445         player = session.query(Player).filter_by(player_id=2).one()\r
446     # else it is a tracked player\r
447     else:\r
448         # see if the player is already in the database\r
449         # if not, create one and the hashkey along with it\r
450         try:\r
451             hk = session.query(Hashkey).filter_by(\r
452                     hashkey=hashkey).one()\r
453             player = session.query(Player).filter_by(\r
454                     player_id=hk.player_id).one()\r
455             log.debug("Found existing player {0} with hashkey {1}".format(\r
456                 player.player_id, hashkey))\r
457         except:\r
458             player = Player()\r
459             session.add(player)\r
460             session.flush()\r
461 \r
462             # if nick is given to us, use it. If not, use "Anonymous Player"\r
463             # with a suffix added for uniqueness.\r
464             if nick:\r
465                 player.nick = nick[:128]\r
466                 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))\r
467             else:\r
468                 player.nick = "Anonymous Player #{0}".format(player.player_id)\r
469                 player.stripped_nick = player.nick\r
470 \r
471             hk = Hashkey(player_id=player.player_id, hashkey=hashkey)\r
472             session.add(hk)\r
473             log.debug("Created player {0} ({2}) with hashkey {1}".format(\r
474                 player.player_id, hashkey, player.nick.encode('utf-8')))\r
475 \r
476     return player\r
477 \r
478 \r
479 def create_game_stat(session, game_meta, game, server, gmap, player, events):\r
480     """Game stats handler for all game types"""\r
481 \r
482     # this is what we have to do to get partitioned records in - grab the\r
483     # sequence value first, then insert using the explicit ID (vs autogenerate)\r
484     seq = Sequence('player_game_stats_player_game_stat_id_seq')\r
485     pgstat_id = session.execute(seq)\r
486     pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,\r
487             create_dt=datetime.datetime.utcnow())\r
488 \r
489     # these fields should be on every pgstat record\r
490     pgstat.game_id       = game.game_id\r
491     pgstat.player_id     = player.player_id\r
492     pgstat.nick          = events.get('n', 'Anonymous Player')[:128]\r
493     pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))\r
494     pgstat.score         = int(events.get('scoreboard-score', 0))\r
495     pgstat.alivetime     = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))\r
496     pgstat.rank          = int(events.get('rank', None))\r
497     pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))\r
498 \r
499     # defaults for common game types only\r
500     if game.game_type_cd == 'dm' or game.game_type_cd == 'tdm' or game.game_type_cd == 'duel':\r
501         pgstat.kills = 0\r
502         pgstat.deaths = 0\r
503         pgstat.suicides = 0\r
504     elif game.game_type_cd == 'ctf':\r
505         pgstat.kills = 0\r
506         pgstat.captures = 0\r
507         pgstat.pickups = 0\r
508         pgstat.drops = 0\r
509         pgstat.returns = 0\r
510         pgstat.carrier_frags = 0\r
511 \r
512     if pgstat.nick != player.nick \\r
513             and player.player_id > 2 \\r
514             and pgstat.nick != 'Anonymous Player':\r
515         register_new_nick(session, player, pgstat.nick)\r
516 \r
517     wins = False\r
518 \r
519     # gametype-specific stuff is handled here. if passed to us, we store it\r
520     for (key,value) in events.items():\r
521         if key == 'wins': wins = True\r
522         if key == 't': pgstat.team = int(value)\r
523         if key == 'scoreboard-drops': pgstat.drops = int(value)\r
524         if key == 'scoreboard-returns': pgstat.returns = int(value)\r
525         if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)\r
526         if key == 'scoreboard-pickups': pgstat.pickups = int(value)\r
527         if key == 'scoreboard-caps': pgstat.captures = int(value)\r
528         if key == 'scoreboard-score': pgstat.score = int(value)\r
529         if key == 'scoreboard-deaths': pgstat.deaths = int(value)\r
530         if key == 'scoreboard-kills': pgstat.kills = int(value)\r
531         if key == 'scoreboard-suicides': pgstat.suicides = int(value)\r
532         if key == 'avglatency': pgstat.avg_latency = float(value)\r
533 \r
534         if key == 'scoreboard-captime':\r
535             pgstat.fastest_cap = datetime.timedelta(seconds=float(value)/100)\r
536             if game.game_type_cd == 'ctf':\r
537                 update_fastest_cap(session, player.player_id, game.game_id,\r
538                         gmap.map_id, pgstat.fastest_cap)\r
539 \r
540     # there is no "winning team" field, so we have to derive it\r
541     if wins and pgstat.team is not None and game.winner is None:\r
542         game.winner = pgstat.team\r
543         session.add(game)\r
544 \r
545     session.add(pgstat)\r
546 \r
547     return pgstat\r
548 \r
549 \r
550 def create_weapon_stats(session, game_meta, game, player, pgstat, events):\r
551     """Weapon stats handler for all game types"""\r
552     pwstats = []\r
553 \r
554     # Version 1 of stats submissions doubled the data sent.\r
555     # To counteract this we divide the data by 2 only for\r
556     # POSTs coming from version 1.\r
557     try:\r
558         version = int(game_meta['V'])\r
559         if version == 1:\r
560             is_doubled = True\r
561             log.debug('NOTICE: found a version 1 request, halving the weapon stats...')\r
562         else:\r
563             is_doubled = False\r
564     except:\r
565         is_doubled = False\r
566 \r
567     for (key,value) in events.items():\r
568         matched = re.search("acc-(.*?)-cnt-fired", key)\r
569         if matched:\r
570             weapon_cd = matched.group(1)\r
571             seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')\r
572             pwstat_id = session.execute(seq)\r
573             pwstat = PlayerWeaponStat()\r
574             pwstat.player_weapon_stats_id = pwstat_id\r
575             pwstat.player_id = player.player_id\r
576             pwstat.game_id = game.game_id\r
577             pwstat.player_game_stat_id = pgstat.player_game_stat_id\r
578             pwstat.weapon_cd = weapon_cd\r
579 \r
580             if 'n' in events:\r
581                 pwstat.nick = events['n']\r
582             else:\r
583                 pwstat.nick = events['P']\r
584 \r
585             if 'acc-' + weapon_cd + '-cnt-fired' in events:\r
586                 pwstat.fired = int(round(float(\r
587                         events['acc-' + weapon_cd + '-cnt-fired'])))\r
588             if 'acc-' + weapon_cd + '-fired' in events:\r
589                 pwstat.max = int(round(float(\r
590                         events['acc-' + weapon_cd + '-fired'])))\r
591             if 'acc-' + weapon_cd + '-cnt-hit' in events:\r
592                 pwstat.hit = int(round(float(\r
593                         events['acc-' + weapon_cd + '-cnt-hit'])))\r
594             if 'acc-' + weapon_cd + '-hit' in events:\r
595                 pwstat.actual = int(round(float(\r
596                         events['acc-' + weapon_cd + '-hit'])))\r
597             if 'acc-' + weapon_cd + '-frags' in events:\r
598                 pwstat.frags = int(round(float(\r
599                         events['acc-' + weapon_cd + '-frags'])))\r
600 \r
601             if is_doubled:\r
602                 pwstat.fired = pwstat.fired/2\r
603                 pwstat.max = pwstat.max/2\r
604                 pwstat.hit = pwstat.hit/2\r
605                 pwstat.actual = pwstat.actual/2\r
606                 pwstat.frags = pwstat.frags/2\r
607 \r
608             session.add(pwstat)\r
609             pwstats.append(pwstat)\r
610 \r
611     return pwstats\r
612 \r
613 \r
614 def create_elos(session, game):\r
615     """Elo handler for all game types."""\r
616     try:\r
617         process_elos(game, session)\r
618     except Exception as e:\r
619         log.debug('Error (non-fatal): elo processing failed.')\r
620 \r
621 \r
622 def submit_stats(request):\r
623     """\r
624     Entry handler for POST stats submissions.\r
625     """\r
626     try:\r
627         # placeholder for the actual session\r
628         session = None\r
629 \r
630         log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +\r
631                 "----- END REQUEST BODY -----\n\n")\r
632 \r
633         (idfp, status) = verify_request(request)\r
634         (game_meta, raw_players) = parse_stats_submission(request.body)\r
635         revision = game_meta.get('R', 'unknown')\r
636         duration = game_meta.get('D', None)\r
637 \r
638         # only players present at the end of the match are eligible for stats\r
639         raw_players = filter(played_in_game, raw_players)\r
640 \r
641         do_precondition_checks(request, game_meta, raw_players)\r
642 \r
643         # the "duel" gametype is fake\r
644         if len(raw_players) == 2 \\r
645             and num_real_players(raw_players) == 2 \\r
646             and game_meta['G'] == 'dm':\r
647             game_meta['G'] = 'duel'\r
648 \r
649         #----------------------------------------------------------------------\r
650         # Actual setup (inserts/updates) below here\r
651         #----------------------------------------------------------------------\r
652         session = DBSession()\r
653 \r
654         game_type_cd = game_meta['G']\r
655 \r
656         # All game types create Game, Server, Map, and Player records\r
657         # the same way.\r
658         server = get_or_create_server(\r
659                 session  = session,\r
660                 hashkey  = idfp,\r
661                 name     = game_meta['S'],\r
662                 revision = revision,\r
663                 ip_addr  = get_remote_addr(request))\r
664 \r
665         gmap = get_or_create_map(\r
666                 session = session,\r
667                 name    = game_meta['M'])\r
668 \r
669         game = create_game(\r
670                 session      = session,\r
671                 start_dt     = datetime.datetime.utcnow(),\r
672                 server_id    = server.server_id,\r
673                 game_type_cd = game_type_cd,\r
674                 map_id       = gmap.map_id,\r
675                 match_id     = game_meta['I'],\r
676                 duration     = duration)\r
677 \r
678         for events in raw_players:\r
679             player = get_or_create_player(\r
680                 session = session,\r
681                 hashkey = events['P'],\r
682                 nick    = events.get('n', None))\r
683 \r
684             pgstat = create_game_stat(session, game_meta, game, server,\r
685                     gmap, player, events)\r
686 \r
687             if should_do_weapon_stats(game_type_cd) and player.player_id > 1:\r
688                 pwstats = create_weapon_stats(session, game_meta, game, player,\r
689                         pgstat, events)\r
690 \r
691         if should_do_elos(game_type_cd):\r
692             create_elos(session, game)\r
693 \r
694         session.commit()\r
695         log.debug('Success! Stats recorded.')\r
696         return Response('200 OK')\r
697     except Exception as e:\r
698         if session:\r
699             session.rollback()\r
700         return e\r