fd70934d0d53d8744059b0067b81df7d7b87c764
[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     game_type_cd = game.game_type_cd\r
604 \r
605     pgstat = create_default_game_stat(session, game_type_cd)\r
606 \r
607     # these fields should be on every pgstat record\r
608     pgstat.game_id       = game.game_id\r
609     pgstat.player_id     = player.player_id\r
610     pgstat.nick          = events.get('n', 'Anonymous Player')[:128]\r
611     pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))\r
612     pgstat.score         = int(round(float(events.get('scoreboard-score', 0))))\r
613     pgstat.alivetime     = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))\r
614     pgstat.rank          = int(events.get('rank', None))\r
615     pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))\r
616 \r
617     if pgstat.nick != player.nick \\r
618             and player.player_id > 2 \\r
619             and pgstat.nick != 'Anonymous Player':\r
620         register_new_nick(session, player, pgstat.nick)\r
621 \r
622     wins = False\r
623 \r
624     # gametype-specific stuff is handled here. if passed to us, we store it\r
625     for (key,value) in events.items():\r
626         if key == 'wins': wins = True\r
627         if key == 't': pgstat.team = int(value)\r
628 \r
629         if key == 'scoreboard-drops': pgstat.drops = int(value)\r
630         if key == 'scoreboard-returns': pgstat.returns = int(value)\r
631         if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)\r
632         if key == 'scoreboard-pickups': pgstat.pickups = int(value)\r
633         if key == 'scoreboard-caps': pgstat.captures = int(value)\r
634         if key == 'scoreboard-score': pgstat.score = int(round(float(value)))\r
635         if key == 'scoreboard-deaths': pgstat.deaths = int(value)\r
636         if key == 'scoreboard-kills': pgstat.kills = int(value)\r
637         if key == 'scoreboard-suicides': pgstat.suicides = int(value)\r
638         if key == 'scoreboard-objectives': pgstat.collects = int(value)\r
639         if key == 'scoreboard-captured': pgstat.captures = int(value)\r
640         if key == 'scoreboard-released': pgstat.drops = int(value)\r
641         if key == 'scoreboard-fastest':\r
642             pgstat.fastest = datetime.timedelta(seconds=float(value)/100)\r
643         if key == 'scoreboard-takes': pgstat.pickups = int(value)\r
644         if key == 'scoreboard-ticks': pgstat.drops = int(value)\r
645         if key == 'scoreboard-revivals': pgstat.revivals = int(value)\r
646         if key == 'scoreboard-bctime':\r
647             pgstat.time = datetime.timedelta(seconds=int(value))\r
648         if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)\r
649         if key == 'scoreboard-losses': pgstat.drops = int(value)\r
650         if key == 'scoreboard-pushes': pgstat.pushes = int(value)\r
651         if key == 'scoreboard-destroyed': pgstat.destroys = int(value)\r
652         if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)\r
653         if key == 'scoreboard-lives': pgstat.lives = int(value)\r
654         if key == 'scoreboard-goals': pgstat.captures = int(value)\r
655         if key == 'scoreboard-faults': pgstat.drops = int(value)\r
656         if key == 'scoreboard-laps': pgstat.laps = int(value)\r
657 \r
658         if key == 'avglatency': pgstat.avg_latency = float(value)\r
659         if key == 'scoreboard-captime':\r
660             pgstat.fastest = datetime.timedelta(seconds=float(value)/100)\r
661             if game.game_type_cd == 'ctf':\r
662                 update_fastest_cap(session, player.player_id, game.game_id,\r
663                         gmap.map_id, pgstat.fastest)\r
664 \r
665     # there is no "winning team" field, so we have to derive it\r
666     if wins and pgstat.team is not None and game.winner is None:\r
667         game.winner = pgstat.team\r
668         session.add(game)\r
669 \r
670     session.add(pgstat)\r
671 \r
672     return pgstat\r
673 \r
674 \r
675 def create_weapon_stats(session, game_meta, game, player, pgstat, events):\r
676     """Weapon stats handler for all game types"""\r
677     pwstats = []\r
678 \r
679     # Version 1 of stats submissions doubled the data sent.\r
680     # To counteract this we divide the data by 2 only for\r
681     # POSTs coming from version 1.\r
682     try:\r
683         version = int(game_meta['V'])\r
684         if version == 1:\r
685             is_doubled = True\r
686             log.debug('NOTICE: found a version 1 request, halving the weapon stats...')\r
687         else:\r
688             is_doubled = False\r
689     except:\r
690         is_doubled = False\r
691 \r
692     for (key,value) in events.items():\r
693         matched = re.search("acc-(.*?)-cnt-fired", key)\r
694         if matched:\r
695             weapon_cd = matched.group(1)\r
696             seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')\r
697             pwstat_id = session.execute(seq)\r
698             pwstat = PlayerWeaponStat()\r
699             pwstat.player_weapon_stats_id = pwstat_id\r
700             pwstat.player_id = player.player_id\r
701             pwstat.game_id = game.game_id\r
702             pwstat.player_game_stat_id = pgstat.player_game_stat_id\r
703             pwstat.weapon_cd = weapon_cd\r
704 \r
705             if 'n' in events:\r
706                 pwstat.nick = events['n']\r
707             else:\r
708                 pwstat.nick = events['P']\r
709 \r
710             if 'acc-' + weapon_cd + '-cnt-fired' in events:\r
711                 pwstat.fired = int(round(float(\r
712                         events['acc-' + weapon_cd + '-cnt-fired'])))\r
713             if 'acc-' + weapon_cd + '-fired' in events:\r
714                 pwstat.max = int(round(float(\r
715                         events['acc-' + weapon_cd + '-fired'])))\r
716             if 'acc-' + weapon_cd + '-cnt-hit' in events:\r
717                 pwstat.hit = int(round(float(\r
718                         events['acc-' + weapon_cd + '-cnt-hit'])))\r
719             if 'acc-' + weapon_cd + '-hit' in events:\r
720                 pwstat.actual = int(round(float(\r
721                         events['acc-' + weapon_cd + '-hit'])))\r
722             if 'acc-' + weapon_cd + '-frags' in events:\r
723                 pwstat.frags = int(round(float(\r
724                         events['acc-' + weapon_cd + '-frags'])))\r
725 \r
726             if is_doubled:\r
727                 pwstat.fired = pwstat.fired/2\r
728                 pwstat.max = pwstat.max/2\r
729                 pwstat.hit = pwstat.hit/2\r
730                 pwstat.actual = pwstat.actual/2\r
731                 pwstat.frags = pwstat.frags/2\r
732 \r
733             session.add(pwstat)\r
734             pwstats.append(pwstat)\r
735 \r
736     return pwstats\r
737 \r
738 \r
739 def create_elos(session, game):\r
740     """Elo handler for all game types."""\r
741     try:\r
742         process_elos(game, session)\r
743     except Exception as e:\r
744         log.debug('Error (non-fatal): elo processing failed.')\r
745 \r
746 \r
747 def submit_stats(request):\r
748     """\r
749     Entry handler for POST stats submissions.\r
750     """\r
751     try:\r
752         # placeholder for the actual session\r
753         session = None\r
754 \r
755         log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +\r
756                 "----- END REQUEST BODY -----\n\n")\r
757 \r
758         (idfp, status) = verify_request(request)\r
759         (game_meta, raw_players) = parse_stats_submission(request.body)\r
760         revision = game_meta.get('R', 'unknown')\r
761         duration = game_meta.get('D', None)\r
762 \r
763         # only players present at the end of the match are eligible for stats\r
764         raw_players = filter(played_in_game, raw_players)\r
765 \r
766         do_precondition_checks(request, game_meta, raw_players)\r
767 \r
768         # the "duel" gametype is fake\r
769         if len(raw_players) == 2 \\r
770             and num_real_players(raw_players) == 2 \\r
771             and game_meta['G'] == 'dm':\r
772             game_meta['G'] = 'duel'\r
773 \r
774         #----------------------------------------------------------------------\r
775         # Actual setup (inserts/updates) below here\r
776         #----------------------------------------------------------------------\r
777         session = DBSession()\r
778 \r
779         game_type_cd = game_meta['G']\r
780 \r
781         # All game types create Game, Server, Map, and Player records\r
782         # the same way.\r
783         server = get_or_create_server(\r
784                 session  = session,\r
785                 hashkey  = idfp,\r
786                 name     = game_meta['S'],\r
787                 revision = revision,\r
788                 ip_addr  = get_remote_addr(request),\r
789                 port     = game_meta.get('U', None))\r
790 \r
791         gmap = get_or_create_map(\r
792                 session = session,\r
793                 name    = game_meta['M'])\r
794 \r
795         game = create_game(\r
796                 session      = session,\r
797                 start_dt     = datetime.datetime.utcnow(),\r
798                 server_id    = server.server_id,\r
799                 game_type_cd = game_type_cd,\r
800                 map_id       = gmap.map_id,\r
801                 match_id     = game_meta['I'],\r
802                 duration     = duration,\r
803                 mod          = game_meta.get('O', None))\r
804 \r
805         for events in raw_players:\r
806             player = get_or_create_player(\r
807                 session = session,\r
808                 hashkey = events['P'],\r
809                 nick    = events.get('n', None))\r
810 \r
811             pgstat = create_game_stat(session, game_meta, game, server,\r
812                     gmap, player, events)\r
813 \r
814             if should_do_weapon_stats(game_type_cd) and player.player_id > 1:\r
815                 pwstats = create_weapon_stats(session, game_meta, game, player,\r
816                         pgstat, events)\r
817 \r
818         if should_do_elos(game_type_cd):\r
819             create_elos(session, game)\r
820 \r
821         session.commit()\r
822         log.debug('Success! Stats recorded.')\r
823         return Response('200 OK')\r
824     except Exception as e:\r
825         if session:\r
826             session.rollback()\r
827         return e\r