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
135 # see if the server is already in the database
\r
136 # if not, create one and the hashkey along with it
\r
138 hashkey = session.query(ServerHashkey).filter_by(
\r
139 hashkey=hashkey).one()
\r
140 server = session.query(Server).filter_by(
\r
141 server_id=hashkey.server_id).one()
\r
142 log.debug("Found existing server {0} with hashkey {1}".format(
\r
143 server.server_id, hashkey.hashkey))
\r
147 session.add(server)
\r
150 hashkey = ServerHashkey(server_id=server.server_id,
\r
152 session.add(hashkey)
\r
153 log.debug("Created server {0} with hashkey {1}".format(
\r
154 server.server_id, hashkey.hashkey))
\r
159 def get_or_create_map(session=None, name=None):
\r
161 Find a map by name or create one if not found. Parameters:
\r
163 session - SQLAlchemy database session factory
\r
164 name - map name of the map to be found or created
\r
167 # find one by the name, if it exists
\r
168 gmap = session.query(Map).filter_by(name=name).one()
\r
169 log.debug("Found map id {0}: {1}".format(gmap.map_id,
\r
171 except NoResultFound, e:
\r
172 gmap = Map(name=name)
\r
175 log.debug("Created map id {0}: {1}".format(gmap.map_id,
\r
177 except MultipleResultsFound, e:
\r
178 # multiple found, so use the first one but warn
\r
180 gmaps = session.query(Map).filter_by(name=name).order_by(
\r
183 log.debug("Found map id {0}: {1} but found \
\r
184 multiple".format(gmap.map_id, gmap.name))
\r
189 def create_game(session=None, start_dt=None, game_type_cd=None,
\r
190 server_id=None, map_id=None, winner=None):
\r
192 Creates a game. Parameters:
\r
194 session - SQLAlchemy database session factory
\r
195 start_dt - when the game started (datetime object)
\r
196 game_type_cd - the game type of the game being played
\r
197 server_id - server identifier of the server hosting the game
\r
198 map_id - map on which the game was played
\r
199 winner - the team id of the team that won
\r
202 game = Game(start_dt=start_dt, game_type_cd=game_type_cd,
\r
203 server_id=server_id, map_id=map_id, winner=winner)
\r
206 log.debug("Created game id {0} on server {1}, map {2} at \
\r
207 {3}".format(game.game_id,
\r
208 server_id, map_id, start_dt))
\r
213 def get_or_create_player(session=None, hashkey=None, nick=None):
\r
215 Finds a player by hashkey or creates a new one (along with a
\r
216 corresponding hashkey entry. Parameters:
\r
218 session - SQLAlchemy database session factory
\r
219 hashkey - hashkey of the player to be found or created
\r
220 nick - nick of the player (in case of a first time create)
\r
223 if re.search('^bot#\d+$', hashkey):
\r
224 player = session.query(Player).filter_by(player_id=1).one()
\r
225 # if we have an untracked player
\r
226 elif re.search('^player#\d+$', hashkey):
\r
227 player = session.query(Player).filter_by(player_id=2).one()
\r
228 # else it is a tracked player
\r
230 # see if the player is already in the database
\r
231 # if not, create one and the hashkey along with it
\r
233 hashkey = session.query(Hashkey).filter_by(
\r
234 hashkey=hashkey).one()
\r
235 player = session.query(Player).filter_by(
\r
236 player_id=hashkey.player_id).one()
\r
237 log.debug("Found existing player {0} with hashkey {1}".format(
\r
238 player.player_id, hashkey.hashkey))
\r
241 session.add(player)
\r
244 # if nick is given to us, use it. If not, use "Anonymous Player"
\r
245 # with a suffix added for uniqueness.
\r
247 player.nick = nick[:128]
\r
249 player.nick = "Anonymous Player #{0}".format(player.player_id)
\r
251 hashkey = Hashkey(player_id=player.player_id, hashkey=hashkey)
\r
252 session.add(hashkey)
\r
253 log.debug("Created player {0} ({2}) with hashkey {1}".format(
\r
254 player.player_id, hashkey.hashkey, player.nick.encode('utf-8')))
\r
258 def create_player_game_stat(session=None, player=None,
\r
259 game=None, player_events=None):
\r
261 Creates game statistics for a given player in a given game. Parameters:
\r
263 session - SQLAlchemy session factory
\r
264 player - Player record of the player who owns the stats
\r
265 game - Game record for the game to which the stats pertain
\r
266 player_events - dictionary for the actual stats that need to be transformed
\r
269 # in here setup default values (e.g. if game type is CTF then
\r
270 # set kills=0, score=0, captures=0, pickups=0, fckills=0, etc
\r
271 # TODO: use game's create date here instead of now()
\r
272 pgstat = PlayerGameStat(create_dt=datetime.datetime.now())
\r
274 # set player id from player record
\r
275 pgstat.player_id = player.player_id
\r
277 #set game id from game record
\r
278 pgstat.game_id = game.game_id
\r
280 # all games have a score
\r
283 if game.game_type_cd == 'dm':
\r
286 pgstat.suicides = 0
\r
287 elif game.game_type_cd == 'ctf':
\r
289 pgstat.captures = 0
\r
293 pgstat.carrier_frags = 0
\r
295 for (key,value) in player_events.items():
\r
296 if key == 'n': pgstat.nick = value[:128]
\r
297 if key == 't': pgstat.team = value
\r
298 if key == 'rank': pgstat.rank = value
\r
299 if key == 'alivetime':
\r
300 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(value))))
\r
301 if key == 'scoreboard-drops': pgstat.drops = value
\r
302 if key == 'scoreboard-returns': pgstat.returns = value
\r
303 if key == 'scoreboard-fckills': pgstat.carrier_frags = value
\r
304 if key == 'scoreboard-pickups': pgstat.pickups = value
\r
305 if key == 'scoreboard-caps': pgstat.captures = value
\r
306 if key == 'scoreboard-score': pgstat.score = value
\r
307 if key == 'scoreboard-deaths': pgstat.deaths = value
\r
308 if key == 'scoreboard-kills': pgstat.kills = value
\r
309 if key == 'scoreboard-suicides': pgstat.suicides = value
\r
311 # check to see if we had a name, and if
\r
312 # not use the name from the player id
\r
313 if pgstat.nick == None:
\r
314 pgstat.nick = player.nick
\r
316 # if the nick we end up with is different from the one in the
\r
317 # player record, change the nick to reflect the new value
\r
318 if pgstat.nick != player.nick and player.player_id > 1:
\r
319 register_new_nick(session, player, pgstat.nick)
\r
321 # if the player is ranked #1 and it is a team game, set the game's winner
\r
322 # to be the team of that player
\r
323 # FIXME: this is a hack, should be using the 'W' field (not present)
\r
324 if pgstat.rank == '1' and pgstat.team:
\r
325 game.winner = pgstat.team
\r
328 session.add(pgstat)
\r
334 def create_player_weapon_stats(session=None, player=None,
\r
335 game=None, pgstat=None, player_events=None):
\r
337 Creates accuracy records for each weapon used by a given player in a
\r
338 given game. Parameters:
\r
340 session - SQLAlchemy session factory object
\r
341 player - Player record who owns the weapon stats
\r
342 game - Game record in which the stats were created
\r
343 pgstat - Corresponding PlayerGameStat record for these weapon stats
\r
344 player_events - dictionary containing the raw weapon values that need to be
\r
349 for (key,value) in player_events.items():
\r
350 matched = re.search("acc-(.*?)-cnt-fired", key)
\r
352 weapon_cd = matched.group(1)
\r
353 pwstat = PlayerWeaponStat()
\r
354 pwstat.player_id = player.player_id
\r
355 pwstat.game_id = game.game_id
\r
356 pwstat.player_game_stat_id = pgstat.player_game_stat_id
\r
357 pwstat.weapon_cd = weapon_cd
\r
359 if 'n' in player_events:
\r
360 pwstat.nick = player_events['n']
\r
362 pwstat.nick = player_events['P']
\r
364 if 'acc-' + weapon_cd + '-cnt-fired' in player_events:
\r
365 pwstat.fired = int(round(float(
\r
366 player_events['acc-' + weapon_cd + '-cnt-fired'])))
\r
367 if 'acc-' + weapon_cd + '-fired' in player_events:
\r
368 pwstat.max = int(round(float(
\r
369 player_events['acc-' + weapon_cd + '-fired'])))
\r
370 if 'acc-' + weapon_cd + '-cnt-hit' in player_events:
\r
371 pwstat.hit = int(round(float(
\r
372 player_events['acc-' + weapon_cd + '-cnt-hit'])))
\r
373 if 'acc-' + weapon_cd + '-hit' in player_events:
\r
374 pwstat.actual = int(round(float(
\r
375 player_events['acc-' + weapon_cd + '-hit'])))
\r
376 if 'acc-' + weapon_cd + '-frags' in player_events:
\r
377 pwstat.frags = int(round(float(
\r
378 player_events['acc-' + weapon_cd + '-frags'])))
\r
380 session.add(pwstat)
\r
381 pwstats.append(pwstat)
\r
386 def parse_body(request):
\r
388 Parses the POST request body for a stats submission
\r
390 # storage vars for the request body
\r
393 current_team = None
\r
396 log.debug("----- BEGIN REQUEST BODY -----")
\r
397 log.debug(request.body)
\r
398 log.debug("----- END REQUEST BODY -----")
\r
400 for line in request.body.split('\n'):
\r
402 (key, value) = line.strip().split(' ', 1)
\r
404 # Server (S) and Nick (n) fields can have international characters.
\r
405 # We encode these as UTF-8.
\r
407 value = unicode(value, 'utf-8')
\r
409 if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W':
\r
410 game_meta[key] = value
\r
413 # if we were working on a player record already, append
\r
414 # it and work on a new one (only set team info)
\r
415 if len(player_events) != 0:
\r
416 players.append(player_events)
\r
419 player_events[key] = value
\r
422 (subkey, subvalue) = value.split(' ', 1)
\r
423 player_events[subkey] = subvalue
\r
425 player_events[key] = value
\r
427 player_events[key] = value
\r
429 # no key/value pair - move on to the next line
\r
432 # add the last player we were working on
\r
433 if len(player_events) > 0:
\r
434 players.append(player_events)
\r
436 return (game_meta, players)
\r
439 def create_player_stats(session=None, player=None, game=None,
\r
440 player_events=None):
\r
442 Creates player game and weapon stats according to what type of player
\r
444 pgstat = create_player_game_stat(session=session,
\r
445 player=player, game=game, player_events=player_events)
\r
447 #TODO: put this into a config setting in the ini file?
\r
448 if not re.search('^bot#\d+$', player_events['P']):
\r
449 create_player_weapon_stats(session=session,
\r
450 player=player, game=game, pgstat=pgstat,
\r
451 player_events=player_events)
\r
454 def stats_submit(request):
\r
456 Entry handler for POST stats submissions.
\r
459 (idfp, status) = verify_request(request)
\r
461 raise Exception("Request is not verified.")
\r
463 session = DBSession()
\r
465 (game_meta, players) = parse_body(request)
\r
467 if not has_required_metadata(game_meta):
\r
468 log.debug("Required game meta fields (T, G, M, or S) missing. "\
\r
470 raise Exception("Required game meta fields (T, G, M, or S) missing.")
\r
472 if not is_supported_gametype(game_meta['G']):
\r
473 raise Exception("Gametype not supported.")
\r
475 if not has_minimum_real_players(players):
\r
476 raise Exception("The number of real players is below the minimum. "\
\r
477 "Stats will be ignored.")
\r
479 server = get_or_create_server(session=session, hashkey=idfp,
\r
480 name=game_meta['S'])
\r
482 gmap = get_or_create_map(session=session, name=game_meta['M'])
\r
484 game = create_game(session=session,
\r
485 start_dt=datetime.datetime(
\r
486 *time.gmtime(float(game_meta['T']))[:6]),
\r
487 server_id=server.server_id, game_type_cd=game_meta['G'],
\r
488 map_id=gmap.map_id)
\r
490 # find or create a record for each player
\r
491 # and add stats for each if they were present at the end
\r
493 for player_events in players:
\r
494 if 'n' in player_events:
\r
495 nick = player_events['n']
\r
499 if 'matches' in player_events and 'scoreboardvalid' \
\r
501 player = get_or_create_player(session=session,
\r
502 hashkey=player_events['P'], nick=nick)
\r
503 log.debug('Creating stats for %s' % player_events['P'])
\r
504 create_player_stats(session=session, player=player, game=game,
\r
505 player_events=player_events)
\r
508 log.debug('Success! Stats recorded.')
\r
509 return Response('200 OK')
\r
510 except Exception as e:
\r