]> de.git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/views/submission.py
Adding teamscores (proof-of-concept)
[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, start_dt, game_type_cd, server_id, map_id,\r
450         match_id, duration, mod, winner=None):\r
451     """\r
452     Creates a game. Parameters:\r
453 \r
454     session - SQLAlchemy database session factory\r
455     start_dt - when the game started (datetime object)\r
456     game_type_cd - the game type of the game being played\r
457     server_id - server identifier of the server hosting the game\r
458     map_id - map on which the game was played\r
459     winner - the team id of the team that won\r
460     duration - how long the game lasted\r
461     mod - mods in use during the game\r
462     """\r
463     seq = Sequence('games_game_id_seq')\r
464     game_id = session.execute(seq)\r
465     game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,\r
466                 server_id=server_id, map_id=map_id, winner=winner)\r
467     game.match_id = match_id\r
468     game.mod = mod[:64]\r
469 \r
470     try:\r
471         game.duration = datetime.timedelta(seconds=int(round(float(duration))))\r
472     except:\r
473         pass\r
474 \r
475     try:\r
476         session.query(Game).filter(Game.server_id==server_id).\\r
477                 filter(Game.match_id==match_id).one()\r
478 \r
479         log.debug("Error: game with same server and match_id found! Ignoring.")\r
480 \r
481         # if a game under the same server and match_id found,\r
482         # this is a duplicate game and can be ignored\r
483         raise pyramid.httpexceptions.HTTPOk('OK')\r
484     except NoResultFound, e:\r
485         # server_id/match_id combination not found. game is ok to insert\r
486         session.add(game)\r
487         session.flush()\r
488         log.debug("Created game id {0} on server {1}, map {2} at \\r
489                 {3}".format(game.game_id,\r
490                     server_id, map_id, start_dt))\r
491 \r
492     return game\r
493 \r
494 \r
495 def get_or_create_player(session=None, hashkey=None, nick=None):\r
496     """\r
497     Finds a player by hashkey or creates a new one (along with a\r
498     corresponding hashkey entry. Parameters:\r
499 \r
500     session - SQLAlchemy database session factory\r
501     hashkey - hashkey of the player to be found or created\r
502     nick - nick of the player (in case of a first time create)\r
503     """\r
504     # if we have a bot\r
505     if re.search('^bot#\d+$', hashkey) or re.search('^bot#\d+#', hashkey):\r
506         player = session.query(Player).filter_by(player_id=1).one()\r
507     # if we have an untracked player\r
508     elif re.search('^player#\d+$', hashkey):\r
509         player = session.query(Player).filter_by(player_id=2).one()\r
510     # else it is a tracked player\r
511     else:\r
512         # see if the player is already in the database\r
513         # if not, create one and the hashkey along with it\r
514         try:\r
515             hk = session.query(Hashkey).filter_by(\r
516                     hashkey=hashkey).one()\r
517             player = session.query(Player).filter_by(\r
518                     player_id=hk.player_id).one()\r
519             log.debug("Found existing player {0} with hashkey {1}".format(\r
520                 player.player_id, hashkey))\r
521         except:\r
522             player = Player()\r
523             session.add(player)\r
524             session.flush()\r
525 \r
526             # if nick is given to us, use it. If not, use "Anonymous Player"\r
527             # with a suffix added for uniqueness.\r
528             if nick:\r
529                 player.nick = nick[:128]\r
530                 player.stripped_nick = strip_colors(qfont_decode(nick[:128]))\r
531             else:\r
532                 player.nick = "Anonymous Player #{0}".format(player.player_id)\r
533                 player.stripped_nick = player.nick\r
534 \r
535             hk = Hashkey(player_id=player.player_id, hashkey=hashkey)\r
536             session.add(hk)\r
537             log.debug("Created player {0} ({2}) with hashkey {1}".format(\r
538                 player.player_id, hashkey, player.nick.encode('utf-8')))\r
539 \r
540     return player\r
541 \r
542 \r
543 def create_default_game_stat(session, game_type_cd):\r
544     """Creates a blanked-out pgstat record for the given game type"""\r
545 \r
546     # this is what we have to do to get partitioned records in - grab the\r
547     # sequence value first, then insert using the explicit ID (vs autogenerate)\r
548     seq = Sequence('player_game_stats_player_game_stat_id_seq')\r
549     pgstat_id = session.execute(seq)\r
550     pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,\r
551             create_dt=datetime.datetime.utcnow())\r
552 \r
553     if game_type_cd == 'as':\r
554         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0\r
555 \r
556     if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':\r
557         pgstat.kills = pgstat.deaths = pgstat.suicides = 0\r
558 \r
559     if game_type_cd == 'cq':\r
560         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0\r
561         pgstat.drops = 0\r
562 \r
563     if game_type_cd == 'ctf':\r
564         pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0\r
565         pgstat.returns = pgstat.carrier_frags = 0\r
566 \r
567     if game_type_cd == 'cts':\r
568         pgstat.deaths = 0\r
569 \r
570     if game_type_cd == 'dom':\r
571         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0\r
572         pgstat.drops = 0\r
573 \r
574     if game_type_cd == 'ft':\r
575         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0\r
576 \r
577     if game_type_cd == 'ka':\r
578         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0\r
579         pgstat.carrier_frags = 0\r
580         pgstat.time = datetime.timedelta(seconds=0)\r
581 \r
582     if game_type_cd == 'kh':\r
583         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0\r
584         pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0\r
585         pgstat.carrier_frags = 0\r
586 \r
587     if game_type_cd == 'lms':\r
588         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0\r
589 \r
590     if game_type_cd == 'nb':\r
591         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0\r
592         pgstat.drops = 0\r
593 \r
594     if game_type_cd == 'rc':\r
595         pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0\r
596 \r
597     return pgstat\r
598 \r
599 \r
600 def create_game_stat(session, game_meta, game, server, gmap, player, events):\r
601     """Game stats handler for all game types"""\r
602 \r
603     game_type_cd = game.game_type_cd\r
604 \r
605     pgstat = create_default_game_stat(session, game_type_cd)\r
606 \r
607     # these fields should be on every pgstat record\r
608     pgstat.game_id       = game.game_id\r
609     pgstat.player_id     = player.player_id\r
610     pgstat.nick          = events.get('n', 'Anonymous Player')[:128]\r
611     pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))\r
612     pgstat.score         = int(round(float(events.get('scoreboard-score', 0))))\r
613     pgstat.alivetime     = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))\r
614     pgstat.rank          = int(events.get('rank', None))\r
615     pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))\r
616 \r
617     if pgstat.nick != player.nick \\r
618             and player.player_id > 2 \\r
619             and pgstat.nick != 'Anonymous Player':\r
620         register_new_nick(session, player, pgstat.nick)\r
621 \r
622     wins = False\r
623 \r
624     # gametype-specific stuff is handled here. if passed to us, we store it\r
625     for (key,value) in events.items():\r
626         if key == 'wins': wins = True\r
627         if key == 't': pgstat.team = int(value)\r
628 \r
629         if key == 'scoreboard-drops': pgstat.drops = int(value)\r
630         if key == 'scoreboard-returns': pgstat.returns = int(value)\r
631         if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)\r
632         if key == 'scoreboard-pickups': pgstat.pickups = int(value)\r
633         if key == 'scoreboard-caps': pgstat.captures = int(value)\r
634         if key == 'scoreboard-score': pgstat.score = int(round(float(value)))\r
635         if key == 'scoreboard-teamscore': pgstat.teamscore = int(round(float(value)))\r
636         if key == 'scoreboard-deaths': pgstat.deaths = int(value)\r
637         if key == 'scoreboard-kills': pgstat.kills = int(value)\r
638         if key == 'scoreboard-suicides': pgstat.suicides = int(value)\r
639         if key == 'scoreboard-objectives': pgstat.collects = int(value)\r
640         if key == 'scoreboard-captured': pgstat.captures = int(value)\r
641         if key == 'scoreboard-released': pgstat.drops = int(value)\r
642         if key == 'scoreboard-fastest':\r
643             pgstat.fastest = datetime.timedelta(seconds=float(value)/100)\r
644         if key == 'scoreboard-takes': pgstat.pickups = int(value)\r
645         if key == 'scoreboard-ticks': pgstat.drops = int(value)\r
646         if key == 'scoreboard-revivals': pgstat.revivals = int(value)\r
647         if key == 'scoreboard-bctime':\r
648             pgstat.time = datetime.timedelta(seconds=int(value))\r
649         if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)\r
650         if key == 'scoreboard-losses': pgstat.drops = int(value)\r
651         if key == 'scoreboard-pushes': pgstat.pushes = int(value)\r
652         if key == 'scoreboard-destroyed': pgstat.destroys = int(value)\r
653         if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)\r
654         if key == 'scoreboard-lives': pgstat.lives = int(value)\r
655         if key == 'scoreboard-goals': pgstat.captures = int(value)\r
656         if key == 'scoreboard-faults': pgstat.drops = int(value)\r
657         if key == 'scoreboard-laps': pgstat.laps = int(value)\r
658 \r
659         if key == 'avglatency': pgstat.avg_latency = float(value)\r
660         if key == 'scoreboard-captime':\r
661             pgstat.fastest = datetime.timedelta(seconds=float(value)/100)\r
662             if game.game_type_cd == 'ctf':\r
663                 update_fastest_cap(session, player.player_id, game.game_id,\r
664                         gmap.map_id, pgstat.fastest)\r
665 \r
666     # there is no "winning team" field, so we have to derive it\r
667     if wins and pgstat.team is not None and game.winner is None:\r
668         game.winner = pgstat.team\r
669         session.add(game)\r
670 \r
671     session.add(pgstat)\r
672 \r
673     return pgstat\r
674 \r
675 \r
676 def create_weapon_stats(session, game_meta, game, player, pgstat, events):\r
677     """Weapon stats handler for all game types"""\r
678     pwstats = []\r
679 \r
680     # Version 1 of stats submissions doubled the data sent.\r
681     # To counteract this we divide the data by 2 only for\r
682     # POSTs coming from version 1.\r
683     try:\r
684         version = int(game_meta['V'])\r
685         if version == 1:\r
686             is_doubled = True\r
687             log.debug('NOTICE: found a version 1 request, halving the weapon stats...')\r
688         else:\r
689             is_doubled = False\r
690     except:\r
691         is_doubled = False\r
692 \r
693     for (key,value) in events.items():\r
694         matched = re.search("acc-(.*?)-cnt-fired", key)\r
695         if matched:\r
696             weapon_cd = matched.group(1)\r
697             seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')\r
698             pwstat_id = session.execute(seq)\r
699             pwstat = PlayerWeaponStat()\r
700             pwstat.player_weapon_stats_id = pwstat_id\r
701             pwstat.player_id = player.player_id\r
702             pwstat.game_id = game.game_id\r
703             pwstat.player_game_stat_id = pgstat.player_game_stat_id\r
704             pwstat.weapon_cd = weapon_cd\r
705 \r
706             if 'n' in events:\r
707                 pwstat.nick = events['n']\r
708             else:\r
709                 pwstat.nick = events['P']\r
710 \r
711             if 'acc-' + weapon_cd + '-cnt-fired' in events:\r
712                 pwstat.fired = int(round(float(\r
713                         events['acc-' + weapon_cd + '-cnt-fired'])))\r
714             if 'acc-' + weapon_cd + '-fired' in events:\r
715                 pwstat.max = int(round(float(\r
716                         events['acc-' + weapon_cd + '-fired'])))\r
717             if 'acc-' + weapon_cd + '-cnt-hit' in events:\r
718                 pwstat.hit = int(round(float(\r
719                         events['acc-' + weapon_cd + '-cnt-hit'])))\r
720             if 'acc-' + weapon_cd + '-hit' in events:\r
721                 pwstat.actual = int(round(float(\r
722                         events['acc-' + weapon_cd + '-hit'])))\r
723             if 'acc-' + weapon_cd + '-frags' in events:\r
724                 pwstat.frags = int(round(float(\r
725                         events['acc-' + weapon_cd + '-frags'])))\r
726 \r
727             if is_doubled:\r
728                 pwstat.fired = pwstat.fired/2\r
729                 pwstat.max = pwstat.max/2\r
730                 pwstat.hit = pwstat.hit/2\r
731                 pwstat.actual = pwstat.actual/2\r
732                 pwstat.frags = pwstat.frags/2\r
733 \r
734             session.add(pwstat)\r
735             pwstats.append(pwstat)\r
736 \r
737     return pwstats\r
738 \r
739 \r
740 def create_elos(session, game):\r
741     """Elo handler for all game types."""\r
742     try:\r
743         process_elos(game, session)\r
744     except Exception as e:\r
745         log.debug('Error (non-fatal): elo processing failed.')\r
746 \r
747 \r
748 def submit_stats(request):\r
749     """\r
750     Entry handler for POST stats submissions.\r
751     """\r
752     try:\r
753         # placeholder for the actual session\r
754         session = None\r
755 \r
756         log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +\r
757                 "----- END REQUEST BODY -----\n\n")\r
758 \r
759         (idfp, status) = verify_request(request)\r
760         (game_meta, raw_players) = parse_stats_submission(request.body)\r
761         revision = game_meta.get('R', 'unknown')\r
762         duration = game_meta.get('D', None)\r
763 \r
764         # only players present at the end of the match are eligible for stats\r
765         raw_players = filter(played_in_game, raw_players)\r
766 \r
767         do_precondition_checks(request, game_meta, raw_players)\r
768 \r
769         # the "duel" gametype is fake\r
770         if len(raw_players) == 2 \\r
771             and num_real_players(raw_players) == 2 \\r
772             and game_meta['G'] == 'dm':\r
773             game_meta['G'] = 'duel'\r
774 \r
775         #----------------------------------------------------------------------\r
776         # Actual setup (inserts/updates) below here\r
777         #----------------------------------------------------------------------\r
778         session = DBSession()\r
779 \r
780         game_type_cd = game_meta['G']\r
781 \r
782         # All game types create Game, Server, Map, and Player records\r
783         # the same way.\r
784         server = get_or_create_server(\r
785                 session  = session,\r
786                 hashkey  = idfp,\r
787                 name     = game_meta['S'],\r
788                 revision = revision,\r
789                 ip_addr  = get_remote_addr(request),\r
790                 port     = game_meta.get('U', None))\r
791 \r
792         gmap = get_or_create_map(\r
793                 session = session,\r
794                 name    = game_meta['M'])\r
795 \r
796         game = create_game(\r
797                 session      = session,\r
798                 start_dt     = datetime.datetime.utcnow(),\r
799                 server_id    = server.server_id,\r
800                 game_type_cd = game_type_cd,\r
801                 map_id       = gmap.map_id,\r
802                 match_id     = game_meta['I'],\r
803                 duration     = duration,\r
804                 mod          = game_meta.get('O', None))\r
805 \r
806         for events in raw_players:\r
807             player = get_or_create_player(\r
808                 session = session,\r
809                 hashkey = events['P'],\r
810                 nick    = events.get('n', None))\r
811 \r
812             pgstat = create_game_stat(session, game_meta, game, server,\r
813                     gmap, player, events)\r
814 \r
815             if should_do_weapon_stats(game_type_cd) and player.player_id > 1:\r
816                 pwstats = create_weapon_stats(session, game_meta, game, player,\r
817                         pgstat, events)\r
818 \r
819         if should_do_elos(game_type_cd):\r
820             create_elos(session, game)\r
821 \r
822         session.commit()\r
823         log.debug('Success! Stats recorded.')\r
824         return Response('200 OK')\r
825     except Exception as e:\r
826         if session:\r
827             session.rollback()\r
828         return e\r