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