]> git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/views/submission.py
Missed a gametype in the supported_games list.
[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(gametype, 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 (for non-CTS games)\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     ... or for CTS, which doesn't record accuracy events\r
76 \r
77     1) a match in which no player made a fastest lap AND was\r
78     on the scoreboard\r
79     """\r
80     r = re.compile(r'acc-.*-cnt-fired')\r
81     flg_nonzero_score = False\r
82     flg_acc_events = False\r
83     flg_fastest_lap = False\r
84 \r
85     for events in players:\r
86         if is_real_player(events) and played_in_game(events):\r
87             for (key,value) in events.items():\r
88                 if key == 'scoreboard-score' and value != 0:\r
89                     flg_nonzero_score = True\r
90                 if r.search(key):\r
91                     flg_acc_events = True\r
92                 if key == 'scoreboard-fastest':\r
93                     flg_fastest_lap = True\r
94 \r
95     if gametype == 'cts':\r
96         return not flg_fastest_lap\r
97     else:\r
98         return not (flg_nonzero_score and flg_acc_events)\r
99 \r
100 \r
101 def get_remote_addr(request):\r
102     """Get the Xonotic server's IP address"""\r
103     if 'X-Forwarded-For' in request.headers:\r
104         return request.headers['X-Forwarded-For']\r
105     else:\r
106         return request.remote_addr\r
107 \r
108 \r
109 def is_supported_gametype(gametype, version):\r
110     """Whether a gametype is supported or not"""\r
111     is_supported = False\r
112 \r
113     # if the type can be supported, but with version constraints, uncomment\r
114     # here and add the restriction for a specific version below\r
115     supported_game_types = (\r
116             'as',\r
117             'ca',\r
118             # 'cq',\r
119             'ctf',\r
120             'cts',\r
121             'dm',\r
122             'dom',\r
123             'ft', 'freezetag',\r
124             'ka', 'keepaway',\r
125             'kh',\r
126             # 'lms',\r
127             'nb', 'nexball',\r
128             # 'rc',\r
129             'rune',\r
130             'tdm',\r
131         )\r
132 \r
133     if gametype in supported_game_types:\r
134         is_supported = True\r
135     else:\r
136         is_supported = False\r
137 \r
138     # some game types were buggy before revisions, thus this additional filter\r
139     if gametype == 'ca' and version <= 5:\r
140         is_supported = False\r
141 \r
142     return is_supported\r
143 \r
144 \r
145 def verify_request(request):\r
146     """Verify requests using the d0_blind_id library"""\r
147 \r
148     # first determine if we should be verifying or not\r
149     val_verify_requests = request.registry.settings.get('xonstat.verify_requests', 'true')\r
150     if val_verify_requests == "true":\r
151         flg_verify_requests = True\r
152     else:\r
153         flg_verify_requests = False\r
154 \r
155     try:\r
156         (idfp, status) = d0_blind_id_verify(\r
157                 sig=request.headers['X-D0-Blind-Id-Detached-Signature'],\r
158                 querystring='',\r
159                 postdata=request.body)\r
160 \r
161         log.debug('\nidfp: {0}\nstatus: {1}'.format(idfp, status))\r
162     except:\r
163         idfp = None\r
164         status = None\r
165 \r
166     if flg_verify_requests and not idfp:\r
167         log.debug("ERROR: Unverified request")\r
168         raise pyramid.httpexceptions.HTTPUnauthorized("Unverified request")\r
169 \r
170     return (idfp, status)\r
171 \r
172 \r
173 def do_precondition_checks(request, game_meta, raw_players):\r
174     """Precondition checks for ALL gametypes.\r
175        These do not require a database connection."""\r
176     if not has_required_metadata(game_meta):\r
177         log.debug("ERROR: Required game meta missing")\r
178         raise pyramid.httpexceptions.HTTPUnprocessableEntity("Missing game meta")\r
179 \r
180     try:\r
181         version = int(game_meta['V'])\r
182     except:\r
183         log.debug("ERROR: Required game meta invalid")\r
184         raise pyramid.httpexceptions.HTTPUnprocessableEntity("Invalid game meta")\r
185 \r
186     if not is_supported_gametype(game_meta['G'], version):\r
187         log.debug("ERROR: Unsupported gametype")\r
188         raise pyramid.httpexceptions.HTTPOk("OK")\r
189 \r
190     if not has_minimum_real_players(request.registry.settings, raw_players):\r
191         log.debug("ERROR: Not enough real players")\r
192         raise pyramid.httpexceptions.HTTPOk("OK")\r
193 \r
194     if is_blank_game(game_meta['G'], raw_players):\r
195         log.debug("ERROR: Blank game")\r
196         raise pyramid.httpexceptions.HTTPOk("OK")\r
197 \r
198 \r
199 def is_real_player(events):\r
200     """\r
201     Determines if a given set of events correspond with a non-bot\r
202     """\r
203     if not events['P'].startswith('bot'):\r
204         return True\r
205     else:\r
206         return False\r
207 \r
208 \r
209 def played_in_game(events):\r
210     """\r
211     Determines if a given set of player events correspond with a player who\r
212     played in the game (matches 1 and scoreboardvalid 1)\r
213     """\r
214     if 'matches' in events and 'scoreboardvalid' in events:\r
215         return True\r
216     else:\r
217         return False\r
218 \r
219 \r
220 def num_real_players(player_events):\r
221     """\r
222     Returns the number of real players (those who played\r
223     and are on the scoreboard).\r
224     """\r
225     real_players = 0\r
226 \r
227     for events in player_events:\r
228         if is_real_player(events) and played_in_game(events):\r
229             real_players += 1\r
230 \r
231     return real_players\r
232 \r
233 \r
234 def has_minimum_real_players(settings, player_events):\r
235     """\r
236     Determines if the collection of player events has enough "real" players\r
237     to store in the database. The minimum setting comes from the config file\r
238     under the setting xonstat.minimum_real_players.\r
239     """\r
240     flg_has_min_real_players = True\r
241 \r
242     try:\r
243         minimum_required_players = int(\r
244                 settings['xonstat.minimum_required_players'])\r
245     except:\r
246         minimum_required_players = 2\r
247 \r
248     real_players = num_real_players(player_events)\r
249 \r
250     if real_players < minimum_required_players:\r
251         flg_has_min_real_players = False\r
252 \r
253     return flg_has_min_real_players\r
254 \r
255 \r
256 def has_required_metadata(metadata):\r
257     """\r
258     Determines if a give set of metadata has enough data to create a game,\r
259     server, and map with.\r
260     """\r
261     flg_has_req_metadata = True\r
262 \r
263     if 'T' not in metadata or\\r
264         'G' not in metadata or\\r
265         'M' not in metadata or\\r
266         'I' not in metadata or\\r
267         'S' not in metadata:\r
268             flg_has_req_metadata = False\r
269 \r
270     return flg_has_req_metadata\r
271 \r
272 \r
273 def should_do_weapon_stats(game_type_cd):\r
274     """True of the game type should record weapon stats. False otherwise."""\r
275     if game_type_cd in 'cts':\r
276         return False\r
277     else:\r
278         return True\r
279 \r
280 \r
281 def should_do_elos(game_type_cd):\r
282     """True of the game type should process Elos. False otherwise."""\r
283     elo_game_types = ('duel', 'dm', 'ca', 'ctf', 'tdm', 'kh',\r
284             'ka', 'ft', 'freezetag')\r
285 \r
286     if game_type_cd in elo_game_types:\r
287         return True\r
288     else:\r
289         return False\r
290 \r
291 \r
292 def register_new_nick(session, player, new_nick):\r
293     """\r
294     Change the player record's nick to the newly found nick. Store the old\r
295     nick in the player_nicks table for that player.\r
296 \r
297     session - SQLAlchemy database session factory\r
298     player - player record whose nick is changing\r
299     new_nick - the new nickname\r
300     """\r
301     # see if that nick already exists\r
302     stripped_nick = strip_colors(qfont_decode(player.nick))\r
303     try:\r
304         player_nick = session.query(PlayerNick).filter_by(\r
305             player_id=player.player_id, stripped_nick=stripped_nick).one()\r
306     except NoResultFound, e:\r
307         # player_id/stripped_nick not found, create one\r
308         # but we don't store "Anonymous Player #N"\r
309         if not re.search('^Anonymous Player #\d+$', player.nick):\r
310             player_nick = PlayerNick()\r
311             player_nick.player_id = player.player_id\r
312             player_nick.stripped_nick = stripped_nick\r
313             player_nick.nick = player.nick\r
314             session.add(player_nick)\r
315 \r
316     # We change to the new nick regardless\r
317     player.nick = new_nick\r
318     player.stripped_nick = strip_colors(qfont_decode(new_nick))\r
319     session.add(player)\r
320 \r
321 \r
322 def update_fastest_cap(session, player_id, game_id,  map_id, captime):\r
323     """\r
324     Check the fastest cap time for the player and map. If there isn't\r
325     one, insert one. If there is, check if the passed time is faster.\r
326     If so, update!\r
327     """\r
328     # we don't record fastest cap times for bots or anonymous players\r
329     if player_id <= 2:\r
330         return\r
331 \r
332     # see if a cap entry exists already\r
333     # then check to see if the new captime is faster\r
334     try:\r
335         cur_fastest_cap = session.query(PlayerCaptime).filter_by(\r
336             player_id=player_id, map_id=map_id).one()\r
337 \r
338         # current captime is faster, so update\r
339         if captime < cur_fastest_cap.fastest_cap:\r
340             cur_fastest_cap.fastest_cap = captime\r
341             cur_fastest_cap.game_id = game_id\r
342             cur_fastest_cap.create_dt = datetime.datetime.utcnow()\r
343             session.add(cur_fastest_cap)\r
344 \r
345     except NoResultFound, e:\r
346         # none exists, so insert\r
347         cur_fastest_cap = PlayerCaptime(player_id, game_id, map_id, captime)\r
348         session.add(cur_fastest_cap)\r
349         session.flush()\r
350 \r
351 \r
352 def get_or_create_server(session=None, name=None, hashkey=None, ip_addr=None,\r
353         revision=None):\r
354     """\r
355     Find a server by name or create one if not found. Parameters:\r
356 \r
357     session - SQLAlchemy database session factory\r
358     name - server name of the server to be found or created\r
359     hashkey - server hashkey\r
360     """\r
361     try:\r
362         # find one by that name, if it exists\r
363         server = session.query(Server).filter_by(name=name).one()\r
364 \r
365         # store new hashkey\r
366         if server.hashkey != hashkey:\r
367             server.hashkey = hashkey\r
368             session.add(server)\r
369 \r
370         # store new IP address\r
371         if server.ip_addr != ip_addr:\r
372             server.ip_addr = ip_addr\r
373             session.add(server)\r
374 \r
375         # store new revision\r
376         if server.revision != revision:\r
377             server.revision = revision\r
378             session.add(server)\r
379 \r
380         log.debug("Found existing server {0}".format(server.server_id))\r
381 \r
382     except MultipleResultsFound, e:\r
383         # multiple found, so also filter by hashkey\r
384         server = session.query(Server).filter_by(name=name).\\r
385                 filter_by(hashkey=hashkey).one()\r
386         log.debug("Found existing server {0}".format(server.server_id))\r
387 \r
388     except NoResultFound, e:\r
389         # not found, create one\r
390         server = Server(name=name, hashkey=hashkey)\r
391         session.add(server)\r
392         session.flush()\r
393         log.debug("Created server {0} with hashkey {1}".format(\r
394             server.server_id, server.hashkey))\r
395 \r
396     return server\r
397 \r
398 \r
399 def get_or_create_map(session=None, name=None):\r
400     """\r
401     Find a map by name or create one if not found. Parameters:\r
402 \r
403     session - SQLAlchemy database session factory\r
404     name - map name of the map to be found or created\r
405     """\r
406     try:\r
407         # find one by the name, if it exists\r
408         gmap = session.query(Map).filter_by(name=name).one()\r
409         log.debug("Found map id {0}: {1}".format(gmap.map_id,\r
410             gmap.name))\r
411     except NoResultFound, e:\r
412         gmap = Map(name=name)\r
413         session.add(gmap)\r
414         session.flush()\r
415         log.debug("Created map id {0}: {1}".format(gmap.map_id,\r
416             gmap.name))\r
417     except MultipleResultsFound, e:\r
418         # multiple found, so use the first one but warn\r
419         log.debug(e)\r
420         gmaps = session.query(Map).filter_by(name=name).order_by(\r
421                 Map.map_id).all()\r
422         gmap = gmaps[0]\r
423         log.debug("Found map id {0}: {1} but found \\r
424                 multiple".format(gmap.map_id, gmap.name))\r
425 \r
426     return gmap\r
427 \r
428 \r
429 def create_game(session=None, start_dt=None, game_type_cd=None,\r
430         server_id=None, map_id=None, winner=None, match_id=None,\r
431         duration=None):\r
432     """\r
433     Creates a game. Parameters:\r
434 \r
435     session - SQLAlchemy database session factory\r
436     start_dt - when the game started (datetime object)\r
437     game_type_cd - the game type of the game being played\r
438     server_id - server identifier of the server hosting the game\r
439     map_id - map on which the game was played\r
440     winner - the team id of the team that won\r
441     """\r
442     seq = Sequence('games_game_id_seq')\r
443     game_id = session.execute(seq)\r
444     game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,\r
445                 server_id=server_id, map_id=map_id, winner=winner)\r
446     game.match_id = match_id\r
447 \r
448     try:\r
449         game.duration = datetime.timedelta(seconds=int(round(float(duration))))\r
450     except:\r
451         pass\r
452 \r
453     try:\r
454         session.query(Game).filter(Game.server_id==server_id).\\r
455                 filter(Game.match_id==match_id).one()\r
456 \r
457         log.debug("Error: game with same server and match_id found! Ignoring.")\r
458 \r
459         # if a game under the same server and match_id found,\r
460         # this is a duplicate game and can be ignored\r
461         raise pyramid.httpexceptions.HTTPOk('OK')\r
462     except NoResultFound, e:\r
463         # server_id/match_id combination not found. game is ok to insert\r
464         session.add(game)\r
465         session.flush()\r
466         log.debug("Created game id {0} on server {1}, map {2} at \\r
467                 {3}".format(game.game_id,\r
468                     server_id, map_id, start_dt))\r
469 \r
470     return game\r
471 \r
472 \r
473 def get_or_create_player(session=None, hashkey=None, nick=None):\r
474     """\r
475     Finds a player by hashkey or creates a new one (along with a\r
476     corresponding hashkey entry. Parameters:\r
477 \r
478     session - SQLAlchemy database session factory\r
479     hashkey - hashkey of the player to be found or created\r
480     nick - nick of the player (in case of a first time create)\r
481     """\r
482     # if we have a bot\r
483     if re.search('^bot#\d+$', hashkey) or re.search('^bot#\d+#', hashkey):\r
484         player = session.query(Player).filter_by(player_id=1).one()\r
485     # if we have an untracked player\r
486     elif re.search('^player#\d+$', hashkey):\r
487         player = session.query(Player).filter_by(player_id=2).one()\r
488     # else it is a tracked player\r
489     else:\r
490         # see if the player is already in the database\r
491         # if not, create one and the hashkey along with it\r
492         try:\r
493             hk = session.query(Hashkey).filter_by(\r
494                     hashkey=hashkey).one()\r
495             player = session.query(Player).filter_by(\r
496                     player_id=hk.player_id).one()\r
497             log.debug("Found existing player {0} with hashkey {1}".format(\r
498                 player.player_id, hashkey))\r
499         except:\r
500             player = Player()\r
501             session.add(player)\r
502             session.flush()\r
503 \r
504             # if nick is given to us, use it. If not, use "Anonymous Player"\r
505             # with a suffix added for uniqueness.\r
506             if nick:\r
507                 player.nick = nick[:128]\r
508                 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))\r
509             else:\r
510                 player.nick = "Anonymous Player #{0}".format(player.player_id)\r
511                 player.stripped_nick = player.nick\r
512 \r
513             hk = Hashkey(player_id=player.player_id, hashkey=hashkey)\r
514             session.add(hk)\r
515             log.debug("Created player {0} ({2}) with hashkey {1}".format(\r
516                 player.player_id, hashkey, player.nick.encode('utf-8')))\r
517 \r
518     return player\r
519 \r
520 \r
521 def create_default_game_stat(session, game_type_cd):\r
522     """Creates a blanked-out pgstat record for the given game type"""\r
523 \r
524     # this is what we have to do to get partitioned records in - grab the\r
525     # sequence value first, then insert using the explicit ID (vs autogenerate)\r
526     seq = Sequence('player_game_stats_player_game_stat_id_seq')\r
527     pgstat_id = session.execute(seq)\r
528     pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,\r
529             create_dt=datetime.datetime.utcnow())\r
530 \r
531     if game_type_cd == 'as':\r
532         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0\r
533 \r
534     if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':\r
535         pgstat.kills = pgstat.deaths = pgstat.suicides = 0\r
536 \r
537     if game_type_cd == 'cq':\r
538         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0\r
539         pgstat.drops = 0\r
540 \r
541     if game_type_cd == 'ctf':\r
542         pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0\r
543         pgstat.returns = pgstat.carrier_frags = 0\r
544 \r
545     if game_type_cd == 'cts':\r
546         pgstat.deaths = 0\r
547 \r
548     if game_type_cd == 'dom':\r
549         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0\r
550         pgstat.drops = 0\r
551 \r
552     if game_type_cd == 'ft':\r
553         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0\r
554 \r
555     if game_type_cd == 'ka':\r
556         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0\r
557         pgstat.carrier_frags = 0\r
558         pgstat.time = datetime.timedelta(seconds=0)\r
559 \r
560     if game_type_cd == 'kh':\r
561         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0\r
562         pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0\r
563         pgstat.carrier_frags = 0\r
564 \r
565     if game_type_cd == 'lms':\r
566         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0\r
567 \r
568     if game_type_cd == 'nb':\r
569         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0\r
570         pgstat.drops = 0\r
571 \r
572     if game_type_cd == 'rc':\r
573         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0\r
574 \r
575     return pgstat\r
576 \r
577 \r
578 def create_game_stat(session, game_meta, game, server, gmap, player, events):\r
579     """Game stats handler for all game types"""\r
580 \r
581     pgstat = create_default_game_stat(session, game.game_type_cd)\r
582 \r
583     # these fields should be on every pgstat record\r
584     pgstat.game_id       = game.game_id\r
585     pgstat.player_id     = player.player_id\r
586     pgstat.nick          = events.get('n', 'Anonymous Player')[:128]\r
587     pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))\r
588     pgstat.score         = int(round(float(events.get('scoreboard-score', 0))))\r
589     pgstat.alivetime     = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))\r
590     pgstat.rank          = int(events.get('rank', None))\r
591     pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))\r
592 \r
593     if pgstat.nick != player.nick \\r
594             and player.player_id > 2 \\r
595             and pgstat.nick != 'Anonymous Player':\r
596         register_new_nick(session, player, pgstat.nick)\r
597 \r
598     wins = False\r
599 \r
600     # gametype-specific stuff is handled here. if passed to us, we store it\r
601     for (key,value) in events.items():\r
602         if key == 'wins': wins = True\r
603         if key == 't': pgstat.team = int(value)\r
604 \r
605         if key == 'scoreboard-drops': pgstat.drops = int(value)\r
606         if key == 'scoreboard-returns': pgstat.returns = int(value)\r
607         if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)\r
608         if key == 'scoreboard-pickups': pgstat.pickups = int(value)\r
609         if key == 'scoreboard-caps': pgstat.captures = int(value)\r
610         if key == 'scoreboard-score': pgstat.score = int(round(float(value)))\r
611         if key == 'scoreboard-deaths': pgstat.deaths = int(value)\r
612         if key == 'scoreboard-kills': pgstat.kills = int(value)\r
613         if key == 'scoreboard-suicides': pgstat.suicides = int(value)\r
614         if key == 'scoreboard-objectives': pgstat.collects = int(value)\r
615         if key == 'scoreboard-captured': pgstat.captures = int(value)\r
616         if key == 'scoreboard-released': pgstat.drops = int(value)\r
617         if key == 'scoreboard-fastest':\r
618             pgstat.fastest = datetime.timedelta(seconds=float(value)/100)\r
619         if key == 'scoreboard-takes': pgstat.pickups = int(value)\r
620         if key == 'scoreboard-ticks': pgstat.drops = int(value)\r
621         if key == 'scoreboard-revivals': pgstat.revivals = int(value)\r
622         if key == 'scoreboard-bctime':\r
623             pgstat.time = datetime.timedelta(seconds=int(value))\r
624         if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)\r
625         if key == 'scoreboard-losses': pgstat.drops = int(value)\r
626         if key == 'scoreboard-pushes': pgstat.pushes = int(value)\r
627         if key == 'scoreboard-destroyed': pgstat.destroys = int(value)\r
628         if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)\r
629         if key == 'scoreboard-lives': pgstat.lives = int(value)\r
630         if key == 'scoreboard-goals': pgstat.captures = int(value)\r
631         if key == 'scoreboard-faults': pgstat.drops = int(value)\r
632         if key == 'scoreboard-laps': pgstat.laps = int(value)\r
633 \r
634         if key == 'avglatency': pgstat.avg_latency = float(value)\r
635         if key == 'scoreboard-captime':\r
636             pgstat.fastest_cap = datetime.timedelta(seconds=float(value)/100)\r
637             if game.game_type_cd == 'ctf':\r
638                 update_fastest_cap(session, player.player_id, game.game_id,\r
639                         gmap.map_id, pgstat.fastest_cap)\r
640 \r
641     # there is no "winning team" field, so we have to derive it\r
642     if wins and pgstat.team is not None and game.winner is None:\r
643         game.winner = pgstat.team\r
644         session.add(game)\r
645 \r
646     session.add(pgstat)\r
647 \r
648     return pgstat\r
649 \r
650 \r
651 def create_weapon_stats(session, game_meta, game, player, pgstat, events):\r
652     """Weapon stats handler for all game types"""\r
653     pwstats = []\r
654 \r
655     # Version 1 of stats submissions doubled the data sent.\r
656     # To counteract this we divide the data by 2 only for\r
657     # POSTs coming from version 1.\r
658     try:\r
659         version = int(game_meta['V'])\r
660         if version == 1:\r
661             is_doubled = True\r
662             log.debug('NOTICE: found a version 1 request, halving the weapon stats...')\r
663         else:\r
664             is_doubled = False\r
665     except:\r
666         is_doubled = False\r
667 \r
668     for (key,value) in events.items():\r
669         matched = re.search("acc-(.*?)-cnt-fired", key)\r
670         if matched:\r
671             weapon_cd = matched.group(1)\r
672             seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')\r
673             pwstat_id = session.execute(seq)\r
674             pwstat = PlayerWeaponStat()\r
675             pwstat.player_weapon_stats_id = pwstat_id\r
676             pwstat.player_id = player.player_id\r
677             pwstat.game_id = game.game_id\r
678             pwstat.player_game_stat_id = pgstat.player_game_stat_id\r
679             pwstat.weapon_cd = weapon_cd\r
680 \r
681             if 'n' in events:\r
682                 pwstat.nick = events['n']\r
683             else:\r
684                 pwstat.nick = events['P']\r
685 \r
686             if 'acc-' + weapon_cd + '-cnt-fired' in events:\r
687                 pwstat.fired = int(round(float(\r
688                         events['acc-' + weapon_cd + '-cnt-fired'])))\r
689             if 'acc-' + weapon_cd + '-fired' in events:\r
690                 pwstat.max = int(round(float(\r
691                         events['acc-' + weapon_cd + '-fired'])))\r
692             if 'acc-' + weapon_cd + '-cnt-hit' in events:\r
693                 pwstat.hit = int(round(float(\r
694                         events['acc-' + weapon_cd + '-cnt-hit'])))\r
695             if 'acc-' + weapon_cd + '-hit' in events:\r
696                 pwstat.actual = int(round(float(\r
697                         events['acc-' + weapon_cd + '-hit'])))\r
698             if 'acc-' + weapon_cd + '-frags' in events:\r
699                 pwstat.frags = int(round(float(\r
700                         events['acc-' + weapon_cd + '-frags'])))\r
701 \r
702             if is_doubled:\r
703                 pwstat.fired = pwstat.fired/2\r
704                 pwstat.max = pwstat.max/2\r
705                 pwstat.hit = pwstat.hit/2\r
706                 pwstat.actual = pwstat.actual/2\r
707                 pwstat.frags = pwstat.frags/2\r
708 \r
709             session.add(pwstat)\r
710             pwstats.append(pwstat)\r
711 \r
712     return pwstats\r
713 \r
714 \r
715 def create_elos(session, game):\r
716     """Elo handler for all game types."""\r
717     try:\r
718         process_elos(game, session)\r
719     except Exception as e:\r
720         log.debug('Error (non-fatal): elo processing failed.')\r
721 \r
722 \r
723 def submit_stats(request):\r
724     """\r
725     Entry handler for POST stats submissions.\r
726     """\r
727     try:\r
728         # placeholder for the actual session\r
729         session = None\r
730 \r
731         log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +\r
732                 "----- END REQUEST BODY -----\n\n")\r
733 \r
734         (idfp, status) = verify_request(request)\r
735         (game_meta, raw_players) = parse_stats_submission(request.body)\r
736         revision = game_meta.get('R', 'unknown')\r
737         duration = game_meta.get('D', None)\r
738 \r
739         # only players present at the end of the match are eligible for stats\r
740         raw_players = filter(played_in_game, raw_players)\r
741 \r
742         do_precondition_checks(request, game_meta, raw_players)\r
743 \r
744         # the "duel" gametype is fake\r
745         if len(raw_players) == 2 \\r
746             and num_real_players(raw_players) == 2 \\r
747             and game_meta['G'] == 'dm':\r
748             game_meta['G'] = 'duel'\r
749 \r
750         #----------------------------------------------------------------------\r
751         # Actual setup (inserts/updates) below here\r
752         #----------------------------------------------------------------------\r
753         session = DBSession()\r
754 \r
755         game_type_cd = game_meta['G']\r
756 \r
757         # All game types create Game, Server, Map, and Player records\r
758         # the same way.\r
759         server = get_or_create_server(\r
760                 session  = session,\r
761                 hashkey  = idfp,\r
762                 name     = game_meta['S'],\r
763                 revision = revision,\r
764                 ip_addr  = get_remote_addr(request))\r
765 \r
766         gmap = get_or_create_map(\r
767                 session = session,\r
768                 name    = game_meta['M'])\r
769 \r
770         game = create_game(\r
771                 session      = session,\r
772                 start_dt     = datetime.datetime.utcnow(),\r
773                 server_id    = server.server_id,\r
774                 game_type_cd = game_type_cd,\r
775                 map_id       = gmap.map_id,\r
776                 match_id     = game_meta['I'],\r
777                 duration     = duration)\r
778 \r
779         for events in raw_players:\r
780             player = get_or_create_player(\r
781                 session = session,\r
782                 hashkey = events['P'],\r
783                 nick    = events.get('n', None))\r
784 \r
785             pgstat = create_game_stat(session, game_meta, game, server,\r
786                     gmap, player, events)\r
787 \r
788             if should_do_weapon_stats(game_type_cd) and player.player_id > 1:\r
789                 pwstats = create_weapon_stats(session, game_meta, game, player,\r
790                         pgstat, events)\r
791 \r
792         if should_do_elos(game_type_cd):\r
793             create_elos(session, game)\r
794 \r
795         session.commit()\r
796         log.debug('Success! Stats recorded.')\r
797         return Response('200 OK')\r
798     except Exception as e:\r
799         if session:\r
800             session.rollback()\r
801         return e\r