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