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