]> git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/views/submission.py
Make valid NB games those w/ nonzero scores ONLY. Fixes #152.
[xonotic/xonstat.git] / xonstat / views / submission.py
1 import datetime\r
2 import logging\r
3 import os\r
4 import pyramid.httpexceptions\r
5 import re\r
6 import time\r
7 import sqlalchemy.sql.expression as expr\r
8 from pyramid.response import Response\r
9 from sqlalchemy import Sequence\r
10 from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound\r
11 from xonstat.elo import process_elos\r
12 from xonstat.models import *\r
13 from xonstat.util import strip_colors, qfont_decode, verify_request, weapon_map\r
14 \r
15 \r
16 log = logging.getLogger(__name__)\r
17 \r
18 \r
19 def parse_stats_submission(body):\r
20     """\r
21     Parses the POST request body for a stats submission\r
22     """\r
23     # storage vars for the request body\r
24     game_meta = {}\r
25     events = {}\r
26     players = []\r
27     teams = []\r
28 \r
29     # we're not in either stanza to start\r
30     in_P = in_Q = False\r
31 \r
32     for line in body.split('\n'):\r
33         try:\r
34             (key, value) = line.strip().split(' ', 1)\r
35 \r
36             # Server (S) and Nick (n) fields can have international characters.\r
37             if key in 'S' 'n':\r
38                 value = unicode(value, 'utf-8')\r
39 \r
40             if key not in 'P' 'Q' 'n' 'e' 't' 'i':\r
41                 game_meta[key] = value\r
42 \r
43             if key == 'Q' or key == 'P':\r
44                 #log.debug('Found a {0}'.format(key))\r
45                 #log.debug('in_Q: {0}'.format(in_Q))\r
46                 #log.debug('in_P: {0}'.format(in_P))\r
47                 #log.debug('events: {0}'.format(events))\r
48 \r
49                 # check where we were before and append events accordingly\r
50                 if in_Q and len(events) > 0:\r
51                     #log.debug('creating a team (Q) entry')\r
52                     teams.append(events)\r
53                     events = {}\r
54                 elif in_P and len(events) > 0:\r
55                     #log.debug('creating a player (P) entry')\r
56                     players.append(events)\r
57                     events = {}\r
58 \r
59                 if key == 'P':\r
60                     #log.debug('key == P')\r
61                     in_P = True\r
62                     in_Q = False\r
63                 elif key == 'Q':\r
64                     #log.debug('key == Q')\r
65                     in_P = False\r
66                     in_Q = True\r
67 \r
68                 events[key] = value\r
69 \r
70             if key == 'e':\r
71                 (subkey, subvalue) = value.split(' ', 1)\r
72                 events[subkey] = subvalue\r
73             if key == 'n':\r
74                 events[key] = value\r
75             if key == 't':\r
76                 events[key] = value\r
77         except:\r
78             # no key/value pair - move on to the next line\r
79             pass\r
80 \r
81     # add the last entity we were working on\r
82     if in_P and len(events) > 0:\r
83         players.append(events)\r
84     elif in_Q and len(events) > 0:\r
85         teams.append(events)\r
86 \r
87     return (game_meta, players, teams)\r
88 \r
89 \r
90 def is_blank_game(gametype, players):\r
91     """Determine if this is a blank game or not. A blank game is either:\r
92 \r
93     1) a match that ended in the warmup stage, where accuracy events are not\r
94     present (for non-CTS games)\r
95 \r
96     2) a match in which no player made a positive or negative score AND was\r
97     on the scoreboard\r
98 \r
99     ... or for CTS, which doesn't record accuracy events\r
100 \r
101     1) a match in which no player made a fastest lap AND was\r
102     on the scoreboard\r
103 \r
104     ... or for NB, in which not all maps have weapons\r
105 \r
106     1) a match in which no player made a positive or negative score\r
107     """\r
108     r = re.compile(r'acc-.*-cnt-fired')\r
109     flg_nonzero_score = False\r
110     flg_acc_events = False\r
111     flg_fastest_lap = False\r
112 \r
113     for events in players:\r
114         if is_real_player(events) and played_in_game(events):\r
115             for (key,value) in events.items():\r
116                 if key == 'scoreboard-score' and value != 0:\r
117                     flg_nonzero_score = True\r
118                 if r.search(key):\r
119                     flg_acc_events = True\r
120                 if key == 'scoreboard-fastest':\r
121                     flg_fastest_lap = True\r
122 \r
123     if gametype == 'cts':\r
124         return not flg_fastest_lap\r
125     elif gametype == 'nb':\r
126         return not flg_nonzero_score\r
127     else:\r
128         return not (flg_nonzero_score and flg_acc_events)\r
129 \r
130 \r
131 def get_remote_addr(request):\r
132     """Get the Xonotic server's IP address"""\r
133     if 'X-Forwarded-For' in request.headers:\r
134         return request.headers['X-Forwarded-For']\r
135     else:\r
136         return request.remote_addr\r
137 \r
138 \r
139 def is_supported_gametype(gametype, version):\r
140     """Whether a gametype is supported or not"""\r
141     is_supported = False\r
142 \r
143     # if the type can be supported, but with version constraints, uncomment\r
144     # here and add the restriction for a specific version below\r
145     supported_game_types = (\r
146             'as',\r
147             'ca',\r
148             # 'cq',\r
149             'ctf',\r
150             'cts',\r
151             'dm',\r
152             'dom',\r
153             'ft', 'freezetag',\r
154             'ka', 'keepaway',\r
155             'kh',\r
156             # 'lms',\r
157             'nb', 'nexball',\r
158             # 'rc',\r
159             'rune',\r
160             'tdm',\r
161         )\r
162 \r
163     if gametype in supported_game_types:\r
164         is_supported = True\r
165     else:\r
166         is_supported = False\r
167 \r
168     # some game types were buggy before revisions, thus this additional filter\r
169     if gametype == 'ca' and version <= 5:\r
170         is_supported = False\r
171 \r
172     return is_supported\r
173 \r
174 \r
175 def do_precondition_checks(request, game_meta, raw_players):\r
176     """Precondition checks for ALL gametypes.\r
177        These do not require a database connection."""\r
178     if not has_required_metadata(game_meta):\r
179         log.debug("ERROR: Required game meta missing")\r
180         raise pyramid.httpexceptions.HTTPUnprocessableEntity("Missing game meta")\r
181 \r
182     try:\r
183         version = int(game_meta['V'])\r
184     except:\r
185         log.debug("ERROR: Required game meta invalid")\r
186         raise pyramid.httpexceptions.HTTPUnprocessableEntity("Invalid game meta")\r
187 \r
188     if not is_supported_gametype(game_meta['G'], version):\r
189         log.debug("ERROR: Unsupported gametype")\r
190         raise pyramid.httpexceptions.HTTPOk("OK")\r
191 \r
192     if not has_minimum_real_players(request.registry.settings, raw_players):\r
193         log.debug("ERROR: Not enough real players")\r
194         raise pyramid.httpexceptions.HTTPOk("OK")\r
195 \r
196     if is_blank_game(game_meta['G'], raw_players):\r
197         log.debug("ERROR: Blank game")\r
198         raise pyramid.httpexceptions.HTTPOk("OK")\r
199 \r
200 \r
201 def is_real_player(events):\r
202     """\r
203     Determines if a given set of events correspond with a non-bot\r
204     """\r
205     if not events['P'].startswith('bot'):\r
206         return True\r
207     else:\r
208         return False\r
209 \r
210 \r
211 def played_in_game(events):\r
212     """\r
213     Determines if a given set of player events correspond with a player who\r
214     played in the game (matches 1 and scoreboardvalid 1)\r
215     """\r
216     if 'matches' in events and 'scoreboardvalid' in events:\r
217         return True\r
218     else:\r
219         return False\r
220 \r
221 \r
222 def num_real_players(player_events):\r
223     """\r
224     Returns the number of real players (those who played\r
225     and are on the scoreboard).\r
226     """\r
227     real_players = 0\r
228 \r
229     for events in player_events:\r
230         if is_real_player(events) and played_in_game(events):\r
231             real_players += 1\r
232 \r
233     return real_players\r
234 \r
235 \r
236 def has_minimum_real_players(settings, player_events):\r
237     """\r
238     Determines if the collection of player events has enough "real" players\r
239     to store in the database. The minimum setting comes from the config file\r
240     under the setting xonstat.minimum_real_players.\r
241     """\r
242     flg_has_min_real_players = True\r
243 \r
244     try:\r
245         minimum_required_players = int(\r
246                 settings['xonstat.minimum_required_players'])\r
247     except:\r
248         minimum_required_players = 2\r
249 \r
250     real_players = num_real_players(player_events)\r
251 \r
252     if real_players < minimum_required_players:\r
253         flg_has_min_real_players = False\r
254 \r
255     return flg_has_min_real_players\r
256 \r
257 \r
258 def has_required_metadata(metadata):\r
259     """\r
260     Determines if a give set of metadata has enough data to create a game,\r
261     server, and map with.\r
262     """\r
263     flg_has_req_metadata = True\r
264 \r
265     if '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-deaths': pgstat.deaths = int(value)\r
636         if key == 'scoreboard-kills': pgstat.kills = int(value)\r
637         if key == 'scoreboard-suicides': pgstat.suicides = int(value)\r
638         if key == 'scoreboard-objectives': pgstat.collects = int(value)\r
639         if key == 'scoreboard-captured': pgstat.captures = int(value)\r
640         if key == 'scoreboard-released': pgstat.drops = int(value)\r
641         if key == 'scoreboard-fastest':\r
642             pgstat.fastest = datetime.timedelta(seconds=float(value)/100)\r
643         if key == 'scoreboard-takes': pgstat.pickups = int(value)\r
644         if key == 'scoreboard-ticks': pgstat.drops = int(value)\r
645         if key == 'scoreboard-revivals': pgstat.revivals = int(value)\r
646         if key == 'scoreboard-bctime':\r
647             pgstat.time = datetime.timedelta(seconds=int(value))\r
648         if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)\r
649         if key == 'scoreboard-losses': pgstat.drops = int(value)\r
650         if key == 'scoreboard-pushes': pgstat.pushes = int(value)\r
651         if key == 'scoreboard-destroyed': pgstat.destroys = int(value)\r
652         if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)\r
653         if key == 'scoreboard-lives': pgstat.lives = int(value)\r
654         if key == 'scoreboard-goals': pgstat.captures = int(value)\r
655         if key == 'scoreboard-faults': pgstat.drops = int(value)\r
656         if key == 'scoreboard-laps': pgstat.laps = int(value)\r
657 \r
658         if key == 'avglatency': pgstat.avg_latency = float(value)\r
659         if key == 'scoreboard-captime':\r
660             pgstat.fastest = datetime.timedelta(seconds=float(value)/100)\r
661             if game.game_type_cd == 'ctf':\r
662                 update_fastest_cap(session, player.player_id, game.game_id,\r
663                         gmap.map_id, pgstat.fastest)\r
664 \r
665     # there is no "winning team" field, so we have to derive it\r
666     if wins and pgstat.team is not None and game.winner is None:\r
667         game.winner = pgstat.team\r
668         session.add(game)\r
669 \r
670     session.add(pgstat)\r
671 \r
672     return pgstat\r
673 \r
674 \r
675 def create_anticheats(session, pgstat, game, player, events):\r
676     """Anticheats handler for all game types"""\r
677 \r
678     anticheats = []\r
679 \r
680     # all anticheat events are prefixed by "anticheat"\r
681     for (key,value) in events.items():\r
682         if key.startswith("anticheat"):\r
683             try:\r
684                 ac = PlayerGameAnticheat(\r
685                     player.player_id,\r
686                     game.game_id,\r
687                     key,\r
688                     float(value)\r
689                 )\r
690                 anticheats.append(ac)\r
691                 session.add(ac)\r
692             except Exception as e:\r
693                 log.debug("Could not parse value for key %s. Ignoring." % key)\r
694 \r
695     return anticheats\r
696 \r
697 \r
698 def create_default_team_stat(session, game_type_cd):\r
699     """Creates a blanked-out teamstat record for the given game type"""\r
700 \r
701     # this is what we have to do to get partitioned records in - grab the\r
702     # sequence value first, then insert using the explicit ID (vs autogenerate)\r
703     seq = Sequence('team_game_stats_team_game_stat_id_seq')\r
704     teamstat_id = session.execute(seq)\r
705     teamstat = TeamGameStat(team_game_stat_id=teamstat_id,\r
706             create_dt=datetime.datetime.utcnow())\r
707 \r
708     # all team game modes have a score, so we'll zero that out always\r
709     teamstat.score = 0\r
710 \r
711     if game_type_cd in 'ca' 'ft' 'lms' 'ka':\r
712         teamstat.rounds = 0\r
713 \r
714     if game_type_cd == 'ctf':\r
715         teamstat.caps = 0\r
716 \r
717     return teamstat\r
718 \r
719 \r
720 def create_team_stat(session, game, events):\r
721     """Team stats handler for all game types"""\r
722 \r
723     try:\r
724         teamstat = create_default_team_stat(session, game.game_type_cd)\r
725         teamstat.game_id = game.game_id\r
726 \r
727         # we should have a team ID if we have a 'Q' event\r
728         if re.match(r'^team#\d+$', events.get('Q', '')):\r
729             team = int(events.get('Q').replace('team#', ''))\r
730             teamstat.team = team\r
731 \r
732         # gametype-specific stuff is handled here. if passed to us, we store it\r
733         for (key,value) in events.items():\r
734             if key == 'scoreboard-score': teamstat.score = int(round(float(value)))\r
735             if key == 'scoreboard-caps': teamstat.caps = int(value)\r
736             if key == 'scoreboard-rounds': teamstat.rounds = int(value)\r
737 \r
738         session.add(teamstat)\r
739     except Exception as e:\r
740         raise e\r
741 \r
742     return teamstat\r
743 \r
744 \r
745 def create_weapon_stats(session, game_meta, game, player, pgstat, events):\r
746     """Weapon stats handler for all game types"""\r
747     pwstats = []\r
748 \r
749     # Version 1 of stats submissions doubled the data sent.\r
750     # To counteract this we divide the data by 2 only for\r
751     # POSTs coming from version 1.\r
752     try:\r
753         version = int(game_meta['V'])\r
754         if version == 1:\r
755             is_doubled = True\r
756             log.debug('NOTICE: found a version 1 request, halving the weapon stats...')\r
757         else:\r
758             is_doubled = False\r
759     except:\r
760         is_doubled = False\r
761 \r
762     for (key,value) in events.items():\r
763         matched = re.search("acc-(.*?)-cnt-fired", key)\r
764         if matched:\r
765             weapon_cd = matched.group(1)\r
766 \r
767             # Weapon names changed for 0.8. We'll convert the old\r
768             # ones to use the new scheme as well.\r
769             mapped_weapon_cd = weapon_map.get(weapon_cd, weapon_cd)\r
770 \r
771             seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')\r
772             pwstat_id = session.execute(seq)\r
773             pwstat = PlayerWeaponStat()\r
774             pwstat.player_weapon_stats_id = pwstat_id\r
775             pwstat.player_id = player.player_id\r
776             pwstat.game_id = game.game_id\r
777             pwstat.player_game_stat_id = pgstat.player_game_stat_id\r
778             pwstat.weapon_cd = mapped_weapon_cd\r
779 \r
780             if 'n' in events:\r
781                 pwstat.nick = events['n']\r
782             else:\r
783                 pwstat.nick = events['P']\r
784 \r
785             if 'acc-' + weapon_cd + '-cnt-fired' in events:\r
786                 pwstat.fired = int(round(float(\r
787                         events['acc-' + weapon_cd + '-cnt-fired'])))\r
788             if 'acc-' + weapon_cd + '-fired' in events:\r
789                 pwstat.max = int(round(float(\r
790                         events['acc-' + weapon_cd + '-fired'])))\r
791             if 'acc-' + weapon_cd + '-cnt-hit' in events:\r
792                 pwstat.hit = int(round(float(\r
793                         events['acc-' + weapon_cd + '-cnt-hit'])))\r
794             if 'acc-' + weapon_cd + '-hit' in events:\r
795                 pwstat.actual = int(round(float(\r
796                         events['acc-' + weapon_cd + '-hit'])))\r
797             if 'acc-' + weapon_cd + '-frags' in events:\r
798                 pwstat.frags = int(round(float(\r
799                         events['acc-' + weapon_cd + '-frags'])))\r
800 \r
801             if is_doubled:\r
802                 pwstat.fired = pwstat.fired/2\r
803                 pwstat.max = pwstat.max/2\r
804                 pwstat.hit = pwstat.hit/2\r
805                 pwstat.actual = pwstat.actual/2\r
806                 pwstat.frags = pwstat.frags/2\r
807 \r
808             session.add(pwstat)\r
809             pwstats.append(pwstat)\r
810 \r
811     return pwstats\r
812 \r
813 \r
814 def create_elos(session, game):\r
815     """Elo handler for all game types."""\r
816     try:\r
817         process_elos(game, session)\r
818     except Exception as e:\r
819         log.debug('Error (non-fatal): elo processing failed.')\r
820 \r
821 \r
822 def submit_stats(request):\r
823     """\r
824     Entry handler for POST stats submissions.\r
825     """\r
826     try:\r
827         # placeholder for the actual session\r
828         session = None\r
829 \r
830         log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +\r
831                 "----- END REQUEST BODY -----\n\n")\r
832 \r
833         (idfp, status) = verify_request(request)\r
834         (game_meta, raw_players, raw_teams) = parse_stats_submission(request.body)\r
835         revision = game_meta.get('R', 'unknown')\r
836         duration = game_meta.get('D', None)\r
837 \r
838         # only players present at the end of the match are eligible for stats\r
839         raw_players = filter(played_in_game, raw_players)\r
840 \r
841         do_precondition_checks(request, game_meta, raw_players)\r
842 \r
843         # the "duel" gametype is fake\r
844         if len(raw_players) == 2 \\r
845             and num_real_players(raw_players) == 2 \\r
846             and game_meta['G'] == 'dm':\r
847             game_meta['G'] = 'duel'\r
848 \r
849         #----------------------------------------------------------------------\r
850         # Actual setup (inserts/updates) below here\r
851         #----------------------------------------------------------------------\r
852         session = DBSession()\r
853 \r
854         game_type_cd = game_meta['G']\r
855 \r
856         # All game types create Game, Server, Map, and Player records\r
857         # the same way.\r
858         server = get_or_create_server(\r
859                 session  = session,\r
860                 hashkey  = idfp,\r
861                 name     = game_meta['S'],\r
862                 revision = revision,\r
863                 ip_addr  = get_remote_addr(request),\r
864                 port     = game_meta.get('U', None))\r
865 \r
866         gmap = get_or_create_map(\r
867                 session = session,\r
868                 name    = game_meta['M'])\r
869 \r
870         game = create_game(\r
871                 session      = session,\r
872                 start_dt     = datetime.datetime.utcnow(),\r
873                 server_id    = server.server_id,\r
874                 game_type_cd = game_type_cd,\r
875                 map_id       = gmap.map_id,\r
876                 match_id     = game_meta['I'],\r
877                 duration     = duration,\r
878                 mod          = game_meta.get('O', None))\r
879 \r
880         # keep track of the players we've seen\r
881         player_ids = []\r
882         for events in raw_players:\r
883             player = get_or_create_player(\r
884                 session = session,\r
885                 hashkey = events['P'],\r
886                 nick    = events.get('n', None))\r
887 \r
888             pgstat = create_game_stat(session, game_meta, game, server,\r
889                     gmap, player, events)\r
890 \r
891             if player.player_id > 1:\r
892                 anticheats = create_anticheats(session, pgstat, game, player,\r
893                     events)\r
894                 player_ids.append(player.player_id)\r
895 \r
896             if should_do_weapon_stats(game_type_cd) and player.player_id > 1:\r
897                 pwstats = create_weapon_stats(session, game_meta, game, player,\r
898                         pgstat, events)\r
899 \r
900         # store them on games for easy access\r
901         game.players = player_ids\r
902 \r
903         for events in raw_teams:\r
904             try:\r
905                 teamstat = create_team_stat(session, game, events)\r
906             except Exception as e:\r
907                 raise e\r
908 \r
909         if should_do_elos(game_type_cd):\r
910             create_elos(session, game)\r
911 \r
912         session.commit()\r
913         log.debug('Success! Stats recorded.')\r
914         return Response('200 OK')\r
915     except Exception as e:\r
916         if session:\r
917             session.rollback()\r
918         raise e\r