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
139 # store new hashkey
\r
140 if server.hashkey != hashkey:
\r
141 server.hashkey = hashkey
\r
142 session.add(server)
\r
144 log.debug("Found existing server {0}".format(server.server_id))
\r
146 except MultipleResultsFound, e:
\r
147 # multiple found, so also filter by hashkey
\r
148 server = session.query(Server).filter_by(name=name).\
\r
149 filter_by(hashkey=hashkey).one()
\r
150 log.debug("Found existing server {0}".format(server.server_id))
\r
152 except NoResultFound, e:
\r
153 # not found, create one
\r
154 server = Server(name=name, hashkey=hashkey)
\r
155 session.add(server)
\r
157 log.debug("Created server {0} with hashkey {1}".format(
\r
158 server.server_id, server.hashkey))
\r
163 def get_or_create_map(session=None, name=None):
\r
165 Find a map by name or create one if not found. Parameters:
\r
167 session - SQLAlchemy database session factory
\r
168 name - map name of the map to be found or created
\r
171 # find one by the name, if it exists
\r
172 gmap = session.query(Map).filter_by(name=name).one()
\r
173 log.debug("Found map id {0}: {1}".format(gmap.map_id,
\r
175 except NoResultFound, e:
\r
176 gmap = Map(name=name)
\r
179 log.debug("Created map id {0}: {1}".format(gmap.map_id,
\r
181 except MultipleResultsFound, e:
\r
182 # multiple found, so use the first one but warn
\r
184 gmaps = session.query(Map).filter_by(name=name).order_by(
\r
187 log.debug("Found map id {0}: {1} but found \
\r
188 multiple".format(gmap.map_id, gmap.name))
\r
193 def create_game(session=None, start_dt=None, game_type_cd=None,
\r
194 server_id=None, map_id=None, winner=None):
\r
196 Creates a game. Parameters:
\r
198 session - SQLAlchemy database session factory
\r
199 start_dt - when the game started (datetime object)
\r
200 game_type_cd - the game type of the game being played
\r
201 server_id - server identifier of the server hosting the game
\r
202 map_id - map on which the game was played
\r
203 winner - the team id of the team that won
\r
206 game = Game(start_dt=start_dt, game_type_cd=game_type_cd,
\r
207 server_id=server_id, map_id=map_id, winner=winner)
\r
210 log.debug("Created game id {0} on server {1}, map {2} at \
\r
211 {3}".format(game.game_id,
\r
212 server_id, map_id, start_dt))
\r
217 def get_or_create_player(session=None, hashkey=None, nick=None):
\r
219 Finds a player by hashkey or creates a new one (along with a
\r
220 corresponding hashkey entry. Parameters:
\r
222 session - SQLAlchemy database session factory
\r
223 hashkey - hashkey of the player to be found or created
\r
224 nick - nick of the player (in case of a first time create)
\r
227 if re.search('^bot#\d+$', hashkey):
\r
228 player = session.query(Player).filter_by(player_id=1).one()
\r
229 # if we have an untracked player
\r
230 elif re.search('^player#\d+$', hashkey):
\r
231 player = session.query(Player).filter_by(player_id=2).one()
\r
232 # else it is a tracked player
\r
234 # see if the player is already in the database
\r
235 # if not, create one and the hashkey along with it
\r
237 hashkey = session.query(Hashkey).filter_by(
\r
238 hashkey=hashkey).one()
\r
239 player = session.query(Player).filter_by(
\r
240 player_id=hashkey.player_id).one()
\r
241 log.debug("Found existing player {0} with hashkey {1}".format(
\r
242 player.player_id, hashkey.hashkey))
\r
245 session.add(player)
\r
248 # if nick is given to us, use it. If not, use "Anonymous Player"
\r
249 # with a suffix added for uniqueness.
\r
251 player.nick = nick[:128]
\r
253 player.nick = "Anonymous Player #{0}".format(player.player_id)
\r
255 hashkey = Hashkey(player_id=player.player_id, hashkey=hashkey)
\r
256 session.add(hashkey)
\r
257 log.debug("Created player {0} ({2}) with hashkey {1}".format(
\r
258 player.player_id, hashkey.hashkey, player.nick.encode('utf-8')))
\r
262 def create_player_game_stat(session=None, player=None,
\r
263 game=None, player_events=None):
\r
265 Creates game statistics for a given player in a given game. Parameters:
\r
267 session - SQLAlchemy session factory
\r
268 player - Player record of the player who owns the stats
\r
269 game - Game record for the game to which the stats pertain
\r
270 player_events - dictionary for the actual stats that need to be transformed
\r
273 # in here setup default values (e.g. if game type is CTF then
\r
274 # set kills=0, score=0, captures=0, pickups=0, fckills=0, etc
\r
275 # TODO: use game's create date here instead of now()
\r
276 pgstat = PlayerGameStat(create_dt=datetime.datetime.now())
\r
278 # set player id from player record
\r
279 pgstat.player_id = player.player_id
\r
281 #set game id from game record
\r
282 pgstat.game_id = game.game_id
\r
284 # all games have a score
\r
287 if game.game_type_cd == 'dm':
\r
290 pgstat.suicides = 0
\r
291 elif game.game_type_cd == 'ctf':
\r
293 pgstat.captures = 0
\r
297 pgstat.carrier_frags = 0
\r
299 for (key,value) in player_events.items():
\r
300 if key == 'n': pgstat.nick = value[:128]
\r
301 if key == 't': pgstat.team = value
\r
302 if key == 'rank': pgstat.rank = value
\r
303 if key == 'alivetime':
\r
304 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(value))))
\r
305 if key == 'scoreboard-drops': pgstat.drops = value
\r
306 if key == 'scoreboard-returns': pgstat.returns = value
\r
307 if key == 'scoreboard-fckills': pgstat.carrier_frags = value
\r
308 if key == 'scoreboard-pickups': pgstat.pickups = value
\r
309 if key == 'scoreboard-caps': pgstat.captures = value
\r
310 if key == 'scoreboard-score': pgstat.score = value
\r
311 if key == 'scoreboard-deaths': pgstat.deaths = value
\r
312 if key == 'scoreboard-kills': pgstat.kills = value
\r
313 if key == 'scoreboard-suicides': pgstat.suicides = value
\r
315 # check to see if we had a name, and if
\r
316 # not use the name from the player id
\r
317 if pgstat.nick == None:
\r
318 pgstat.nick = player.nick
\r
320 # if the nick we end up with is different from the one in the
\r
321 # player record, change the nick to reflect the new value
\r
322 if pgstat.nick != player.nick and player.player_id > 1:
\r
323 register_new_nick(session, player, pgstat.nick)
\r
325 # if the player is ranked #1 and it is a team game, set the game's winner
\r
326 # to be the team of that player
\r
327 # FIXME: this is a hack, should be using the 'W' field (not present)
\r
328 if pgstat.rank == '1' and pgstat.team:
\r
329 game.winner = pgstat.team
\r
332 session.add(pgstat)
\r
338 def create_player_weapon_stats(session=None, player=None,
\r
339 game=None, pgstat=None, player_events=None):
\r
341 Creates accuracy records for each weapon used by a given player in a
\r
342 given game. Parameters:
\r
344 session - SQLAlchemy session factory object
\r
345 player - Player record who owns the weapon stats
\r
346 game - Game record in which the stats were created
\r
347 pgstat - Corresponding PlayerGameStat record for these weapon stats
\r
348 player_events - dictionary containing the raw weapon values that need to be
\r
353 for (key,value) in player_events.items():
\r
354 matched = re.search("acc-(.*?)-cnt-fired", key)
\r
356 weapon_cd = matched.group(1)
\r
357 pwstat = PlayerWeaponStat()
\r
358 pwstat.player_id = player.player_id
\r
359 pwstat.game_id = game.game_id
\r
360 pwstat.player_game_stat_id = pgstat.player_game_stat_id
\r
361 pwstat.weapon_cd = weapon_cd
\r
363 if 'n' in player_events:
\r
364 pwstat.nick = player_events['n']
\r
366 pwstat.nick = player_events['P']
\r
368 if 'acc-' + weapon_cd + '-cnt-fired' in player_events:
\r
369 pwstat.fired = int(round(float(
\r
370 player_events['acc-' + weapon_cd + '-cnt-fired'])))
\r
371 if 'acc-' + weapon_cd + '-fired' in player_events:
\r
372 pwstat.max = int(round(float(
\r
373 player_events['acc-' + weapon_cd + '-fired'])))
\r
374 if 'acc-' + weapon_cd + '-cnt-hit' in player_events:
\r
375 pwstat.hit = int(round(float(
\r
376 player_events['acc-' + weapon_cd + '-cnt-hit'])))
\r
377 if 'acc-' + weapon_cd + '-hit' in player_events:
\r
378 pwstat.actual = int(round(float(
\r
379 player_events['acc-' + weapon_cd + '-hit'])))
\r
380 if 'acc-' + weapon_cd + '-frags' in player_events:
\r
381 pwstat.frags = int(round(float(
\r
382 player_events['acc-' + weapon_cd + '-frags'])))
\r
384 session.add(pwstat)
\r
385 pwstats.append(pwstat)
\r
390 def parse_body(request):
\r
392 Parses the POST request body for a stats submission
\r
394 # storage vars for the request body
\r
397 current_team = None
\r
400 log.debug("----- BEGIN REQUEST BODY -----")
\r
401 log.debug(request.body)
\r
402 log.debug("----- END REQUEST BODY -----")
\r
404 for line in request.body.split('\n'):
\r
406 (key, value) = line.strip().split(' ', 1)
\r
408 # Server (S) and Nick (n) fields can have international characters.
\r
409 # We encode these as UTF-8.
\r
411 value = unicode(value, 'utf-8')
\r
413 if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W':
\r
414 game_meta[key] = value
\r
417 # if we were working on a player record already, append
\r
418 # it and work on a new one (only set team info)
\r
419 if len(player_events) != 0:
\r
420 players.append(player_events)
\r
423 player_events[key] = value
\r
426 (subkey, subvalue) = value.split(' ', 1)
\r
427 player_events[subkey] = subvalue
\r
429 player_events[key] = value
\r
431 player_events[key] = value
\r
433 # no key/value pair - move on to the next line
\r
436 # add the last player we were working on
\r
437 if len(player_events) > 0:
\r
438 players.append(player_events)
\r
440 return (game_meta, players)
\r
443 def create_player_stats(session=None, player=None, game=None,
\r
444 player_events=None):
\r
446 Creates player game and weapon stats according to what type of player
\r
448 pgstat = create_player_game_stat(session=session,
\r
449 player=player, game=game, player_events=player_events)
\r
451 #TODO: put this into a config setting in the ini file?
\r
452 if not re.search('^bot#\d+$', player_events['P']):
\r
453 create_player_weapon_stats(session=session,
\r
454 player=player, game=game, pgstat=pgstat,
\r
455 player_events=player_events)
\r
458 def stats_submit(request):
\r
460 Entry handler for POST stats submissions.
\r
463 session = DBSession()
\r
465 (idfp, status) = verify_request(request)
\r
467 raise Exception("Request is not verified.")
\r
469 (game_meta, players) = parse_body(request)
\r
471 if not has_required_metadata(game_meta):
\r
472 log.debug("Required game meta fields (T, G, M, or S) missing. "\
\r
474 raise Exception("Required game meta fields (T, G, M, or S) missing.")
\r
476 if not is_supported_gametype(game_meta['G']):
\r
477 raise Exception("Gametype not supported.")
\r
479 if not has_minimum_real_players(players):
\r
480 raise Exception("The number of real players is below the minimum. "\
\r
481 "Stats will be ignored.")
\r
483 server = get_or_create_server(session=session, hashkey=idfp,
\r
484 name=game_meta['S'])
\r
486 gmap = get_or_create_map(session=session, name=game_meta['M'])
\r
488 game = create_game(session=session,
\r
489 start_dt=datetime.datetime(
\r
490 *time.gmtime(float(game_meta['T']))[:6]),
\r
491 server_id=server.server_id, game_type_cd=game_meta['G'],
\r
492 map_id=gmap.map_id)
\r
494 # find or create a record for each player
\r
495 # and add stats for each if they were present at the end
\r
497 for player_events in players:
\r
498 if 'n' in player_events:
\r
499 nick = player_events['n']
\r
503 if 'matches' in player_events and 'scoreboardvalid' \
\r
505 player = get_or_create_player(session=session,
\r
506 hashkey=player_events['P'], nick=nick)
\r
507 log.debug('Creating stats for %s' % player_events['P'])
\r
508 create_player_stats(session=session, player=player, game=game,
\r
509 player_events=player_events)
\r
512 log.debug('Success! Stats recorded.')
\r
513 return Response('200 OK')
\r
514 except Exception as e:
\r