3 import pyramid.httpexceptions
\r
6 from pyramid.config import get_current_registry
\r
7 from pyramid.response import Response
\r
8 from sqlalchemy import Sequence
\r
9 from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
\r
10 from xonstat.d0_blind_id import d0_blind_id_verify
\r
11 from xonstat.models import *
\r
12 from xonstat.util import strip_colors
\r
14 log = logging.getLogger(__name__)
\r
16 def is_supported_gametype(gametype):
\r
17 """Whether a gametype is supported or not"""
\r
18 flg_supported = True
\r
20 if gametype == 'cts' or gametype == 'ca' or gametype == 'lms':
\r
21 flg_supported = False
\r
23 return flg_supported
\r
26 def verify_request(request):
\r
28 (idfp, status) = d0_blind_id_verify(
\r
29 sig=request.headers['X-D0-Blind-Id-Detached-Signature'],
\r
31 postdata=request.body)
\r
33 log.debug('\nidfp: {0}\nstatus: {1}'.format(idfp, status))
\r
38 return (idfp, status)
\r
41 def has_minimum_real_players(player_events):
\r
43 Determines if the collection of player events has enough "real" players
\r
44 to store in the database. The minimum setting comes from the config file
\r
45 under the setting xonstat.minimum_real_players.
\r
47 flg_has_min_real_players = True
\r
49 settings = get_current_registry().settings
\r
51 minimum_required_players = int(
\r
52 settings['xonstat.minimum_required_players'])
\r
54 minimum_required_players = 2
\r
57 for events in player_events:
\r
58 if is_real_player(events):
\r
61 #TODO: put this into a config setting in the ini file?
\r
62 if real_players < minimum_required_players:
\r
63 flg_has_min_real_players = False
\r
65 return flg_has_min_real_players
\r
68 def has_required_metadata(metadata):
\r
70 Determines if a give set of metadata has enough data to create a game,
\r
71 server, and map with.
\r
73 flg_has_req_metadata = True
\r
75 if 'T' not in metadata or\
\r
76 'G' not in metadata or\
\r
77 'M' not in metadata or\
\r
78 'S' not in metadata:
\r
79 flg_has_req_metadata = False
\r
81 return flg_has_req_metadata
\r
84 def is_real_player(events):
\r
86 Determines if a given set of player events correspond with a player who
\r
88 1) is not a bot (P event does not look like a bot)
\r
89 2) played in the game (matches 1)
\r
90 3) was present at the end of the game (scoreboardvalid 1)
\r
92 Returns True if the player meets the above conditions, and false otherwise.
\r
96 if not events['P'].startswith('bot'):
\r
97 # removing 'joins' here due to bug, but it should be here
\r
98 if 'matches' in events and 'scoreboardvalid' in events:
\r
104 def register_new_nick(session, player, new_nick):
\r
106 Change the player record's nick to the newly found nick. Store the old
\r
107 nick in the player_nicks table for that player.
\r
109 session - SQLAlchemy database session factory
\r
110 player - player record whose nick is changing
\r
111 new_nick - the new nickname
\r
113 # see if that nick already exists
\r
114 stripped_nick = strip_colors(player.nick)
\r
116 player_nick = session.query(PlayerNick).filter_by(
\r
117 player_id=player.player_id, stripped_nick=stripped_nick).one()
\r
118 except NoResultFound, e:
\r
119 # player_id/stripped_nick not found, create one
\r
120 # but we don't store "Anonymous Player #N"
\r
121 if not re.search('^Anonymous Player #\d+$', player.nick):
\r
122 player_nick = PlayerNick()
\r
123 player_nick.player_id = player.player_id
\r
124 player_nick.stripped_nick = stripped_nick
\r
125 player_nick.nick = player.nick
\r
126 session.add(player_nick)
\r
128 # We change to the new nick regardless
\r
129 player.nick = new_nick
\r
130 session.add(player)
\r
133 def get_or_create_server(session=None, name=None, hashkey=None):
\r
135 Find a server by name or create one if not found. Parameters:
\r
137 session - SQLAlchemy database session factory
\r
138 name - server name of the server to be found or created
\r
139 hashkey - server hashkey
\r
142 # find one by that name, if it exists
\r
143 server = session.query(Server).filter_by(name=name).one()
\r
145 # store new hashkey
\r
146 if server.hashkey != hashkey:
\r
147 server.hashkey = hashkey
\r
148 session.add(server)
\r
150 log.debug("Found existing server {0}".format(server.server_id))
\r
152 except MultipleResultsFound, e:
\r
153 # multiple found, so also filter by hashkey
\r
154 server = session.query(Server).filter_by(name=name).\
\r
155 filter_by(hashkey=hashkey).one()
\r
156 log.debug("Found existing server {0}".format(server.server_id))
\r
158 except NoResultFound, e:
\r
159 # not found, create one
\r
160 server = Server(name=name, hashkey=hashkey)
\r
161 session.add(server)
\r
163 log.debug("Created server {0} with hashkey {1}".format(
\r
164 server.server_id, server.hashkey))
\r
169 def get_or_create_map(session=None, name=None):
\r
171 Find a map by name or create one if not found. Parameters:
\r
173 session - SQLAlchemy database session factory
\r
174 name - map name of the map to be found or created
\r
177 # find one by the name, if it exists
\r
178 gmap = session.query(Map).filter_by(name=name).one()
\r
179 log.debug("Found map id {0}: {1}".format(gmap.map_id,
\r
181 except NoResultFound, e:
\r
182 gmap = Map(name=name)
\r
185 log.debug("Created map id {0}: {1}".format(gmap.map_id,
\r
187 except MultipleResultsFound, e:
\r
188 # multiple found, so use the first one but warn
\r
190 gmaps = session.query(Map).filter_by(name=name).order_by(
\r
193 log.debug("Found map id {0}: {1} but found \
\r
194 multiple".format(gmap.map_id, gmap.name))
\r
199 def create_game(session=None, start_dt=None, game_type_cd=None,
\r
200 server_id=None, map_id=None, winner=None):
\r
202 Creates a game. Parameters:
\r
204 session - SQLAlchemy database session factory
\r
205 start_dt - when the game started (datetime object)
\r
206 game_type_cd - the game type of the game being played
\r
207 server_id - server identifier of the server hosting the game
\r
208 map_id - map on which the game was played
\r
209 winner - the team id of the team that won
\r
211 seq = Sequence('games_game_id_seq')
\r
212 game_id = session.execute(seq)
\r
213 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
\r
214 server_id=server_id, map_id=map_id, winner=winner)
\r
216 log.debug("Created game id {0} on server {1}, map {2} at \
\r
217 {3}".format(game.game_id,
\r
218 server_id, map_id, start_dt))
\r
223 def get_or_create_player(session=None, hashkey=None, nick=None):
\r
225 Finds a player by hashkey or creates a new one (along with a
\r
226 corresponding hashkey entry. Parameters:
\r
228 session - SQLAlchemy database session factory
\r
229 hashkey - hashkey of the player to be found or created
\r
230 nick - nick of the player (in case of a first time create)
\r
233 if re.search('^bot#\d+$', hashkey):
\r
234 player = session.query(Player).filter_by(player_id=1).one()
\r
235 # if we have an untracked player
\r
236 elif re.search('^player#\d+$', hashkey):
\r
237 player = session.query(Player).filter_by(player_id=2).one()
\r
238 # else it is a tracked player
\r
240 # see if the player is already in the database
\r
241 # if not, create one and the hashkey along with it
\r
243 hashkey = session.query(Hashkey).filter_by(
\r
244 hashkey=hashkey).one()
\r
245 player = session.query(Player).filter_by(
\r
246 player_id=hashkey.player_id).one()
\r
247 log.debug("Found existing player {0} with hashkey {1}".format(
\r
248 player.player_id, hashkey.hashkey))
\r
251 session.add(player)
\r
254 # if nick is given to us, use it. If not, use "Anonymous Player"
\r
255 # with a suffix added for uniqueness.
\r
257 player.nick = nick[:128]
\r
259 player.nick = "Anonymous Player #{0}".format(player.player_id)
\r
261 hashkey = Hashkey(player_id=player.player_id, hashkey=hashkey)
\r
262 session.add(hashkey)
\r
263 log.debug("Created player {0} ({2}) with hashkey {1}".format(
\r
264 player.player_id, hashkey.hashkey, player.nick.encode('utf-8')))
\r
268 def create_player_game_stat(session=None, player=None,
\r
269 game=None, player_events=None):
\r
271 Creates game statistics for a given player in a given game. Parameters:
\r
273 session - SQLAlchemy session factory
\r
274 player - Player record of the player who owns the stats
\r
275 game - Game record for the game to which the stats pertain
\r
276 player_events - dictionary for the actual stats that need to be transformed
\r
279 # in here setup default values (e.g. if game type is CTF then
\r
280 # set kills=0, score=0, captures=0, pickups=0, fckills=0, etc
\r
281 # TODO: use game's create date here instead of now()
\r
282 seq = Sequence('player_game_stats_player_game_stat_id_seq')
\r
283 pgstat_id = session.execute(seq)
\r
284 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
\r
285 create_dt=datetime.datetime.now())
\r
287 # set player id from player record
\r
288 pgstat.player_id = player.player_id
\r
290 #set game id from game record
\r
291 pgstat.game_id = game.game_id
\r
293 # all games have a score
\r
296 if game.game_type_cd == 'dm':
\r
299 pgstat.suicides = 0
\r
300 elif game.game_type_cd == 'ctf':
\r
302 pgstat.captures = 0
\r
306 pgstat.carrier_frags = 0
\r
308 for (key,value) in player_events.items():
\r
309 if key == 'n': pgstat.nick = value[:128]
\r
310 if key == 't': pgstat.team = value
\r
311 if key == 'rank': pgstat.rank = value
\r
312 if key == 'alivetime':
\r
313 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(value))))
\r
314 if key == 'scoreboard-drops': pgstat.drops = value
\r
315 if key == 'scoreboard-returns': pgstat.returns = value
\r
316 if key == 'scoreboard-fckills': pgstat.carrier_frags = value
\r
317 if key == 'scoreboard-pickups': pgstat.pickups = value
\r
318 if key == 'scoreboard-caps': pgstat.captures = value
\r
319 if key == 'scoreboard-score': pgstat.score = value
\r
320 if key == 'scoreboard-deaths': pgstat.deaths = value
\r
321 if key == 'scoreboard-kills': pgstat.kills = value
\r
322 if key == 'scoreboard-suicides': pgstat.suicides = value
\r
324 # check to see if we had a name, and if
\r
325 # not use the name from the player id
\r
326 if pgstat.nick == None:
\r
327 pgstat.nick = player.nick
\r
329 # if the nick we end up with is different from the one in the
\r
330 # player record, change the nick to reflect the new value
\r
331 if pgstat.nick != player.nick and player.player_id > 2:
\r
332 register_new_nick(session, player, pgstat.nick)
\r
334 # if the player is ranked #1 and it is a team game, set the game's winner
\r
335 # to be the team of that player
\r
336 # FIXME: this is a hack, should be using the 'W' field (not present)
\r
337 if pgstat.rank == '1' and pgstat.team:
\r
338 game.winner = pgstat.team
\r
341 session.add(pgstat)
\r
346 def create_player_weapon_stats(session=None, player=None,
\r
347 game=None, pgstat=None, player_events=None):
\r
349 Creates accuracy records for each weapon used by a given player in a
\r
350 given game. Parameters:
\r
352 session - SQLAlchemy session factory object
\r
353 player - Player record who owns the weapon stats
\r
354 game - Game record in which the stats were created
\r
355 pgstat - Corresponding PlayerGameStat record for these weapon stats
\r
356 player_events - dictionary containing the raw weapon values that need to be
\r
361 for (key,value) in player_events.items():
\r
362 matched = re.search("acc-(.*?)-cnt-fired", key)
\r
364 weapon_cd = matched.group(1)
\r
365 pwstat = PlayerWeaponStat()
\r
366 pwstat.player_id = player.player_id
\r
367 pwstat.game_id = game.game_id
\r
368 pwstat.player_game_stat_id = pgstat.player_game_stat_id
\r
369 pwstat.weapon_cd = weapon_cd
\r
371 if 'n' in player_events:
\r
372 pwstat.nick = player_events['n']
\r
374 pwstat.nick = player_events['P']
\r
376 if 'acc-' + weapon_cd + '-cnt-fired' in player_events:
\r
377 pwstat.fired = int(round(float(
\r
378 player_events['acc-' + weapon_cd + '-cnt-fired'])))
\r
379 if 'acc-' + weapon_cd + '-fired' in player_events:
\r
380 pwstat.max = int(round(float(
\r
381 player_events['acc-' + weapon_cd + '-fired'])))
\r
382 if 'acc-' + weapon_cd + '-cnt-hit' in player_events:
\r
383 pwstat.hit = int(round(float(
\r
384 player_events['acc-' + weapon_cd + '-cnt-hit'])))
\r
385 if 'acc-' + weapon_cd + '-hit' in player_events:
\r
386 pwstat.actual = int(round(float(
\r
387 player_events['acc-' + weapon_cd + '-hit'])))
\r
388 if 'acc-' + weapon_cd + '-frags' in player_events:
\r
389 pwstat.frags = int(round(float(
\r
390 player_events['acc-' + weapon_cd + '-frags'])))
\r
392 session.add(pwstat)
\r
393 pwstats.append(pwstat)
\r
398 def parse_body(request):
\r
400 Parses the POST request body for a stats submission
\r
402 # storage vars for the request body
\r
405 current_team = None
\r
408 log.debug("----- BEGIN REQUEST BODY -----")
\r
409 log.debug(request.body)
\r
410 log.debug("----- END REQUEST BODY -----")
\r
412 for line in request.body.split('\n'):
\r
414 (key, value) = line.strip().split(' ', 1)
\r
416 # Server (S) and Nick (n) fields can have international characters.
\r
417 # We encode these as UTF-8.
\r
419 value = unicode(value, 'utf-8')
\r
421 if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W':
\r
422 game_meta[key] = value
\r
425 # if we were working on a player record already, append
\r
426 # it and work on a new one (only set team info)
\r
427 if len(player_events) != 0:
\r
428 players.append(player_events)
\r
431 player_events[key] = value
\r
434 (subkey, subvalue) = value.split(' ', 1)
\r
435 player_events[subkey] = subvalue
\r
437 player_events[key] = value
\r
439 player_events[key] = value
\r
441 # no key/value pair - move on to the next line
\r
444 # add the last player we were working on
\r
445 if len(player_events) > 0:
\r
446 players.append(player_events)
\r
448 return (game_meta, players)
\r
451 def create_player_stats(session=None, player=None, game=None,
\r
452 player_events=None):
\r
454 Creates player game and weapon stats according to what type of player
\r
456 pgstat = create_player_game_stat(session=session,
\r
457 player=player, game=game, player_events=player_events)
\r
459 #TODO: put this into a config setting in the ini file?
\r
460 if not re.search('^bot#\d+$', player_events['P']):
\r
461 create_player_weapon_stats(session=session,
\r
462 player=player, game=game, pgstat=pgstat,
\r
463 player_events=player_events)
\r
466 def stats_submit(request):
\r
468 Entry handler for POST stats submissions.
\r
471 session = DBSession()
\r
473 (idfp, status) = verify_request(request)
\r
475 raise pyramid.httpexceptions.HTTPUnauthorized
\r
477 (game_meta, players) = parse_body(request)
\r
479 if not has_required_metadata(game_meta):
\r
480 log.debug("Required game meta fields missing. "\
\r
482 raise pyramid.exceptions.HTTPUnprocessableEntity
\r
484 if not is_supported_gametype(game_meta['G']):
\r
485 raise pyramid.httpexceptions.HTTPOk
\r
487 if not has_minimum_real_players(players):
\r
488 log.debug("The number of real players is below the minimum. " +
\r
489 "Stats will be ignored.")
\r
490 raise pyramid.httpexceptions.HTTPOk
\r
492 server = get_or_create_server(session=session, hashkey=idfp,
\r
493 name=game_meta['S'])
\r
495 gmap = get_or_create_map(session=session, name=game_meta['M'])
\r
497 game = create_game(session=session,
\r
498 start_dt=datetime.datetime(
\r
499 *time.gmtime(float(game_meta['T']))[:6]),
\r
500 server_id=server.server_id, game_type_cd=game_meta['G'],
\r
501 map_id=gmap.map_id)
\r
503 # find or create a record for each player
\r
504 # and add stats for each if they were present at the end
\r
506 for player_events in players:
\r
507 if 'n' in player_events:
\r
508 nick = player_events['n']
\r
512 if 'matches' in player_events and 'scoreboardvalid' \
\r
514 player = get_or_create_player(session=session,
\r
515 hashkey=player_events['P'], nick=nick)
\r
516 log.debug('Creating stats for %s' % player_events['P'])
\r
517 create_player_stats(session=session, player=player, game=game,
\r
518 player_events=player_events)
\r
521 log.debug('Success! Stats recorded.')
\r
522 return Response('200 OK')
\r
523 except Exception as e:
\r