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