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