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