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