]> de.git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/views/submission.py
No Elo for keyhunt, remove old ka name.
[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', 'ka', 'ft')\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 = 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)\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