Add anticheat logging.
[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_anticheats(session, pgstat, game, player, events):\r
670     """Anticheats handler for all game types"""\r
671 \r
672     anticheats = []\r
673 \r
674     # all anticheat events are prefixed by "anticheat"\r
675     for (key,value) in events.items():\r
676         if key.startswith("anticheat"):\r
677             try:\r
678                 ac = PlayerGameAnticheat(\r
679                     player.player_id,\r
680                     game.game_id,\r
681                     key,\r
682                     float(value)\r
683                 )\r
684                 anticheats.append(ac)\r
685                 session.add(ac)\r
686             except Exception as e:\r
687                 log.debug("Could not parse value for key %s. Ignoring." % key)\r
688 \r
689     return anticheats\r
690 \r
691 \r
692 def create_default_team_stat(session, game_type_cd):\r
693     """Creates a blanked-out teamstat record for the given game type"""\r
694 \r
695     # this is what we have to do to get partitioned records in - grab the\r
696     # sequence value first, then insert using the explicit ID (vs autogenerate)\r
697     seq = Sequence('team_game_stats_team_game_stat_id_seq')\r
698     teamstat_id = session.execute(seq)\r
699     teamstat = TeamGameStat(team_game_stat_id=teamstat_id,\r
700             create_dt=datetime.datetime.utcnow())\r
701 \r
702     # all team game modes have a score, so we'll zero that out always\r
703     teamstat.score = 0\r
704 \r
705     if game_type_cd in 'ca' 'ft' 'lms' 'ka':\r
706         teamstat.rounds = 0\r
707 \r
708     if game_type_cd == 'ctf':\r
709         teamstat.caps = 0\r
710 \r
711     return teamstat\r
712 \r
713 \r
714 def create_team_stat(session, game, events):\r
715     """Team stats handler for all game types"""\r
716 \r
717     try:\r
718         teamstat = create_default_team_stat(session, game.game_type_cd)\r
719         teamstat.game_id = game.game_id\r
720 \r
721         # we should have a team ID if we have a 'Q' event\r
722         if re.match(r'^team#\d+$', events.get('Q', '')):\r
723             team = int(events.get('Q').replace('team#', ''))\r
724             teamstat.team = team\r
725 \r
726         # gametype-specific stuff is handled here. if passed to us, we store it\r
727         for (key,value) in events.items():\r
728             if key == 'scoreboard-score': teamstat.score = int(round(float(value)))\r
729             if key == 'scoreboard-caps': teamstat.caps = int(value)\r
730             if key == 'scoreboard-rounds': teamstat.rounds = int(value)\r
731 \r
732         session.add(teamstat)\r
733     except Exception as e:\r
734         raise e\r
735 \r
736     return teamstat\r
737 \r
738 \r
739 def create_weapon_stats(session, game_meta, game, player, pgstat, events):\r
740     """Weapon stats handler for all game types"""\r
741     pwstats = []\r
742 \r
743     # Version 1 of stats submissions doubled the data sent.\r
744     # To counteract this we divide the data by 2 only for\r
745     # POSTs coming from version 1.\r
746     try:\r
747         version = int(game_meta['V'])\r
748         if version == 1:\r
749             is_doubled = True\r
750             log.debug('NOTICE: found a version 1 request, halving the weapon stats...')\r
751         else:\r
752             is_doubled = False\r
753     except:\r
754         is_doubled = False\r
755 \r
756     for (key,value) in events.items():\r
757         matched = re.search("acc-(.*?)-cnt-fired", key)\r
758         if matched:\r
759             weapon_cd = matched.group(1)\r
760             seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')\r
761             pwstat_id = session.execute(seq)\r
762             pwstat = PlayerWeaponStat()\r
763             pwstat.player_weapon_stats_id = pwstat_id\r
764             pwstat.player_id = player.player_id\r
765             pwstat.game_id = game.game_id\r
766             pwstat.player_game_stat_id = pgstat.player_game_stat_id\r
767             pwstat.weapon_cd = weapon_cd\r
768 \r
769             if 'n' in events:\r
770                 pwstat.nick = events['n']\r
771             else:\r
772                 pwstat.nick = events['P']\r
773 \r
774             if 'acc-' + weapon_cd + '-cnt-fired' in events:\r
775                 pwstat.fired = int(round(float(\r
776                         events['acc-' + weapon_cd + '-cnt-fired'])))\r
777             if 'acc-' + weapon_cd + '-fired' in events:\r
778                 pwstat.max = int(round(float(\r
779                         events['acc-' + weapon_cd + '-fired'])))\r
780             if 'acc-' + weapon_cd + '-cnt-hit' in events:\r
781                 pwstat.hit = int(round(float(\r
782                         events['acc-' + weapon_cd + '-cnt-hit'])))\r
783             if 'acc-' + weapon_cd + '-hit' in events:\r
784                 pwstat.actual = int(round(float(\r
785                         events['acc-' + weapon_cd + '-hit'])))\r
786             if 'acc-' + weapon_cd + '-frags' in events:\r
787                 pwstat.frags = int(round(float(\r
788                         events['acc-' + weapon_cd + '-frags'])))\r
789 \r
790             if is_doubled:\r
791                 pwstat.fired = pwstat.fired/2\r
792                 pwstat.max = pwstat.max/2\r
793                 pwstat.hit = pwstat.hit/2\r
794                 pwstat.actual = pwstat.actual/2\r
795                 pwstat.frags = pwstat.frags/2\r
796 \r
797             session.add(pwstat)\r
798             pwstats.append(pwstat)\r
799 \r
800     return pwstats\r
801 \r
802 \r
803 def create_elos(session, game):\r
804     """Elo handler for all game types."""\r
805     try:\r
806         process_elos(game, session)\r
807     except Exception as e:\r
808         log.debug('Error (non-fatal): elo processing failed.')\r
809 \r
810 \r
811 def submit_stats(request):\r
812     """\r
813     Entry handler for POST stats submissions.\r
814     """\r
815     try:\r
816         # placeholder for the actual session\r
817         session = None\r
818 \r
819         log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +\r
820                 "----- END REQUEST BODY -----\n\n")\r
821 \r
822         (idfp, status) = verify_request(request)\r
823         (game_meta, raw_players, raw_teams) = parse_stats_submission(request.body)\r
824         revision = game_meta.get('R', 'unknown')\r
825         duration = game_meta.get('D', None)\r
826 \r
827         # only players present at the end of the match are eligible for stats\r
828         raw_players = filter(played_in_game, raw_players)\r
829 \r
830         do_precondition_checks(request, game_meta, raw_players)\r
831 \r
832         # the "duel" gametype is fake\r
833         if len(raw_players) == 2 \\r
834             and num_real_players(raw_players) == 2 \\r
835             and game_meta['G'] == 'dm':\r
836             game_meta['G'] = 'duel'\r
837 \r
838         #----------------------------------------------------------------------\r
839         # Actual setup (inserts/updates) below here\r
840         #----------------------------------------------------------------------\r
841         session = DBSession()\r
842 \r
843         game_type_cd = game_meta['G']\r
844 \r
845         # All game types create Game, Server, Map, and Player records\r
846         # the same way.\r
847         server = get_or_create_server(\r
848                 session  = session,\r
849                 hashkey  = idfp,\r
850                 name     = game_meta['S'],\r
851                 revision = revision,\r
852                 ip_addr  = get_remote_addr(request),\r
853                 port     = game_meta.get('U', None))\r
854 \r
855         gmap = get_or_create_map(\r
856                 session = session,\r
857                 name    = game_meta['M'])\r
858 \r
859         game = create_game(\r
860                 session      = session,\r
861                 start_dt     = datetime.datetime.utcnow(),\r
862                 server_id    = server.server_id,\r
863                 game_type_cd = game_type_cd,\r
864                 map_id       = gmap.map_id,\r
865                 match_id     = game_meta['I'],\r
866                 duration     = duration,\r
867                 mod          = game_meta.get('O', None))\r
868 \r
869         for events in raw_players:\r
870             player = get_or_create_player(\r
871                 session = session,\r
872                 hashkey = events['P'],\r
873                 nick    = events.get('n', None))\r
874 \r
875             pgstat = create_game_stat(session, game_meta, game, server,\r
876                     gmap, player, events)\r
877 \r
878             if player.player_id > 1:\r
879                 anticheats = create_anticheats(session, pgstat, game, player,\r
880                     events)\r
881 \r
882             if should_do_weapon_stats(game_type_cd) and player.player_id > 1:\r
883                 pwstats = create_weapon_stats(session, game_meta, game, player,\r
884                         pgstat, events)\r
885 \r
886         for events in raw_teams:\r
887             try:\r
888                 teamstat = create_team_stat(session, game, events)\r
889             except Exception as e:\r
890                 raise e\r
891 \r
892         if should_do_elos(game_type_cd):\r
893             create_elos(session, game)\r
894 \r
895         session.commit()\r
896         log.debug('Success! Stats recorded.')\r
897         return Response('200 OK')\r
898     except Exception as e:\r
899         if session:\r
900             session.rollback()\r
901         raise e\r