]> git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/views/submission.py
Allow version-specific gametype support.
[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, version):\r
99     """Whether a gametype is supported or not"""\r
100     is_supported = False\r
101 \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
105             'as',\r
106             'ca',\r
107             # 'cq',\r
108             # 'cts',\r
109             'dm',\r
110             'dom',\r
111             'ft', 'freezetag',\r
112             'ka', 'keepaway',\r
113             'kh',\r
114             # 'lms',\r
115             'nb', 'nexball',\r
116             # 'rc',\r
117             'rune',\r
118             'tdm',\r
119         )\r
120 \r
121     if gametype in supported_game_types:\r
122         is_supported = True\r
123     else:\r
124         is_supported = False\r
125 \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
129 \r
130     return is_supported\r
131 \r
132 \r
133 def verify_request(request):\r
134     """Verify requests using the d0_blind_id library"""\r
135 \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
140     else:\r
141         flg_verify_requests = False\r
142 \r
143     try:\r
144         (idfp, status) = d0_blind_id_verify(\r
145                 sig=request.headers['X-D0-Blind-Id-Detached-Signature'],\r
146                 querystring='',\r
147                 postdata=request.body)\r
148 \r
149         log.debug('\nidfp: {0}\nstatus: {1}'.format(idfp, status))\r
150     except:\r
151         idfp = None\r
152         status = None\r
153 \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
157 \r
158     return (idfp, status)\r
159 \r
160 \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
167 \r
168     try:\r
169         version = int(game_meta['V'])\r
170     except:\r
171         log.debug("ERROR: Required game meta invalid")\r
172         raise pyramid.httpexceptions.HTTPUnprocessableEntity("Invalid game meta")\r
173 \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
177 \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
181 \r
182     if is_blank_game(raw_players):\r
183         log.debug("ERROR: Blank game")\r
184         raise pyramid.httpexceptions.HTTPOk("OK")\r
185 \r
186 \r
187 def is_real_player(events):\r
188     """\r
189     Determines if a given set of events correspond with a non-bot\r
190     """\r
191     if not events['P'].startswith('bot'):\r
192         return True\r
193     else:\r
194         return False\r
195 \r
196 \r
197 def played_in_game(events):\r
198     """\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
201     """\r
202     if 'matches' in events and 'scoreboardvalid' in events:\r
203         return True\r
204     else:\r
205         return False\r
206 \r
207 \r
208 def num_real_players(player_events):\r
209     """\r
210     Returns the number of real players (those who played\r
211     and are on the scoreboard).\r
212     """\r
213     real_players = 0\r
214 \r
215     for events in player_events:\r
216         if is_real_player(events) and played_in_game(events):\r
217             real_players += 1\r
218 \r
219     return real_players\r
220 \r
221 \r
222 def has_minimum_real_players(settings, player_events):\r
223     """\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
227     """\r
228     flg_has_min_real_players = True\r
229 \r
230     try:\r
231         minimum_required_players = int(\r
232                 settings['xonstat.minimum_required_players'])\r
233     except:\r
234         minimum_required_players = 2\r
235 \r
236     real_players = num_real_players(player_events)\r
237 \r
238     if real_players < minimum_required_players:\r
239         flg_has_min_real_players = False\r
240 \r
241     return flg_has_min_real_players\r
242 \r
243 \r
244 def has_required_metadata(metadata):\r
245     """\r
246     Determines if a give set of metadata has enough data to create a game,\r
247     server, and map with.\r
248     """\r
249     flg_has_req_metadata = True\r
250 \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
257 \r
258     return flg_has_req_metadata\r
259 \r
260 \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
264         return False\r
265     else:\r
266         return True\r
267 \r
268 \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
273 \r
274     if game_type_cd in elo_game_types:\r
275         return True\r
276     else:\r
277         return False\r
278 \r
279 \r
280 def register_new_nick(session, player, new_nick):\r
281     """\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
284 \r
285     session - SQLAlchemy database session factory\r
286     player - player record whose nick is changing\r
287     new_nick - the new nickname\r
288     """\r
289     # see if that nick already exists\r
290     stripped_nick = strip_colors(qfont_decode(player.nick))\r
291     try:\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
303 \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
308 \r
309 \r
310 def update_fastest_cap(session, player_id, game_id,  map_id, captime):\r
311     """\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
314     If so, update!\r
315     """\r
316     # we don't record fastest cap times for bots or anonymous players\r
317     if player_id <= 2:\r
318         return\r
319 \r
320     # see if a cap entry exists already\r
321     # then check to see if the new captime is faster\r
322     try:\r
323         cur_fastest_cap = session.query(PlayerCaptime).filter_by(\r
324             player_id=player_id, map_id=map_id).one()\r
325 \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
332 \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
337         session.flush()\r
338 \r
339 \r
340 def get_or_create_server(session=None, name=None, hashkey=None, ip_addr=None,\r
341         revision=None):\r
342     """\r
343     Find a server by name or create one if not found. Parameters:\r
344 \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
348     """\r
349     try:\r
350         # find one by that name, if it exists\r
351         server = session.query(Server).filter_by(name=name).one()\r
352 \r
353         # store new hashkey\r
354         if server.hashkey != hashkey:\r
355             server.hashkey = hashkey\r
356             session.add(server)\r
357 \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
362 \r
363         # store new revision\r
364         if server.revision != revision:\r
365             server.revision = revision\r
366             session.add(server)\r
367 \r
368         log.debug("Found existing server {0}".format(server.server_id))\r
369 \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
375 \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
380         session.flush()\r
381         log.debug("Created server {0} with hashkey {1}".format(\r
382             server.server_id, server.hashkey))\r
383 \r
384     return server\r
385 \r
386 \r
387 def get_or_create_map(session=None, name=None):\r
388     """\r
389     Find a map by name or create one if not found. Parameters:\r
390 \r
391     session - SQLAlchemy database session factory\r
392     name - map name of the map to be found or created\r
393     """\r
394     try:\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
398             gmap.name))\r
399     except NoResultFound, e:\r
400         gmap = Map(name=name)\r
401         session.add(gmap)\r
402         session.flush()\r
403         log.debug("Created map id {0}: {1}".format(gmap.map_id,\r
404             gmap.name))\r
405     except MultipleResultsFound, e:\r
406         # multiple found, so use the first one but warn\r
407         log.debug(e)\r
408         gmaps = session.query(Map).filter_by(name=name).order_by(\r
409                 Map.map_id).all()\r
410         gmap = gmaps[0]\r
411         log.debug("Found map id {0}: {1} but found \\r
412                 multiple".format(gmap.map_id, gmap.name))\r
413 \r
414     return gmap\r
415 \r
416 \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
419         duration=None):\r
420     """\r
421     Creates a game. Parameters:\r
422 \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
429     """\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
435 \r
436     try:\r
437         game.duration = datetime.timedelta(seconds=int(round(float(duration))))\r
438     except:\r
439         pass\r
440 \r
441     try:\r
442         session.query(Game).filter(Game.server_id==server_id).\\r
443                 filter(Game.match_id==match_id).one()\r
444 \r
445         log.debug("Error: game with same server and match_id found! Ignoring.")\r
446 \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
452         session.add(game)\r
453         session.flush()\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
457 \r
458     return game\r
459 \r
460 \r
461 def get_or_create_player(session=None, hashkey=None, nick=None):\r
462     """\r
463     Finds a player by hashkey or creates a new one (along with a\r
464     corresponding hashkey entry. Parameters:\r
465 \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
469     """\r
470     # if we have a bot\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
477     else:\r
478         # see if the player is already in the database\r
479         # if not, create one and the hashkey along with it\r
480         try:\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
487         except:\r
488             player = Player()\r
489             session.add(player)\r
490             session.flush()\r
491 \r
492             # if nick is given to us, use it. If not, use "Anonymous Player"\r
493             # with a suffix added for uniqueness.\r
494             if nick:\r
495                 player.nick = nick[:128]\r
496                 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))\r
497             else:\r
498                 player.nick = "Anonymous Player #{0}".format(player.player_id)\r
499                 player.stripped_nick = player.nick\r
500 \r
501             hk = Hashkey(player_id=player.player_id, hashkey=hashkey)\r
502             session.add(hk)\r
503             log.debug("Created player {0} ({2}) with hashkey {1}".format(\r
504                 player.player_id, hashkey, player.nick.encode('utf-8')))\r
505 \r
506     return player\r
507 \r
508 \r
509 def create_game_stat(session, game_meta, game, server, gmap, player, events):\r
510     """Game stats handler for all game types"""\r
511 \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
518 \r
519     # these fields should be on every pgstat record\r
520     pgstat.game_id       = game.game_id\r
521     pgstat.player_id     = player.player_id\r
522     pgstat.nick          = events.get('n', 'Anonymous Player')[:128]\r
523     pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))\r
524     pgstat.score         = int(round(float(events.get('scoreboard-score', 0))))\r
525     pgstat.alivetime     = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))\r
526     pgstat.rank          = int(events.get('rank', None))\r
527     pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))\r
528 \r
529     # defaults for common game types only\r
530     if game.game_type_cd == 'dm' or game.game_type_cd == 'tdm' or game.game_type_cd == 'duel':\r
531         pgstat.kills = 0\r
532         pgstat.deaths = 0\r
533         pgstat.suicides = 0\r
534     elif game.game_type_cd == 'ctf':\r
535         pgstat.kills = 0\r
536         pgstat.captures = 0\r
537         pgstat.pickups = 0\r
538         pgstat.drops = 0\r
539         pgstat.returns = 0\r
540         pgstat.carrier_frags = 0\r
541 \r
542     if pgstat.nick != player.nick \\r
543             and player.player_id > 2 \\r
544             and pgstat.nick != 'Anonymous Player':\r
545         register_new_nick(session, player, pgstat.nick)\r
546 \r
547     wins = False\r
548 \r
549     # gametype-specific stuff is handled here. if passed to us, we store it\r
550     for (key,value) in events.items():\r
551         if key == 'wins': wins = True\r
552         if key == 't': pgstat.team = int(value)\r
553         if key == 'scoreboard-drops': pgstat.drops = int(value)\r
554         if key == 'scoreboard-returns': pgstat.returns = int(value)\r
555         if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)\r
556         if key == 'scoreboard-pickups': pgstat.pickups = int(value)\r
557         if key == 'scoreboard-caps': pgstat.captures = int(value)\r
558         if key == 'scoreboard-score': pgstat.score = int(round(float(value)))\r
559         if key == 'scoreboard-deaths': pgstat.deaths = int(value)\r
560         if key == 'scoreboard-kills': pgstat.kills = int(value)\r
561         if key == 'scoreboard-suicides': pgstat.suicides = int(value)\r
562         if key == 'avglatency': pgstat.avg_latency = float(value)\r
563 \r
564         if key == 'scoreboard-captime':\r
565             pgstat.fastest_cap = datetime.timedelta(seconds=float(value)/100)\r
566             if game.game_type_cd == 'ctf':\r
567                 update_fastest_cap(session, player.player_id, game.game_id,\r
568                         gmap.map_id, pgstat.fastest_cap)\r
569 \r
570     # there is no "winning team" field, so we have to derive it\r
571     if wins and pgstat.team is not None and game.winner is None:\r
572         game.winner = pgstat.team\r
573         session.add(game)\r
574 \r
575     session.add(pgstat)\r
576 \r
577     return pgstat\r
578 \r
579 \r
580 def create_weapon_stats(session, game_meta, game, player, pgstat, events):\r
581     """Weapon stats handler for all game types"""\r
582     pwstats = []\r
583 \r
584     # Version 1 of stats submissions doubled the data sent.\r
585     # To counteract this we divide the data by 2 only for\r
586     # POSTs coming from version 1.\r
587     try:\r
588         version = int(game_meta['V'])\r
589         if version == 1:\r
590             is_doubled = True\r
591             log.debug('NOTICE: found a version 1 request, halving the weapon stats...')\r
592         else:\r
593             is_doubled = False\r
594     except:\r
595         is_doubled = False\r
596 \r
597     for (key,value) in events.items():\r
598         matched = re.search("acc-(.*?)-cnt-fired", key)\r
599         if matched:\r
600             weapon_cd = matched.group(1)\r
601             seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')\r
602             pwstat_id = session.execute(seq)\r
603             pwstat = PlayerWeaponStat()\r
604             pwstat.player_weapon_stats_id = pwstat_id\r
605             pwstat.player_id = player.player_id\r
606             pwstat.game_id = game.game_id\r
607             pwstat.player_game_stat_id = pgstat.player_game_stat_id\r
608             pwstat.weapon_cd = weapon_cd\r
609 \r
610             if 'n' in events:\r
611                 pwstat.nick = events['n']\r
612             else:\r
613                 pwstat.nick = events['P']\r
614 \r
615             if 'acc-' + weapon_cd + '-cnt-fired' in events:\r
616                 pwstat.fired = int(round(float(\r
617                         events['acc-' + weapon_cd + '-cnt-fired'])))\r
618             if 'acc-' + weapon_cd + '-fired' in events:\r
619                 pwstat.max = int(round(float(\r
620                         events['acc-' + weapon_cd + '-fired'])))\r
621             if 'acc-' + weapon_cd + '-cnt-hit' in events:\r
622                 pwstat.hit = int(round(float(\r
623                         events['acc-' + weapon_cd + '-cnt-hit'])))\r
624             if 'acc-' + weapon_cd + '-hit' in events:\r
625                 pwstat.actual = int(round(float(\r
626                         events['acc-' + weapon_cd + '-hit'])))\r
627             if 'acc-' + weapon_cd + '-frags' in events:\r
628                 pwstat.frags = int(round(float(\r
629                         events['acc-' + weapon_cd + '-frags'])))\r
630 \r
631             if is_doubled:\r
632                 pwstat.fired = pwstat.fired/2\r
633                 pwstat.max = pwstat.max/2\r
634                 pwstat.hit = pwstat.hit/2\r
635                 pwstat.actual = pwstat.actual/2\r
636                 pwstat.frags = pwstat.frags/2\r
637 \r
638             session.add(pwstat)\r
639             pwstats.append(pwstat)\r
640 \r
641     return pwstats\r
642 \r
643 \r
644 def create_elos(session, game):\r
645     """Elo handler for all game types."""\r
646     try:\r
647         process_elos(game, session)\r
648     except Exception as e:\r
649         log.debug('Error (non-fatal): elo processing failed.')\r
650 \r
651 \r
652 def submit_stats(request):\r
653     """\r
654     Entry handler for POST stats submissions.\r
655     """\r
656     try:\r
657         # placeholder for the actual session\r
658         session = None\r
659 \r
660         log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +\r
661                 "----- END REQUEST BODY -----\n\n")\r
662 \r
663         (idfp, status) = verify_request(request)\r
664         (game_meta, raw_players) = parse_stats_submission(request.body)\r
665         revision = game_meta.get('R', 'unknown')\r
666         duration = game_meta.get('D', None)\r
667 \r
668         # only players present at the end of the match are eligible for stats\r
669         raw_players = filter(played_in_game, raw_players)\r
670 \r
671         do_precondition_checks(request, game_meta, raw_players)\r
672 \r
673         # the "duel" gametype is fake\r
674         if len(raw_players) == 2 \\r
675             and num_real_players(raw_players) == 2 \\r
676             and game_meta['G'] == 'dm':\r
677             game_meta['G'] = 'duel'\r
678 \r
679         #----------------------------------------------------------------------\r
680         # Actual setup (inserts/updates) below here\r
681         #----------------------------------------------------------------------\r
682         session = DBSession()\r
683 \r
684         game_type_cd = game_meta['G']\r
685 \r
686         # All game types create Game, Server, Map, and Player records\r
687         # the same way.\r
688         server = get_or_create_server(\r
689                 session  = session,\r
690                 hashkey  = idfp,\r
691                 name     = game_meta['S'],\r
692                 revision = revision,\r
693                 ip_addr  = get_remote_addr(request))\r
694 \r
695         gmap = get_or_create_map(\r
696                 session = session,\r
697                 name    = game_meta['M'])\r
698 \r
699         game = create_game(\r
700                 session      = session,\r
701                 start_dt     = datetime.datetime.utcnow(),\r
702                 server_id    = server.server_id,\r
703                 game_type_cd = game_type_cd,\r
704                 map_id       = gmap.map_id,\r
705                 match_id     = game_meta['I'],\r
706                 duration     = duration)\r
707 \r
708         for events in raw_players:\r
709             player = get_or_create_player(\r
710                 session = session,\r
711                 hashkey = events['P'],\r
712                 nick    = events.get('n', None))\r
713 \r
714             pgstat = create_game_stat(session, game_meta, game, server,\r
715                     gmap, player, events)\r
716 \r
717             if should_do_weapon_stats(game_type_cd) and player.player_id > 1:\r
718                 pwstats = create_weapon_stats(session, game_meta, game, player,\r
719                         pgstat, events)\r
720 \r
721         if should_do_elos(game_type_cd):\r
722             create_elos(session, game)\r
723 \r
724         session.commit()\r
725         log.debug('Success! Stats recorded.')\r
726         return Response('200 OK')\r
727     except Exception as e:\r
728         if session:\r
729             session.rollback()\r
730         return e\r