3 import pyramid.httpexceptions
\r
6 from pyramid.response import Response
\r
7 from sqlalchemy import Sequence
\r
8 from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
\r
9 from xonstat.d0_blind_id import d0_blind_id_verify
\r
10 from xonstat.models import *
\r
11 from xonstat.util import strip_colors, qfont_decode
\r
13 log = logging.getLogger(__name__)
\r
15 def is_supported_gametype(gametype):
\r
16 """Whether a gametype is supported or not"""
\r
17 flg_supported = True
\r
19 if gametype == 'cts' or gametype == 'ca' or gametype == 'lms':
\r
20 flg_supported = False
\r
22 return flg_supported
\r
25 def verify_request(request):
\r
27 (idfp, status) = d0_blind_id_verify(
\r
28 sig=request.headers['X-D0-Blind-Id-Detached-Signature'],
\r
30 postdata=request.body)
\r
32 log.debug('\nidfp: {0}\nstatus: {1}'.format(idfp, status))
\r
37 return (idfp, status)
\r
40 def has_minimum_real_players(settings, player_events):
\r
42 Determines if the collection of player events has enough "real" players
\r
43 to store in the database. The minimum setting comes from the config file
\r
44 under the setting xonstat.minimum_real_players.
\r
46 flg_has_min_real_players = True
\r
49 minimum_required_players = int(
\r
50 settings['xonstat.minimum_required_players'])
\r
52 minimum_required_players = 2
\r
55 for events in player_events:
\r
56 if is_real_player(events):
\r
59 #TODO: put this into a config setting in the ini file?
\r
60 if real_players < minimum_required_players:
\r
61 flg_has_min_real_players = False
\r
63 return flg_has_min_real_players
\r
66 def has_required_metadata(metadata):
\r
68 Determines if a give set of metadata has enough data to create a game,
\r
69 server, and map with.
\r
71 flg_has_req_metadata = True
\r
73 if 'T' not in metadata or\
\r
74 'G' not in metadata or\
\r
75 'M' not in metadata or\
\r
76 'S' not in metadata:
\r
77 flg_has_req_metadata = False
\r
79 return flg_has_req_metadata
\r
82 def is_real_player(events):
\r
84 Determines if a given set of player events correspond with a player who
\r
86 1) is not a bot (P event does not look like a bot)
\r
87 2) played in the game (matches 1)
\r
88 3) was present at the end of the game (scoreboardvalid 1)
\r
90 Returns True if the player meets the above conditions, and false otherwise.
\r
94 if not events['P'].startswith('bot'):
\r
95 # removing 'joins' here due to bug, but it should be here
\r
96 if 'matches' in events and 'scoreboardvalid' in events:
\r
102 def register_new_nick(session, player, new_nick):
\r
104 Change the player record's nick to the newly found nick. Store the old
\r
105 nick in the player_nicks table for that player.
\r
107 session - SQLAlchemy database session factory
\r
108 player - player record whose nick is changing
\r
109 new_nick - the new nickname
\r
111 # see if that nick already exists
\r
112 stripped_nick = strip_colors(player.nick)
\r
114 player_nick = session.query(PlayerNick).filter_by(
\r
115 player_id=player.player_id, stripped_nick=stripped_nick).one()
\r
116 except NoResultFound, e:
\r
117 # player_id/stripped_nick not found, create one
\r
118 # but we don't store "Anonymous Player #N"
\r
119 if not re.search('^Anonymous Player #\d+$', player.nick):
\r
120 player_nick = PlayerNick()
\r
121 player_nick.player_id = player.player_id
\r
122 player_nick.stripped_nick = player.stripped_nick
\r
123 player_nick.nick = player.nick
\r
124 session.add(player_nick)
\r
126 # We change to the new nick regardless
\r
127 player.nick = new_nick
\r
128 player.stripped_nick = strip_colors(new_nick)
\r
129 session.add(player)
\r
132 def get_or_create_server(session=None, name=None, hashkey=None):
\r
134 Find a server by name or create one if not found. Parameters:
\r
136 session - SQLAlchemy database session factory
\r
137 name - server name of the server to be found or created
\r
138 hashkey - server hashkey
\r
141 # find one by that name, if it exists
\r
142 server = session.query(Server).filter_by(name=name).one()
\r
144 # store new hashkey
\r
145 if server.hashkey != hashkey:
\r
146 server.hashkey = hashkey
\r
147 session.add(server)
\r
149 log.debug("Found existing server {0}".format(server.server_id))
\r
151 except MultipleResultsFound, e:
\r
152 # multiple found, so also filter by hashkey
\r
153 server = session.query(Server).filter_by(name=name).\
\r
154 filter_by(hashkey=hashkey).one()
\r
155 log.debug("Found existing server {0}".format(server.server_id))
\r
157 except NoResultFound, e:
\r
158 # not found, create one
\r
159 server = Server(name=name, hashkey=hashkey)
\r
160 session.add(server)
\r
162 log.debug("Created server {0} with hashkey {1}".format(
\r
163 server.server_id, server.hashkey))
\r
168 def get_or_create_map(session=None, name=None):
\r
170 Find a map by name or create one if not found. Parameters:
\r
172 session - SQLAlchemy database session factory
\r
173 name - map name of the map to be found or created
\r
176 # find one by the name, if it exists
\r
177 gmap = session.query(Map).filter_by(name=name).one()
\r
178 log.debug("Found map id {0}: {1}".format(gmap.map_id,
\r
180 except NoResultFound, e:
\r
181 gmap = Map(name=name)
\r
184 log.debug("Created map id {0}: {1}".format(gmap.map_id,
\r
186 except MultipleResultsFound, e:
\r
187 # multiple found, so use the first one but warn
\r
189 gmaps = session.query(Map).filter_by(name=name).order_by(
\r
192 log.debug("Found map id {0}: {1} but found \
\r
193 multiple".format(gmap.map_id, gmap.name))
\r
198 def create_game(session=None, start_dt=None, game_type_cd=None,
\r
199 server_id=None, map_id=None, winner=None):
\r
201 Creates a game. Parameters:
\r
203 session - SQLAlchemy database session factory
\r
204 start_dt - when the game started (datetime object)
\r
205 game_type_cd - the game type of the game being played
\r
206 server_id - server identifier of the server hosting the game
\r
207 map_id - map on which the game was played
\r
208 winner - the team id of the team that won
\r
210 seq = Sequence('games_game_id_seq')
\r
211 game_id = session.execute(seq)
\r
212 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
\r
213 server_id=server_id, map_id=map_id, winner=winner)
\r
215 log.debug("Created game id {0} on server {1}, map {2} at \
\r
216 {3}".format(game.game_id,
\r
217 server_id, map_id, start_dt))
\r
222 def get_or_create_player(session=None, hashkey=None, nick=None):
\r
224 Finds a player by hashkey or creates a new one (along with a
\r
225 corresponding hashkey entry. Parameters:
\r
227 session - SQLAlchemy database session factory
\r
228 hashkey - hashkey of the player to be found or created
\r
229 nick - nick of the player (in case of a first time create)
\r
232 if re.search('^bot#\d+$', hashkey):
\r
233 player = session.query(Player).filter_by(player_id=1).one()
\r
234 # if we have an untracked player
\r
235 elif re.search('^player#\d+$', hashkey):
\r
236 player = session.query(Player).filter_by(player_id=2).one()
\r
237 # else it is a tracked player
\r
239 # see if the player is already in the database
\r
240 # if not, create one and the hashkey along with it
\r
242 hashkey = session.query(Hashkey).filter_by(
\r
243 hashkey=hashkey).one()
\r
244 player = session.query(Player).filter_by(
\r
245 player_id=hashkey.player_id).one()
\r
246 log.debug("Found existing player {0} with hashkey {1}".format(
\r
247 player.player_id, hashkey.hashkey))
\r
250 session.add(player)
\r
253 # if nick is given to us, use it. If not, use "Anonymous Player"
\r
254 # with a suffix added for uniqueness.
\r
256 player.nick = nick[:128]
\r
257 player.stripped_nick = strip_colors(nick[:128])
\r
259 player.nick = "Anonymous Player #{0}".format(player.player_id)
\r
260 player.stripped_nick = player.nick
\r
262 hashkey = Hashkey(player_id=player.player_id, hashkey=hashkey)
\r
263 session.add(hashkey)
\r
264 log.debug("Created player {0} ({2}) with hashkey {1}".format(
\r
265 player.player_id, hashkey.hashkey, player.nick.encode('utf-8')))
\r
269 def create_player_game_stat(session=None, player=None,
\r
270 game=None, player_events=None):
\r
272 Creates game statistics for a given player in a given game. Parameters:
\r
274 session - SQLAlchemy session factory
\r
275 player - Player record of the player who owns the stats
\r
276 game - Game record for the game to which the stats pertain
\r
277 player_events - dictionary for the actual stats that need to be transformed
\r
280 # in here setup default values (e.g. if game type is CTF then
\r
281 # set kills=0, score=0, captures=0, pickups=0, fckills=0, etc
\r
282 # TODO: use game's create date here instead of now()
\r
283 seq = Sequence('player_game_stats_player_game_stat_id_seq')
\r
284 pgstat_id = session.execute(seq)
\r
285 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
\r
286 create_dt=datetime.datetime.utcnow())
\r
288 # set player id from player record
\r
289 pgstat.player_id = player.player_id
\r
291 #set game id from game record
\r
292 pgstat.game_id = game.game_id
\r
294 # all games have a score
\r
297 if game.game_type_cd == 'dm':
\r
300 pgstat.suicides = 0
\r
301 elif game.game_type_cd == 'ctf':
\r
303 pgstat.captures = 0
\r
307 pgstat.carrier_frags = 0
\r
309 for (key,value) in player_events.items():
\r
310 if key == 'n': pgstat.nick = value[:128]
\r
311 if key == 't': pgstat.team = value
\r
312 if key == 'rank': pgstat.rank = value
\r
313 if key == 'alivetime':
\r
314 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(value))))
\r
315 if key == 'scoreboard-drops': pgstat.drops = value
\r
316 if key == 'scoreboard-returns': pgstat.returns = value
\r
317 if key == 'scoreboard-fckills': pgstat.carrier_frags = value
\r
318 if key == 'scoreboard-pickups': pgstat.pickups = value
\r
319 if key == 'scoreboard-caps': pgstat.captures = value
\r
320 if key == 'scoreboard-score': pgstat.score = value
\r
321 if key == 'scoreboard-deaths': pgstat.deaths = value
\r
322 if key == 'scoreboard-kills': pgstat.kills = value
\r
323 if key == 'scoreboard-suicides': pgstat.suicides = value
\r
325 # check to see if we had a name, and if
\r
326 # not use the name from the player id
\r
327 if pgstat.nick == None:
\r
328 pgstat.nick = player.nick
\r
330 # if the nick we end up with is different from the one in the
\r
331 # player record, change the nick to reflect the new value
\r
332 if pgstat.nick != player.nick and player.player_id > 2:
\r
333 register_new_nick(session, player, pgstat.nick)
\r
335 # if the player is ranked #1 and it is a team game, set the game's winner
\r
336 # to be the team of that player
\r
337 # FIXME: this is a hack, should be using the 'W' field (not present)
\r
338 if pgstat.rank == '1' and pgstat.team:
\r
339 game.winner = pgstat.team
\r
342 session.add(pgstat)
\r
347 def create_player_weapon_stats(session=None, player=None,
\r
348 game=None, pgstat=None, player_events=None):
\r
350 Creates accuracy records for each weapon used by a given player in a
\r
351 given game. Parameters:
\r
353 session - SQLAlchemy session factory object
\r
354 player - Player record who owns the weapon stats
\r
355 game - Game record in which the stats were created
\r
356 pgstat - Corresponding PlayerGameStat record for these weapon stats
\r
357 player_events - dictionary containing the raw weapon values that need to be
\r
362 for (key,value) in player_events.items():
\r
363 matched = re.search("acc-(.*?)-cnt-fired", key)
\r
365 weapon_cd = matched.group(1)
\r
366 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
\r
367 pwstat_id = session.execute(seq)
\r
368 pwstat = PlayerWeaponStat()
\r
369 pwstat.player_weapon_stats_id = pwstat_id
\r
370 pwstat.player_id = player.player_id
\r
371 pwstat.game_id = game.game_id
\r
372 pwstat.player_game_stat_id = pgstat.player_game_stat_id
\r
373 pwstat.weapon_cd = weapon_cd
\r
375 if 'n' in player_events:
\r
376 pwstat.nick = player_events['n']
\r
378 pwstat.nick = player_events['P']
\r
380 if 'acc-' + weapon_cd + '-cnt-fired' in player_events:
\r
381 pwstat.fired = int(round(float(
\r
382 player_events['acc-' + weapon_cd + '-cnt-fired'])))
\r
383 if 'acc-' + weapon_cd + '-fired' in player_events:
\r
384 pwstat.max = int(round(float(
\r
385 player_events['acc-' + weapon_cd + '-fired'])))
\r
386 if 'acc-' + weapon_cd + '-cnt-hit' in player_events:
\r
387 pwstat.hit = int(round(float(
\r
388 player_events['acc-' + weapon_cd + '-cnt-hit'])))
\r
389 if 'acc-' + weapon_cd + '-hit' in player_events:
\r
390 pwstat.actual = int(round(float(
\r
391 player_events['acc-' + weapon_cd + '-hit'])))
\r
392 if 'acc-' + weapon_cd + '-frags' in player_events:
\r
393 pwstat.frags = int(round(float(
\r
394 player_events['acc-' + weapon_cd + '-frags'])))
\r
397 session.add(pwstat)
\r
399 pwstats.append(pwstat)
\r
404 def parse_body(request):
\r
406 Parses the POST request body for a stats submission
\r
408 # storage vars for the request body
\r
411 current_team = None
\r
414 log.debug("----- BEGIN REQUEST BODY -----")
\r
415 log.debug(request.body)
\r
416 log.debug("----- END REQUEST BODY -----")
\r
418 for line in request.body.split('\n'):
\r
420 (key, value) = line.strip().split(' ', 1)
\r
422 # Server (S) and Nick (n) fields can have international characters.
\r
423 # We first convert to UTF-8, then to ASCII. Characters will be lost
\r
424 # in this conversion for the sake of presenting what otherwise
\r
425 # would have to use CSS sprites.
\r
427 value = qfont_decode(unicode(value, 'utf-8'))
\r
429 if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W':
\r
430 game_meta[key] = value
\r
433 # if we were working on a player record already, append
\r
434 # it and work on a new one (only set team info)
\r
435 if len(player_events) != 0:
\r
436 players.append(player_events)
\r
439 player_events[key] = value
\r
442 (subkey, subvalue) = value.split(' ', 1)
\r
443 player_events[subkey] = subvalue
\r
445 player_events[key] = value
\r
447 player_events[key] = value
\r
449 # no key/value pair - move on to the next line
\r
452 # add the last player we were working on
\r
453 if len(player_events) > 0:
\r
454 players.append(player_events)
\r
456 return (game_meta, players)
\r
459 def create_player_stats(session=None, player=None, game=None,
\r
460 player_events=None):
\r
462 Creates player game and weapon stats according to what type of player
\r
464 pgstat = create_player_game_stat(session=session,
\r
465 player=player, game=game, player_events=player_events)
\r
467 #TODO: put this into a config setting in the ini file?
\r
468 if not re.search('^bot#\d+$', player_events['P']):
\r
469 create_player_weapon_stats(session=session,
\r
470 player=player, game=game, pgstat=pgstat,
\r
471 player_events=player_events)
\r
474 def stats_submit(request):
\r
476 Entry handler for POST stats submissions.
\r
479 session = DBSession()
\r
481 (idfp, status) = verify_request(request)
\r
483 raise pyramid.httpexceptions.HTTPUnauthorized
\r
485 (game_meta, players) = parse_body(request)
\r
487 if not has_required_metadata(game_meta):
\r
488 log.debug("Required game meta fields missing. "\
\r
490 raise pyramid.exceptions.HTTPUnprocessableEntity
\r
492 if not is_supported_gametype(game_meta['G']):
\r
493 raise pyramid.httpexceptions.HTTPOk
\r
495 if not has_minimum_real_players(request.registry.settings, players):
\r
496 log.debug("The number of real players is below the minimum. " +
\r
497 "Stats will be ignored.")
\r
498 raise pyramid.httpexceptions.HTTPOk
\r
500 server = get_or_create_server(session=session, hashkey=idfp,
\r
501 name=game_meta['S'])
\r
503 gmap = get_or_create_map(session=session, name=game_meta['M'])
\r
506 game = create_game(session=session,
\r
507 start_dt=datetime.datetime(
\r
508 *time.gmtime(float(game_meta['T']))[:6]),
\r
509 server_id=server.server_id, game_type_cd=game_meta['G'],
\r
510 map_id=gmap.map_id)
\r
513 # find or create a record for each player
\r
514 # and add stats for each if they were present at the end
\r
516 for player_events in players:
\r
517 if 'n' in player_events:
\r
518 nick = player_events['n']
\r
522 if 'matches' in player_events and 'scoreboardvalid' \
\r
524 player = get_or_create_player(session=session,
\r
525 hashkey=player_events['P'], nick=nick)
\r
526 log.debug('Creating stats for %s' % player_events['P'])
\r
527 create_player_stats(session=session, player=player, game=game,
\r
528 player_events=player_events)
\r
531 log.debug('Success! Stats recorded.')
\r
532 return Response('200 OK')
\r
533 except Exception as e:
\r