5 from pyramid.config import get_current_registry
\r
6 from pyramid.response import Response
\r
7 from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
\r
8 from xonstat.d0_blind_id import d0_blind_id_verify
\r
9 from xonstat.models import *
\r
10 from xonstat.util import strip_colors
\r
12 log = logging.getLogger(__name__)
\r
14 def is_supported_gametype(gametype):
\r
15 """Whether a gametype is supported or not"""
\r
16 flg_supported = True
\r
18 if gametype == 'cts' or gametype == 'ca' or gametype == 'lms':
\r
19 flg_supported = False
\r
21 return flg_supported
\r
24 def verify_request(request):
\r
25 (idfp, status) = d0_blind_id_verify(
\r
26 sig=request.headers['X-D0-Blind-Id-Detached-Signature'],
\r
28 postdata=request.body)
\r
30 log.debug('\nidfp: {0}\nstatus: {1}'.format(idfp, status))
\r
32 return (idfp, status)
\r
35 def has_minimum_real_players(player_events):
\r
37 Determines if the collection of player events has enough "real" players
\r
38 to store in the database. The minimum setting comes from the config file
\r
39 under the setting xonstat.minimum_real_players.
\r
41 flg_has_min_real_players = True
\r
43 settings = get_current_registry().settings
\r
45 minimum_required_players = int(
\r
46 settings['xonstat.minimum_required_players'])
\r
48 minimum_required_players = 2
\r
51 for events in player_events:
\r
52 if is_real_player(events):
\r
55 #TODO: put this into a config setting in the ini file?
\r
56 if real_players < minimum_required_players:
\r
57 flg_has_min_real_players = False
\r
59 return flg_has_min_real_players
\r
62 def has_required_metadata(metadata):
\r
64 Determines if a give set of metadata has enough data to create a game,
\r
65 server, and map with.
\r
67 flg_has_req_metadata = True
\r
69 if 'T' not in metadata or\
\r
70 'G' not in metadata or\
\r
71 'M' not in metadata or\
\r
72 'S' not in metadata:
\r
73 flg_has_req_metadata = False
\r
75 return flg_has_req_metadata
\r
78 def is_real_player(events):
\r
80 Determines if a given set of player events correspond with a player who
\r
82 1) is not a bot (P event does not look like a bot)
\r
83 2) played in the game (matches 1)
\r
84 3) was present at the end of the game (scoreboardvalid 1)
\r
86 Returns True if the player meets the above conditions, and false otherwise.
\r
90 if not events['P'].startswith('bot'):
\r
91 # removing 'joins' here due to bug, but it should be here
\r
92 if 'matches' in events and 'scoreboardvalid' in events:
\r
98 def register_new_nick(session, player, new_nick):
\r
100 Change the player record's nick to the newly found nick. Store the old
\r
101 nick in the player_nicks table for that player.
\r
103 session - SQLAlchemy database session factory
\r
104 player - player record whose nick is changing
\r
105 new_nick - the new nickname
\r
107 # see if that nick already exists
\r
108 stripped_nick = strip_colors(player.nick)
\r
110 player_nick = session.query(PlayerNick).filter_by(
\r
111 player_id=player.player_id, stripped_nick=stripped_nick).one()
\r
112 except NoResultFound, e:
\r
113 # player_id/stripped_nick not found, create one
\r
114 # but we don't store "Anonymous Player #N"
\r
115 if not re.search('^Anonymous Player #\d+$', player.nick):
\r
116 player_nick = PlayerNick()
\r
117 player_nick.player_id = player.player_id
\r
118 player_nick.stripped_nick = stripped_nick
\r
119 player_nick.nick = player.nick
\r
120 session.add(player_nick)
\r
122 # We change to the new nick regardless
\r
123 player.nick = new_nick
\r
124 session.add(player)
\r
127 def get_or_create_server(session=None, name=None, hashkey=None):
\r
129 Find a server by name or create one if not found. Parameters:
\r
131 session - SQLAlchemy database session factory
\r
132 name - server name of the server to be found or created
\r
133 hashkey - server hashkey
\r
136 # find one by that name, if it exists
\r
137 server = session.query(Server).filter_by(name=name).one()
\r
138 log.debug("Found existing server {0}".format(server.server_id))
\r
140 except MultipleResultsFound, e:
\r
141 # multiple found, so also filter by hashkey
\r
142 server = session.query(Server).filter_by(name=name).\
\r
143 filter_by(hashkey=hashkey).one()
\r
144 log.debug("Found existing server {0}".format(server.server_id))
\r
146 except NoResultFound, e:
\r
147 # not found, create one
\r
148 server = Server(name=name, hashkey=hashkey)
\r
149 session.add(server)
\r
151 log.debug("Created server {0} with hashkey {1}".format(
\r
152 server.server_id, server.hashkey))
\r
157 def get_or_create_map(session=None, name=None):
\r
159 Find a map by name or create one if not found. Parameters:
\r
161 session - SQLAlchemy database session factory
\r
162 name - map name of the map to be found or created
\r
165 # find one by the name, if it exists
\r
166 gmap = session.query(Map).filter_by(name=name).one()
\r
167 log.debug("Found map id {0}: {1}".format(gmap.map_id,
\r
169 except NoResultFound, e:
\r
170 gmap = Map(name=name)
\r
173 log.debug("Created map id {0}: {1}".format(gmap.map_id,
\r
175 except MultipleResultsFound, e:
\r
176 # multiple found, so use the first one but warn
\r
178 gmaps = session.query(Map).filter_by(name=name).order_by(
\r
181 log.debug("Found map id {0}: {1} but found \
\r
182 multiple".format(gmap.map_id, gmap.name))
\r
187 def create_game(session=None, start_dt=None, game_type_cd=None,
\r
188 server_id=None, map_id=None, winner=None):
\r
190 Creates a game. Parameters:
\r
192 session - SQLAlchemy database session factory
\r
193 start_dt - when the game started (datetime object)
\r
194 game_type_cd - the game type of the game being played
\r
195 server_id - server identifier of the server hosting the game
\r
196 map_id - map on which the game was played
\r
197 winner - the team id of the team that won
\r
200 game = Game(start_dt=start_dt, game_type_cd=game_type_cd,
\r
201 server_id=server_id, map_id=map_id, winner=winner)
\r
204 log.debug("Created game id {0} on server {1}, map {2} at \
\r
205 {3}".format(game.game_id,
\r
206 server_id, map_id, start_dt))
\r
211 def get_or_create_player(session=None, hashkey=None, nick=None):
\r
213 Finds a player by hashkey or creates a new one (along with a
\r
214 corresponding hashkey entry. Parameters:
\r
216 session - SQLAlchemy database session factory
\r
217 hashkey - hashkey of the player to be found or created
\r
218 nick - nick of the player (in case of a first time create)
\r
221 if re.search('^bot#\d+$', hashkey):
\r
222 player = session.query(Player).filter_by(player_id=1).one()
\r
223 # if we have an untracked player
\r
224 elif re.search('^player#\d+$', hashkey):
\r
225 player = session.query(Player).filter_by(player_id=2).one()
\r
226 # else it is a tracked player
\r
228 # see if the player is already in the database
\r
229 # if not, create one and the hashkey along with it
\r
231 hashkey = session.query(Hashkey).filter_by(
\r
232 hashkey=hashkey).one()
\r
233 player = session.query(Player).filter_by(
\r
234 player_id=hashkey.player_id).one()
\r
235 log.debug("Found existing player {0} with hashkey {1}".format(
\r
236 player.player_id, hashkey.hashkey))
\r
239 session.add(player)
\r
242 # if nick is given to us, use it. If not, use "Anonymous Player"
\r
243 # with a suffix added for uniqueness.
\r
245 player.nick = nick[:128]
\r
247 player.nick = "Anonymous Player #{0}".format(player.player_id)
\r
249 hashkey = Hashkey(player_id=player.player_id, hashkey=hashkey)
\r
250 session.add(hashkey)
\r
251 log.debug("Created player {0} ({2}) with hashkey {1}".format(
\r
252 player.player_id, hashkey.hashkey, player.nick.encode('utf-8')))
\r
256 def create_player_game_stat(session=None, player=None,
\r
257 game=None, player_events=None):
\r
259 Creates game statistics for a given player in a given game. Parameters:
\r
261 session - SQLAlchemy session factory
\r
262 player - Player record of the player who owns the stats
\r
263 game - Game record for the game to which the stats pertain
\r
264 player_events - dictionary for the actual stats that need to be transformed
\r
267 # in here setup default values (e.g. if game type is CTF then
\r
268 # set kills=0, score=0, captures=0, pickups=0, fckills=0, etc
\r
269 # TODO: use game's create date here instead of now()
\r
270 pgstat = PlayerGameStat(create_dt=datetime.datetime.now())
\r
272 # set player id from player record
\r
273 pgstat.player_id = player.player_id
\r
275 #set game id from game record
\r
276 pgstat.game_id = game.game_id
\r
278 # all games have a score
\r
281 if game.game_type_cd == 'dm':
\r
284 pgstat.suicides = 0
\r
285 elif game.game_type_cd == 'ctf':
\r
287 pgstat.captures = 0
\r
291 pgstat.carrier_frags = 0
\r
293 for (key,value) in player_events.items():
\r
294 if key == 'n': pgstat.nick = value[:128]
\r
295 if key == 't': pgstat.team = value
\r
296 if key == 'rank': pgstat.rank = value
\r
297 if key == 'alivetime':
\r
298 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(value))))
\r
299 if key == 'scoreboard-drops': pgstat.drops = value
\r
300 if key == 'scoreboard-returns': pgstat.returns = value
\r
301 if key == 'scoreboard-fckills': pgstat.carrier_frags = value
\r
302 if key == 'scoreboard-pickups': pgstat.pickups = value
\r
303 if key == 'scoreboard-caps': pgstat.captures = value
\r
304 if key == 'scoreboard-score': pgstat.score = value
\r
305 if key == 'scoreboard-deaths': pgstat.deaths = value
\r
306 if key == 'scoreboard-kills': pgstat.kills = value
\r
307 if key == 'scoreboard-suicides': pgstat.suicides = value
\r
309 # check to see if we had a name, and if
\r
310 # not use the name from the player id
\r
311 if pgstat.nick == None:
\r
312 pgstat.nick = player.nick
\r
314 # if the nick we end up with is different from the one in the
\r
315 # player record, change the nick to reflect the new value
\r
316 if pgstat.nick != player.nick and player.player_id > 1:
\r
317 register_new_nick(session, player, pgstat.nick)
\r
319 # if the player is ranked #1 and it is a team game, set the game's winner
\r
320 # to be the team of that player
\r
321 # FIXME: this is a hack, should be using the 'W' field (not present)
\r
322 if pgstat.rank == '1' and pgstat.team:
\r
323 game.winner = pgstat.team
\r
326 session.add(pgstat)
\r
332 def create_player_weapon_stats(session=None, player=None,
\r
333 game=None, pgstat=None, player_events=None):
\r
335 Creates accuracy records for each weapon used by a given player in a
\r
336 given game. Parameters:
\r
338 session - SQLAlchemy session factory object
\r
339 player - Player record who owns the weapon stats
\r
340 game - Game record in which the stats were created
\r
341 pgstat - Corresponding PlayerGameStat record for these weapon stats
\r
342 player_events - dictionary containing the raw weapon values that need to be
\r
347 for (key,value) in player_events.items():
\r
348 matched = re.search("acc-(.*?)-cnt-fired", key)
\r
350 weapon_cd = matched.group(1)
\r
351 pwstat = PlayerWeaponStat()
\r
352 pwstat.player_id = player.player_id
\r
353 pwstat.game_id = game.game_id
\r
354 pwstat.player_game_stat_id = pgstat.player_game_stat_id
\r
355 pwstat.weapon_cd = weapon_cd
\r
357 if 'n' in player_events:
\r
358 pwstat.nick = player_events['n']
\r
360 pwstat.nick = player_events['P']
\r
362 if 'acc-' + weapon_cd + '-cnt-fired' in player_events:
\r
363 pwstat.fired = int(round(float(
\r
364 player_events['acc-' + weapon_cd + '-cnt-fired'])))
\r
365 if 'acc-' + weapon_cd + '-fired' in player_events:
\r
366 pwstat.max = int(round(float(
\r
367 player_events['acc-' + weapon_cd + '-fired'])))
\r
368 if 'acc-' + weapon_cd + '-cnt-hit' in player_events:
\r
369 pwstat.hit = int(round(float(
\r
370 player_events['acc-' + weapon_cd + '-cnt-hit'])))
\r
371 if 'acc-' + weapon_cd + '-hit' in player_events:
\r
372 pwstat.actual = int(round(float(
\r
373 player_events['acc-' + weapon_cd + '-hit'])))
\r
374 if 'acc-' + weapon_cd + '-frags' in player_events:
\r
375 pwstat.frags = int(round(float(
\r
376 player_events['acc-' + weapon_cd + '-frags'])))
\r
378 session.add(pwstat)
\r
379 pwstats.append(pwstat)
\r
384 def parse_body(request):
\r
386 Parses the POST request body for a stats submission
\r
388 # storage vars for the request body
\r
391 current_team = None
\r
394 log.debug("----- BEGIN REQUEST BODY -----")
\r
395 log.debug(request.body)
\r
396 log.debug("----- END REQUEST BODY -----")
\r
398 for line in request.body.split('\n'):
\r
400 (key, value) = line.strip().split(' ', 1)
\r
402 # Server (S) and Nick (n) fields can have international characters.
\r
403 # We encode these as UTF-8.
\r
405 value = unicode(value, 'utf-8')
\r
407 if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W':
\r
408 game_meta[key] = value
\r
411 # if we were working on a player record already, append
\r
412 # it and work on a new one (only set team info)
\r
413 if len(player_events) != 0:
\r
414 players.append(player_events)
\r
417 player_events[key] = value
\r
420 (subkey, subvalue) = value.split(' ', 1)
\r
421 player_events[subkey] = subvalue
\r
423 player_events[key] = value
\r
425 player_events[key] = value
\r
427 # no key/value pair - move on to the next line
\r
430 # add the last player we were working on
\r
431 if len(player_events) > 0:
\r
432 players.append(player_events)
\r
434 return (game_meta, players)
\r
437 def create_player_stats(session=None, player=None, game=None,
\r
438 player_events=None):
\r
440 Creates player game and weapon stats according to what type of player
\r
442 pgstat = create_player_game_stat(session=session,
\r
443 player=player, game=game, player_events=player_events)
\r
445 #TODO: put this into a config setting in the ini file?
\r
446 if not re.search('^bot#\d+$', player_events['P']):
\r
447 create_player_weapon_stats(session=session,
\r
448 player=player, game=game, pgstat=pgstat,
\r
449 player_events=player_events)
\r
452 def stats_submit(request):
\r
454 Entry handler for POST stats submissions.
\r
457 session = DBSession()
\r
459 (idfp, status) = verify_request(request)
\r
461 raise Exception("Request is not verified.")
\r
463 (game_meta, players) = parse_body(request)
\r
465 if not has_required_metadata(game_meta):
\r
466 log.debug("Required game meta fields (T, G, M, or S) missing. "\
\r
468 raise Exception("Required game meta fields (T, G, M, or S) missing.")
\r
470 if not is_supported_gametype(game_meta['G']):
\r
471 raise Exception("Gametype not supported.")
\r
473 if not has_minimum_real_players(players):
\r
474 raise Exception("The number of real players is below the minimum. "\
\r
475 "Stats will be ignored.")
\r
477 server = get_or_create_server(session=session, hashkey=idfp,
\r
478 name=game_meta['S'])
\r
480 gmap = get_or_create_map(session=session, name=game_meta['M'])
\r
482 game = create_game(session=session,
\r
483 start_dt=datetime.datetime(
\r
484 *time.gmtime(float(game_meta['T']))[:6]),
\r
485 server_id=server.server_id, game_type_cd=game_meta['G'],
\r
486 map_id=gmap.map_id)
\r
488 # find or create a record for each player
\r
489 # and add stats for each if they were present at the end
\r
491 for player_events in players:
\r
492 if 'n' in player_events:
\r
493 nick = player_events['n']
\r
497 if 'matches' in player_events and 'scoreboardvalid' \
\r
499 player = get_or_create_player(session=session,
\r
500 hashkey=player_events['P'], nick=nick)
\r
501 log.debug('Creating stats for %s' % player_events['P'])
\r
502 create_player_stats(session=session, player=player, game=game,
\r
503 player_events=player_events)
\r
506 log.debug('Success! Stats recorded.')
\r
507 return Response('200 OK')
\r
508 except Exception as e:
\r