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