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