]> de.git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/views/submission.py
Move the blind_id verification function to util.
[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 'T' not in metadata or\\r
260         'G' not in metadata or\\r
261         'M' not in metadata or\\r
262         'I' not in metadata or\\r
263         'S' not in metadata:\r
264             flg_has_req_metadata = False\r
265 \r
266     return flg_has_req_metadata\r
267 \r
268 \r
269 def should_do_weapon_stats(game_type_cd):\r
270     """True of the game type should record weapon stats. False otherwise."""\r
271     if game_type_cd in 'cts':\r
272         return False\r
273     else:\r
274         return True\r
275 \r
276 \r
277 def should_do_elos(game_type_cd):\r
278     """True of the game type should process Elos. False otherwise."""\r
279     elo_game_types = ('duel', 'dm', 'ca', 'ctf', 'tdm', 'ka', 'ft')\r
280 \r
281     if game_type_cd in elo_game_types:\r
282         return True\r
283     else:\r
284         return False\r
285 \r
286 \r
287 def register_new_nick(session, player, new_nick):\r
288     """\r
289     Change the player record's nick to the newly found nick. Store the old\r
290     nick in the player_nicks table for that player.\r
291 \r
292     session - SQLAlchemy database session factory\r
293     player - player record whose nick is changing\r
294     new_nick - the new nickname\r
295     """\r
296     # see if that nick already exists\r
297     stripped_nick = strip_colors(qfont_decode(player.nick))\r
298     try:\r
299         player_nick = session.query(PlayerNick).filter_by(\r
300             player_id=player.player_id, stripped_nick=stripped_nick).one()\r
301     except NoResultFound, e:\r
302         # player_id/stripped_nick not found, create one\r
303         # but we don't store "Anonymous Player #N"\r
304         if not re.search('^Anonymous Player #\d+$', player.nick):\r
305             player_nick = PlayerNick()\r
306             player_nick.player_id = player.player_id\r
307             player_nick.stripped_nick = stripped_nick\r
308             player_nick.nick = player.nick\r
309             session.add(player_nick)\r
310 \r
311     # We change to the new nick regardless\r
312     player.nick = new_nick\r
313     player.stripped_nick = strip_colors(qfont_decode(new_nick))\r
314     session.add(player)\r
315 \r
316 \r
317 def update_fastest_cap(session, player_id, game_id,  map_id, captime):\r
318     """\r
319     Check the fastest cap time for the player and map. If there isn't\r
320     one, insert one. If there is, check if the passed time is faster.\r
321     If so, update!\r
322     """\r
323     # we don't record fastest cap times for bots or anonymous players\r
324     if player_id <= 2:\r
325         return\r
326 \r
327     # see if a cap entry exists already\r
328     # then check to see if the new captime is faster\r
329     try:\r
330         cur_fastest_cap = session.query(PlayerCaptime).filter_by(\r
331             player_id=player_id, map_id=map_id).one()\r
332 \r
333         # current captime is faster, so update\r
334         if captime < cur_fastest_cap.fastest_cap:\r
335             cur_fastest_cap.fastest_cap = captime\r
336             cur_fastest_cap.game_id = game_id\r
337             cur_fastest_cap.create_dt = datetime.datetime.utcnow()\r
338             session.add(cur_fastest_cap)\r
339 \r
340     except NoResultFound, e:\r
341         # none exists, so insert\r
342         cur_fastest_cap = PlayerCaptime(player_id, game_id, map_id, captime)\r
343         session.add(cur_fastest_cap)\r
344         session.flush()\r
345 \r
346 \r
347 def get_or_create_server(session, name, hashkey, ip_addr, revision, port):\r
348     """\r
349     Find a server by name or create one if not found. Parameters:\r
350 \r
351     session - SQLAlchemy database session factory\r
352     name - server name of the server to be found or created\r
353     hashkey - server hashkey\r
354     """\r
355     server = None\r
356 \r
357     try:\r
358         port = int(port)\r
359     except:\r
360         port = None\r
361 \r
362     # finding by hashkey is preferred, but if not we will fall\r
363     # back to using name only, which can result in dupes\r
364     if hashkey is not None:\r
365         servers = session.query(Server).\\r
366             filter_by(hashkey=hashkey).\\r
367             order_by(expr.desc(Server.create_dt)).limit(1).all()\r
368 \r
369         if len(servers) > 0:\r
370             server = servers[0]\r
371             log.debug("Found existing server {0} by hashkey ({1})".format(\r
372                 server.server_id, server.hashkey))\r
373     else:\r
374         servers = session.query(Server).\\r
375             filter_by(name=name).\\r
376             order_by(expr.desc(Server.create_dt)).limit(1).all()\r
377 \r
378         if len(servers) > 0:\r
379             server = servers[0]\r
380             log.debug("Found existing server {0} by name".format(server.server_id))\r
381 \r
382     # still haven't found a server by hashkey or name, so we need to create one\r
383     if server is None:\r
384         server = Server(name=name, hashkey=hashkey)\r
385         session.add(server)\r
386         session.flush()\r
387         log.debug("Created server {0} with hashkey {1}".format(\r
388             server.server_id, server.hashkey))\r
389 \r
390     # detect changed fields\r
391     if server.name != name:\r
392         server.name = name\r
393         session.add(server)\r
394 \r
395     if server.hashkey != hashkey:\r
396         server.hashkey = hashkey\r
397         session.add(server)\r
398 \r
399     if server.ip_addr != ip_addr:\r
400         server.ip_addr = ip_addr\r
401         session.add(server)\r
402 \r
403     if server.port != port:\r
404         server.port = port\r
405         session.add(server)\r
406 \r
407     if server.revision != revision:\r
408         server.revision = revision\r
409         session.add(server)\r
410 \r
411     return server\r
412 \r
413 \r
414 def get_or_create_map(session=None, name=None):\r
415     """\r
416     Find a map by name or create one if not found. Parameters:\r
417 \r
418     session - SQLAlchemy database session factory\r
419     name - map name of the map to be found or created\r
420     """\r
421     try:\r
422         # find one by the name, if it exists\r
423         gmap = session.query(Map).filter_by(name=name).one()\r
424         log.debug("Found map id {0}: {1}".format(gmap.map_id,\r
425             gmap.name))\r
426     except NoResultFound, e:\r
427         gmap = Map(name=name)\r
428         session.add(gmap)\r
429         session.flush()\r
430         log.debug("Created map id {0}: {1}".format(gmap.map_id,\r
431             gmap.name))\r
432     except MultipleResultsFound, e:\r
433         # multiple found, so use the first one but warn\r
434         log.debug(e)\r
435         gmaps = session.query(Map).filter_by(name=name).order_by(\r
436                 Map.map_id).all()\r
437         gmap = gmaps[0]\r
438         log.debug("Found map id {0}: {1} but found \\r
439                 multiple".format(gmap.map_id, gmap.name))\r
440 \r
441     return gmap\r
442 \r
443 \r
444 def create_game(session, start_dt, game_type_cd, server_id, map_id,\r
445         match_id, duration, mod, winner=None):\r
446     """\r
447     Creates a game. Parameters:\r
448 \r
449     session - SQLAlchemy database session factory\r
450     start_dt - when the game started (datetime object)\r
451     game_type_cd - the game type of the game being played\r
452     server_id - server identifier of the server hosting the game\r
453     map_id - map on which the game was played\r
454     winner - the team id of the team that won\r
455     duration - how long the game lasted\r
456     mod - mods in use during the game\r
457     """\r
458     seq = Sequence('games_game_id_seq')\r
459     game_id = session.execute(seq)\r
460     game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,\r
461                 server_id=server_id, map_id=map_id, winner=winner)\r
462     game.match_id = match_id\r
463     game.mod = mod[:64]\r
464 \r
465     try:\r
466         game.duration = datetime.timedelta(seconds=int(round(float(duration))))\r
467     except:\r
468         pass\r
469 \r
470     try:\r
471         session.query(Game).filter(Game.server_id==server_id).\\r
472                 filter(Game.match_id==match_id).one()\r
473 \r
474         log.debug("Error: game with same server and match_id found! Ignoring.")\r
475 \r
476         # if a game under the same server and match_id found,\r
477         # this is a duplicate game and can be ignored\r
478         raise pyramid.httpexceptions.HTTPOk('OK')\r
479     except NoResultFound, e:\r
480         # server_id/match_id combination not found. game is ok to insert\r
481         session.add(game)\r
482         session.flush()\r
483         log.debug("Created game id {0} on server {1}, map {2} at \\r
484                 {3}".format(game.game_id,\r
485                     server_id, map_id, start_dt))\r
486 \r
487     return game\r
488 \r
489 \r
490 def get_or_create_player(session=None, hashkey=None, nick=None):\r
491     """\r
492     Finds a player by hashkey or creates a new one (along with a\r
493     corresponding hashkey entry. Parameters:\r
494 \r
495     session - SQLAlchemy database session factory\r
496     hashkey - hashkey of the player to be found or created\r
497     nick - nick of the player (in case of a first time create)\r
498     """\r
499     # if we have a bot\r
500     if re.search('^bot#\d+$', hashkey) or re.search('^bot#\d+#', hashkey):\r
501         player = session.query(Player).filter_by(player_id=1).one()\r
502     # if we have an untracked player\r
503     elif re.search('^player#\d+$', hashkey):\r
504         player = session.query(Player).filter_by(player_id=2).one()\r
505     # else it is a tracked player\r
506     else:\r
507         # see if the player is already in the database\r
508         # if not, create one and the hashkey along with it\r
509         try:\r
510             hk = session.query(Hashkey).filter_by(\r
511                     hashkey=hashkey).one()\r
512             player = session.query(Player).filter_by(\r
513                     player_id=hk.player_id).one()\r
514             log.debug("Found existing player {0} with hashkey {1}".format(\r
515                 player.player_id, hashkey))\r
516         except:\r
517             player = Player()\r
518             session.add(player)\r
519             session.flush()\r
520 \r
521             # if nick is given to us, use it. If not, use "Anonymous Player"\r
522             # with a suffix added for uniqueness.\r
523             if nick:\r
524                 player.nick = nick[:128]\r
525                 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))\r
526             else:\r
527                 player.nick = "Anonymous Player #{0}".format(player.player_id)\r
528                 player.stripped_nick = player.nick\r
529 \r
530             hk = Hashkey(player_id=player.player_id, hashkey=hashkey)\r
531             session.add(hk)\r
532             log.debug("Created player {0} ({2}) with hashkey {1}".format(\r
533                 player.player_id, hashkey, player.nick.encode('utf-8')))\r
534 \r
535     return player\r
536 \r
537 \r
538 def create_default_game_stat(session, game_type_cd):\r
539     """Creates a blanked-out pgstat record for the given game type"""\r
540 \r
541     # this is what we have to do to get partitioned records in - grab the\r
542     # sequence value first, then insert using the explicit ID (vs autogenerate)\r
543     seq = Sequence('player_game_stats_player_game_stat_id_seq')\r
544     pgstat_id = session.execute(seq)\r
545     pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,\r
546             create_dt=datetime.datetime.utcnow())\r
547 \r
548     if game_type_cd == 'as':\r
549         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0\r
550 \r
551     if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':\r
552         pgstat.kills = pgstat.deaths = pgstat.suicides = 0\r
553 \r
554     if game_type_cd == 'cq':\r
555         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0\r
556         pgstat.drops = 0\r
557 \r
558     if game_type_cd == 'ctf':\r
559         pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0\r
560         pgstat.returns = pgstat.carrier_frags = 0\r
561 \r
562     if game_type_cd == 'cts':\r
563         pgstat.deaths = 0\r
564 \r
565     if game_type_cd == 'dom':\r
566         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0\r
567         pgstat.drops = 0\r
568 \r
569     if game_type_cd == 'ft':\r
570         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0\r
571 \r
572     if game_type_cd == 'ka':\r
573         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0\r
574         pgstat.carrier_frags = 0\r
575         pgstat.time = datetime.timedelta(seconds=0)\r
576 \r
577     if game_type_cd == 'kh':\r
578         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0\r
579         pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0\r
580         pgstat.carrier_frags = 0\r
581 \r
582     if game_type_cd == 'lms':\r
583         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0\r
584 \r
585     if game_type_cd == 'nb':\r
586         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0\r
587         pgstat.drops = 0\r
588 \r
589     if game_type_cd == 'rc':\r
590         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0\r
591 \r
592     return pgstat\r
593 \r
594 \r
595 def create_game_stat(session, game_meta, game, server, gmap, player, events):\r
596     """Game stats handler for all game types"""\r
597 \r
598     game_type_cd = game.game_type_cd\r
599 \r
600     pgstat = create_default_game_stat(session, game_type_cd)\r
601 \r
602     # these fields should be on every pgstat record\r
603     pgstat.game_id       = game.game_id\r
604     pgstat.player_id     = player.player_id\r
605     pgstat.nick          = events.get('n', 'Anonymous Player')[:128]\r
606     pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))\r
607     pgstat.score         = int(round(float(events.get('scoreboard-score', 0))))\r
608     pgstat.alivetime     = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))\r
609     pgstat.rank          = int(events.get('rank', None))\r
610     pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))\r
611 \r
612     if pgstat.nick != player.nick \\r
613             and player.player_id > 2 \\r
614             and pgstat.nick != 'Anonymous Player':\r
615         register_new_nick(session, player, pgstat.nick)\r
616 \r
617     wins = False\r
618 \r
619     # gametype-specific stuff is handled here. if passed to us, we store it\r
620     for (key,value) in events.items():\r
621         if key == 'wins': wins = True\r
622         if key == 't': pgstat.team = int(value)\r
623 \r
624         if key == 'scoreboard-drops': pgstat.drops = int(value)\r
625         if key == 'scoreboard-returns': pgstat.returns = int(value)\r
626         if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)\r
627         if key == 'scoreboard-pickups': pgstat.pickups = int(value)\r
628         if key == 'scoreboard-caps': pgstat.captures = int(value)\r
629         if key == 'scoreboard-score': pgstat.score = int(round(float(value)))\r
630         if key == 'scoreboard-deaths': pgstat.deaths = int(value)\r
631         if key == 'scoreboard-kills': pgstat.kills = int(value)\r
632         if key == 'scoreboard-suicides': pgstat.suicides = int(value)\r
633         if key == 'scoreboard-objectives': pgstat.collects = int(value)\r
634         if key == 'scoreboard-captured': pgstat.captures = int(value)\r
635         if key == 'scoreboard-released': pgstat.drops = int(value)\r
636         if key == 'scoreboard-fastest':\r
637             pgstat.fastest = datetime.timedelta(seconds=float(value)/100)\r
638         if key == 'scoreboard-takes': pgstat.pickups = int(value)\r
639         if key == 'scoreboard-ticks': pgstat.drops = int(value)\r
640         if key == 'scoreboard-revivals': pgstat.revivals = int(value)\r
641         if key == 'scoreboard-bctime':\r
642             pgstat.time = datetime.timedelta(seconds=int(value))\r
643         if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)\r
644         if key == 'scoreboard-losses': pgstat.drops = int(value)\r
645         if key == 'scoreboard-pushes': pgstat.pushes = int(value)\r
646         if key == 'scoreboard-destroyed': pgstat.destroys = int(value)\r
647         if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)\r
648         if key == 'scoreboard-lives': pgstat.lives = int(value)\r
649         if key == 'scoreboard-goals': pgstat.captures = int(value)\r
650         if key == 'scoreboard-faults': pgstat.drops = int(value)\r
651         if key == 'scoreboard-laps': pgstat.laps = int(value)\r
652 \r
653         if key == 'avglatency': pgstat.avg_latency = float(value)\r
654         if key == 'scoreboard-captime':\r
655             pgstat.fastest = datetime.timedelta(seconds=float(value)/100)\r
656             if game.game_type_cd == 'ctf':\r
657                 update_fastest_cap(session, player.player_id, game.game_id,\r
658                         gmap.map_id, pgstat.fastest)\r
659 \r
660     # there is no "winning team" field, so we have to derive it\r
661     if wins and pgstat.team is not None and game.winner is None:\r
662         game.winner = pgstat.team\r
663         session.add(game)\r
664 \r
665     session.add(pgstat)\r
666 \r
667     return pgstat\r
668 \r
669 \r
670 def create_default_team_stat(session, game_type_cd):\r
671     """Creates a blanked-out teamstat record for the given game type"""\r
672 \r
673     # this is what we have to do to get partitioned records in - grab the\r
674     # sequence value first, then insert using the explicit ID (vs autogenerate)\r
675     seq = Sequence('team_game_stats_team_game_stat_id_seq')\r
676     teamstat_id = session.execute(seq)\r
677     teamstat = TeamGameStat(team_game_stat_id=teamstat_id,\r
678             create_dt=datetime.datetime.utcnow())\r
679 \r
680     # all team game modes have a score, so we'll zero that out always\r
681     teamstat.score = 0\r
682 \r
683     if game_type_cd in 'ca' 'ft' 'lms' 'ka':\r
684         teamstat.rounds = 0\r
685 \r
686     if game_type_cd == 'ctf':\r
687         teamstat.caps = 0\r
688 \r
689     return teamstat\r
690 \r
691 \r
692 def create_team_stat(session, game, events):\r
693     """Team stats handler for all game types"""\r
694 \r
695     try:\r
696         teamstat = create_default_team_stat(session, game.game_type_cd)\r
697         teamstat.game_id = game.game_id\r
698 \r
699         # we should have a team ID if we have a 'Q' event\r
700         if re.match(r'^team#\d+$', events.get('Q', '')):\r
701             team = int(events.get('Q').replace('team#', ''))\r
702             teamstat.team = team\r
703 \r
704         # gametype-specific stuff is handled here. if passed to us, we store it\r
705         for (key,value) in events.items():\r
706             if key == 'scoreboard-score': teamstat.score = int(round(float(value)))\r
707             if key == 'scoreboard-caps': teamstat.caps = int(value)\r
708             if key == 'scoreboard-rounds': teamstat.rounds = int(value)\r
709 \r
710         session.add(teamstat)\r
711     except Exception as e:\r
712         raise e\r
713 \r
714     return teamstat\r
715 \r
716 \r
717 def create_weapon_stats(session, game_meta, game, player, pgstat, events):\r
718     """Weapon stats handler for all game types"""\r
719     pwstats = []\r
720 \r
721     # Version 1 of stats submissions doubled the data sent.\r
722     # To counteract this we divide the data by 2 only for\r
723     # POSTs coming from version 1.\r
724     try:\r
725         version = int(game_meta['V'])\r
726         if version == 1:\r
727             is_doubled = True\r
728             log.debug('NOTICE: found a version 1 request, halving the weapon stats...')\r
729         else:\r
730             is_doubled = False\r
731     except:\r
732         is_doubled = False\r
733 \r
734     for (key,value) in events.items():\r
735         matched = re.search("acc-(.*?)-cnt-fired", key)\r
736         if matched:\r
737             weapon_cd = matched.group(1)\r
738             seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')\r
739             pwstat_id = session.execute(seq)\r
740             pwstat = PlayerWeaponStat()\r
741             pwstat.player_weapon_stats_id = pwstat_id\r
742             pwstat.player_id = player.player_id\r
743             pwstat.game_id = game.game_id\r
744             pwstat.player_game_stat_id = pgstat.player_game_stat_id\r
745             pwstat.weapon_cd = weapon_cd\r
746 \r
747             if 'n' in events:\r
748                 pwstat.nick = events['n']\r
749             else:\r
750                 pwstat.nick = events['P']\r
751 \r
752             if 'acc-' + weapon_cd + '-cnt-fired' in events:\r
753                 pwstat.fired = int(round(float(\r
754                         events['acc-' + weapon_cd + '-cnt-fired'])))\r
755             if 'acc-' + weapon_cd + '-fired' in events:\r
756                 pwstat.max = int(round(float(\r
757                         events['acc-' + weapon_cd + '-fired'])))\r
758             if 'acc-' + weapon_cd + '-cnt-hit' in events:\r
759                 pwstat.hit = int(round(float(\r
760                         events['acc-' + weapon_cd + '-cnt-hit'])))\r
761             if 'acc-' + weapon_cd + '-hit' in events:\r
762                 pwstat.actual = int(round(float(\r
763                         events['acc-' + weapon_cd + '-hit'])))\r
764             if 'acc-' + weapon_cd + '-frags' in events:\r
765                 pwstat.frags = int(round(float(\r
766                         events['acc-' + weapon_cd + '-frags'])))\r
767 \r
768             if is_doubled:\r
769                 pwstat.fired = pwstat.fired/2\r
770                 pwstat.max = pwstat.max/2\r
771                 pwstat.hit = pwstat.hit/2\r
772                 pwstat.actual = pwstat.actual/2\r
773                 pwstat.frags = pwstat.frags/2\r
774 \r
775             session.add(pwstat)\r
776             pwstats.append(pwstat)\r
777 \r
778     return pwstats\r
779 \r
780 \r
781 def create_elos(session, game):\r
782     """Elo handler for all game types."""\r
783     try:\r
784         process_elos(game, session)\r
785     except Exception as e:\r
786         log.debug('Error (non-fatal): elo processing failed.')\r
787 \r
788 \r
789 def submit_stats(request):\r
790     """\r
791     Entry handler for POST stats submissions.\r
792     """\r
793     try:\r
794         # placeholder for the actual session\r
795         session = None\r
796 \r
797         log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +\r
798                 "----- END REQUEST BODY -----\n\n")\r
799 \r
800         (idfp, status) = verify_request(request)\r
801         (game_meta, raw_players, raw_teams) = parse_stats_submission(request.body)\r
802         revision = game_meta.get('R', 'unknown')\r
803         duration = game_meta.get('D', None)\r
804 \r
805         # only players present at the end of the match are eligible for stats\r
806         raw_players = filter(played_in_game, raw_players)\r
807 \r
808         do_precondition_checks(request, game_meta, raw_players)\r
809 \r
810         # the "duel" gametype is fake\r
811         if len(raw_players) == 2 \\r
812             and num_real_players(raw_players) == 2 \\r
813             and game_meta['G'] == 'dm':\r
814             game_meta['G'] = 'duel'\r
815 \r
816         #----------------------------------------------------------------------\r
817         # Actual setup (inserts/updates) below here\r
818         #----------------------------------------------------------------------\r
819         session = DBSession()\r
820 \r
821         game_type_cd = game_meta['G']\r
822 \r
823         # All game types create Game, Server, Map, and Player records\r
824         # the same way.\r
825         server = get_or_create_server(\r
826                 session  = session,\r
827                 hashkey  = idfp,\r
828                 name     = game_meta['S'],\r
829                 revision = revision,\r
830                 ip_addr  = get_remote_addr(request),\r
831                 port     = game_meta.get('U', None))\r
832 \r
833         gmap = get_or_create_map(\r
834                 session = session,\r
835                 name    = game_meta['M'])\r
836 \r
837         game = create_game(\r
838                 session      = session,\r
839                 start_dt     = datetime.datetime.utcnow(),\r
840                 server_id    = server.server_id,\r
841                 game_type_cd = game_type_cd,\r
842                 map_id       = gmap.map_id,\r
843                 match_id     = game_meta['I'],\r
844                 duration     = duration,\r
845                 mod          = game_meta.get('O', None))\r
846 \r
847         for events in raw_players:\r
848             player = get_or_create_player(\r
849                 session = session,\r
850                 hashkey = events['P'],\r
851                 nick    = events.get('n', None))\r
852 \r
853             pgstat = create_game_stat(session, game_meta, game, server,\r
854                     gmap, player, events)\r
855 \r
856             if should_do_weapon_stats(game_type_cd) and player.player_id > 1:\r
857                 pwstats = create_weapon_stats(session, game_meta, game, player,\r
858                         pgstat, events)\r
859 \r
860         for events in raw_teams:\r
861             try:\r
862                 teamstat = create_team_stat(session, game, events)\r
863             except Exception as e:\r
864                 raise e\r
865 \r
866         if should_do_elos(game_type_cd):\r
867             create_elos(session, game)\r
868 \r
869         session.commit()\r
870         log.debug('Success! Stats recorded.')\r
871         return Response('200 OK')\r
872     except Exception as e:\r
873         if session:\r
874             session.rollback()\r
875         raise e\r