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 is_verified_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
38 def has_minimum_real_players(player_events):
\r
40 Determines if the collection of player events has enough "real" players
\r
41 to store in the database. The minimum setting comes from the config file
\r
42 under the setting xonstat.minimum_real_players.
\r
44 flg_has_min_real_players = True
\r
46 settings = get_current_registry().settings
\r
48 minimum_required_players = int(
\r
49 settings['xonstat.minimum_required_players'])
\r
51 minimum_required_players = 2
\r
54 for events in player_events:
\r
55 if is_real_player(events):
\r
58 #TODO: put this into a config setting in the ini file?
\r
59 if real_players < minimum_required_players:
\r
60 flg_has_min_real_players = False
\r
62 return flg_has_min_real_players
\r
65 def has_required_metadata(metadata):
\r
67 Determines if a give set of metadata has enough data to create a game,
\r
68 server, and map with.
\r
70 flg_has_req_metadata = True
\r
72 if 'T' not in metadata or\
\r
73 'G' not in metadata or\
\r
74 'M' not in metadata or\
\r
75 'S' not in metadata:
\r
76 flg_has_req_metadata = False
\r
78 return flg_has_req_metadata
\r
81 def is_real_player(events):
\r
83 Determines if a given set of player events correspond with a player who
\r
85 1) is not a bot (P event does not look like a bot)
\r
86 2) played in the game (matches 1)
\r
87 3) was present at the end of the game (scoreboardvalid 1)
\r
89 Returns True if the player meets the above conditions, and false otherwise.
\r
93 if not events['P'].startswith('bot'):
\r
94 # removing 'joins' here due to bug, but it should be here
\r
95 if 'matches' in events and 'scoreboardvalid' in events:
\r
101 def register_new_nick(session, player, new_nick):
\r
103 Change the player record's nick to the newly found nick. Store the old
\r
104 nick in the player_nicks table for that player.
\r
106 session - SQLAlchemy database session factory
\r
107 player - player record whose nick is changing
\r
108 new_nick - the new nickname
\r
110 # see if that nick already exists
\r
111 stripped_nick = strip_colors(player.nick)
\r
113 player_nick = session.query(PlayerNick).filter_by(
\r
114 player_id=player.player_id, stripped_nick=stripped_nick).one()
\r
115 except NoResultFound, e:
\r
116 # player_id/stripped_nick not found, create one
\r
117 # but we don't store "Anonymous Player #N"
\r
118 if not re.search('^Anonymous Player #\d+$', player.nick):
\r
119 player_nick = PlayerNick()
\r
120 player_nick.player_id = player.player_id
\r
121 player_nick.stripped_nick = stripped_nick
\r
122 player_nick.nick = player.nick
\r
123 session.add(player_nick)
\r
125 # We change to the new nick regardless
\r
126 player.nick = new_nick
\r
127 session.add(player)
\r
130 def get_or_create_server(session=None, name=None):
\r
132 Find a server by name or create one if not found. Parameters:
\r
134 session - SQLAlchemy database session factory
\r
135 name - server name of the server to be found or created
\r
138 # find one by that name, if it exists
\r
139 server = session.query(Server).filter_by(name=name).one()
\r
140 log.debug("Found server id {0}: {1}".format(
\r
141 server.server_id, server.name.encode('utf-8')))
\r
142 except NoResultFound, e:
\r
143 server = Server(name=name)
\r
144 session.add(server)
\r
146 log.debug("Created server id {0}: {1}".format(
\r
147 server.server_id, server.name.encode('utf-8')))
\r
148 except MultipleResultsFound, e:
\r
149 # multiple found, so use the first one but warn
\r
151 servers = session.query(Server).filter_by(name=name).order_by(
\r
152 Server.server_id).all()
\r
153 server = servers[0]
\r
154 log.debug("Created server id {0}: {1} but found \
\r
156 server.server_id, server.name.encode('utf-8')))
\r
160 def get_or_create_map(session=None, name=None):
\r
162 Find a map by name or create one if not found. Parameters:
\r
164 session - SQLAlchemy database session factory
\r
165 name - map name of the map to be found or created
\r
168 # find one by the name, if it exists
\r
169 gmap = session.query(Map).filter_by(name=name).one()
\r
170 log.debug("Found map id {0}: {1}".format(gmap.map_id,
\r
172 except NoResultFound, e:
\r
173 gmap = Map(name=name)
\r
176 log.debug("Created map id {0}: {1}".format(gmap.map_id,
\r
178 except MultipleResultsFound, e:
\r
179 # multiple found, so use the first one but warn
\r
181 gmaps = session.query(Map).filter_by(name=name).order_by(
\r
184 log.debug("Found map id {0}: {1} but found \
\r
185 multiple".format(gmap.map_id, gmap.name))
\r
190 def create_game(session=None, start_dt=None, game_type_cd=None,
\r
191 server_id=None, map_id=None, winner=None):
\r
193 Creates a game. Parameters:
\r
195 session - SQLAlchemy database session factory
\r
196 start_dt - when the game started (datetime object)
\r
197 game_type_cd - the game type of the game being played
\r
198 server_id - server identifier of the server hosting the game
\r
199 map_id - map on which the game was played
\r
200 winner - the team id of the team that won
\r
203 game = Game(start_dt=start_dt, game_type_cd=game_type_cd,
\r
204 server_id=server_id, map_id=map_id, winner=winner)
\r
207 log.debug("Created game id {0} on server {1}, map {2} at \
\r
208 {3}".format(game.game_id,
\r
209 server_id, map_id, start_dt))
\r
214 def get_or_create_player(session=None, hashkey=None, nick=None):
\r
216 Finds a player by hashkey or creates a new one (along with a
\r
217 corresponding hashkey entry. Parameters:
\r
219 session - SQLAlchemy database session factory
\r
220 hashkey - hashkey of the player to be found or created
\r
221 nick - nick of the player (in case of a first time create)
\r
224 if re.search('^bot#\d+$', hashkey):
\r
225 player = session.query(Player).filter_by(player_id=1).one()
\r
226 # if we have an untracked player
\r
227 elif re.search('^player#\d+$', hashkey):
\r
228 player = session.query(Player).filter_by(player_id=2).one()
\r
229 # else it is a tracked player
\r
231 # see if the player is already in the database
\r
232 # if not, create one and the hashkey along with it
\r
234 hashkey = session.query(Hashkey).filter_by(
\r
235 hashkey=hashkey).one()
\r
236 player = session.query(Player).filter_by(
\r
237 player_id=hashkey.player_id).one()
\r
238 log.debug("Found existing player {0} with hashkey {1}".format(
\r
239 player.player_id, hashkey.hashkey))
\r
242 session.add(player)
\r
245 # if nick is given to us, use it. If not, use "Anonymous Player"
\r
246 # with a suffix added for uniqueness.
\r
248 player.nick = nick[:128]
\r
250 player.nick = "Anonymous Player #{0}".format(player.player_id)
\r
252 hashkey = Hashkey(player_id=player.player_id, hashkey=hashkey)
\r
253 session.add(hashkey)
\r
254 log.debug("Created player {0} ({2}) with hashkey {1}".format(
\r
255 player.player_id, hashkey.hashkey, player.nick.encode('utf-8')))
\r
259 def create_player_game_stat(session=None, player=None,
\r
260 game=None, player_events=None):
\r
262 Creates game statistics for a given player in a given game. Parameters:
\r
264 session - SQLAlchemy session factory
\r
265 player - Player record of the player who owns the stats
\r
266 game - Game record for the game to which the stats pertain
\r
267 player_events - dictionary for the actual stats that need to be transformed
\r
270 # in here setup default values (e.g. if game type is CTF then
\r
271 # set kills=0, score=0, captures=0, pickups=0, fckills=0, etc
\r
272 # TODO: use game's create date here instead of now()
\r
273 pgstat = PlayerGameStat(create_dt=datetime.datetime.now())
\r
275 # set player id from player record
\r
276 pgstat.player_id = player.player_id
\r
278 #set game id from game record
\r
279 pgstat.game_id = game.game_id
\r
281 # all games have a score
\r
284 if game.game_type_cd == 'dm':
\r
287 pgstat.suicides = 0
\r
288 elif game.game_type_cd == 'ctf':
\r
290 pgstat.captures = 0
\r
294 pgstat.carrier_frags = 0
\r
296 for (key,value) in player_events.items():
\r
297 if key == 'n': pgstat.nick = value[:128]
\r
298 if key == 't': pgstat.team = value
\r
299 if key == 'rank': pgstat.rank = value
\r
300 if key == 'alivetime':
\r
301 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(value))))
\r
302 if key == 'scoreboard-drops': pgstat.drops = value
\r
303 if key == 'scoreboard-returns': pgstat.returns = value
\r
304 if key == 'scoreboard-fckills': pgstat.carrier_frags = value
\r
305 if key == 'scoreboard-pickups': pgstat.pickups = value
\r
306 if key == 'scoreboard-caps': pgstat.captures = value
\r
307 if key == 'scoreboard-score': pgstat.score = value
\r
308 if key == 'scoreboard-deaths': pgstat.deaths = value
\r
309 if key == 'scoreboard-kills': pgstat.kills = value
\r
310 if key == 'scoreboard-suicides': pgstat.suicides = value
\r
312 # check to see if we had a name, and if
\r
313 # not use the name from the player id
\r
314 if pgstat.nick == None:
\r
315 pgstat.nick = player.nick
\r
317 # if the nick we end up with is different from the one in the
\r
318 # player record, change the nick to reflect the new value
\r
319 if pgstat.nick != player.nick and player.player_id > 1:
\r
320 register_new_nick(session, player, pgstat.nick)
\r
322 # if the player is ranked #1 and it is a team game, set the game's winner
\r
323 # to be the team of that player
\r
324 # FIXME: this is a hack, should be using the 'W' field (not present)
\r
325 if pgstat.rank == '1' and pgstat.team:
\r
326 game.winner = pgstat.team
\r
329 session.add(pgstat)
\r
335 def create_player_weapon_stats(session=None, player=None,
\r
336 game=None, pgstat=None, player_events=None):
\r
338 Creates accuracy records for each weapon used by a given player in a
\r
339 given game. Parameters:
\r
341 session - SQLAlchemy session factory object
\r
342 player - Player record who owns the weapon stats
\r
343 game - Game record in which the stats were created
\r
344 pgstat - Corresponding PlayerGameStat record for these weapon stats
\r
345 player_events - dictionary containing the raw weapon values that need to be
\r
350 for (key,value) in player_events.items():
\r
351 matched = re.search("acc-(.*?)-cnt-fired", key)
\r
353 weapon_cd = matched.group(1)
\r
354 pwstat = PlayerWeaponStat()
\r
355 pwstat.player_id = player.player_id
\r
356 pwstat.game_id = game.game_id
\r
357 pwstat.player_game_stat_id = pgstat.player_game_stat_id
\r
358 pwstat.weapon_cd = weapon_cd
\r
360 if 'n' in player_events:
\r
361 pwstat.nick = player_events['n']
\r
363 pwstat.nick = player_events['P']
\r
365 if 'acc-' + weapon_cd + '-cnt-fired' in player_events:
\r
366 pwstat.fired = int(round(float(
\r
367 player_events['acc-' + weapon_cd + '-cnt-fired'])))
\r
368 if 'acc-' + weapon_cd + '-fired' in player_events:
\r
369 pwstat.max = int(round(float(
\r
370 player_events['acc-' + weapon_cd + '-fired'])))
\r
371 if 'acc-' + weapon_cd + '-cnt-hit' in player_events:
\r
372 pwstat.hit = int(round(float(
\r
373 player_events['acc-' + weapon_cd + '-cnt-hit'])))
\r
374 if 'acc-' + weapon_cd + '-hit' in player_events:
\r
375 pwstat.actual = int(round(float(
\r
376 player_events['acc-' + weapon_cd + '-hit'])))
\r
377 if 'acc-' + weapon_cd + '-frags' in player_events:
\r
378 pwstat.frags = int(round(float(
\r
379 player_events['acc-' + weapon_cd + '-frags'])))
\r
381 session.add(pwstat)
\r
382 pwstats.append(pwstat)
\r
387 def parse_body(request):
\r
389 Parses the POST request body for a stats submission
\r
391 # storage vars for the request body
\r
394 current_team = None
\r
397 log.debug("----- BEGIN REQUEST BODY -----")
\r
398 log.debug(request.body)
\r
399 log.debug("----- END REQUEST BODY -----")
\r
401 for line in request.body.split('\n'):
\r
403 (key, value) = line.strip().split(' ', 1)
\r
405 # Server (S) and Nick (n) fields can have international characters.
\r
406 # We encode these as UTF-8.
\r
408 value = unicode(value, 'utf-8')
\r
410 if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W':
\r
411 game_meta[key] = value
\r
414 # if we were working on a player record already, append
\r
415 # it and work on a new one (only set team info)
\r
416 if len(player_events) != 0:
\r
417 players.append(player_events)
\r
420 player_events[key] = value
\r
423 (subkey, subvalue) = value.split(' ', 1)
\r
424 player_events[subkey] = subvalue
\r
426 player_events[key] = value
\r
428 player_events[key] = value
\r
430 # no key/value pair - move on to the next line
\r
433 # add the last player we were working on
\r
434 if len(player_events) > 0:
\r
435 players.append(player_events)
\r
437 return (game_meta, players)
\r
440 def create_player_stats(session=None, player=None, game=None,
\r
441 player_events=None):
\r
443 Creates player game and weapon stats according to what type of player
\r
445 pgstat = create_player_game_stat(session=session,
\r
446 player=player, game=game, player_events=player_events)
\r
448 #TODO: put this into a config setting in the ini file?
\r
449 if not re.search('^bot#\d+$', player_events['P']):
\r
450 create_player_weapon_stats(session=session,
\r
451 player=player, game=game, pgstat=pgstat,
\r
452 player_events=player_events)
\r
455 def stats_submit(request):
\r
457 Entry handler for POST stats submissions.
\r
460 if not is_verified_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, 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