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